-
-
- {
- this.setRegistration(WinShell.pathUser, e.target.checked)
- }} />
- Add Pulsar to PATH (User Install)
-
- Add Pulsar to Windows PATH to enable CLI usage.
-
-
-
+ return (
+
+
+
+
+ {
+ this.setRegistration(WinShell.pathUser, e.target.checked)
+ }} />
+ Add Pulsar to PATH
+
+ Add Pulsar to Windows PATH to enable CLI usage. (May require a reboot to take effect.)
+
+
- );
- } else {
- return (
-
-
-
-
- {
- this.setRegistration(WinShell.pathMachine, e.target.checked)
- }} />
- Add Pulsar to PATH (Machine Install)
-
- Add Pulsar to Windows PATH for machine installs. Requires administrative privileges.
-
-
-
-
-
- );
- }
+
+ );
}
focus () {
diff --git a/packages/settings-view/lib/themes-panel.js b/packages/settings-view/lib/themes-panel.js
index f063c95d8..599116621 100644
--- a/packages/settings-view/lib/themes-panel.js
+++ b/packages/settings-view/lib/themes-panel.js
@@ -1,7 +1,6 @@
/** @babel */
/** @jsx etch.dom */
-import fuzzaldrin from 'fuzzaldrin'
import etch from 'etch'
import _ from 'underscore-plus'
import {CompositeDisposable, TextEditor} from 'atom'
@@ -356,7 +355,7 @@ export default class ThemesPanel extends CollapsibleSectionPanel {
} else {
const owner = pack.owner != null ? pack.owner : ownerFromRepository(pack.repository)
const filterText = `${pack.name} ${owner}`
- return fuzzaldrin.score(filterText, text) > 0
+ return atom.ui.fuzzyMatcher.score(filterText, text) > 0
}
})
diff --git a/packages/settings-view/package.json b/packages/settings-view/package.json
index af4248cc1..6826a02fc 100644
--- a/packages/settings-view/package.json
+++ b/packages/settings-view/package.json
@@ -42,13 +42,10 @@
},
"dependencies": {
"async": "^3.2.0",
- "dompurify": "^2.0.17",
"etch": "0.9.0",
"fs-plus": "^3.0.0",
- "fuzzaldrin": "^2.1",
"glob": "5.0.15",
"hosted-git-info": "^2.8.9",
- "marked": "5.0.3",
"request": "^2.83.0",
"season": "^6.0.2",
"semver": "^5.3.0",
diff --git a/packages/settings-view/spec/package-detail-view-spec.js b/packages/settings-view/spec/package-detail-view-spec.js
index c0314c712..9caa9bb29 100644
--- a/packages/settings-view/spec/package-detail-view-spec.js
+++ b/packages/settings-view/spec/package-detail-view-spec.js
@@ -112,7 +112,7 @@ describe("PackageDetailView", function() {
expect(view.element.querySelectorAll('.package-readme iframe').length).toBe(0);
expect(view.element.querySelectorAll('.package-readme input[type="checkbox"][disabled]').length).toBe(2);
expect(view.element.querySelector('img[alt="AbsoluteImage"]').getAttribute('src')).toBe('https://example.com/static/image.jpg');
- expect(view.element.querySelector('img[alt="RelativeImage"]').getAttribute('src')).toBe('https://github.com/example/package-with-readme/blob/master/static/image.jpg');
+ expect(view.element.querySelector('img[alt="RelativeImage"]').getAttribute('src')).toBe('https://github.com/example/package-with-readme/raw/HEAD/static/image.jpg');
expect(view.element.querySelector('img[alt="Base64Image"]').getAttribute('src')).toBe('data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==');
});
diff --git a/packages/settings-view/spec/rich-description-spec.js b/packages/settings-view/spec/rich-description-spec.js
index 7445a08c7..d79f4b566 100644
--- a/packages/settings-view/spec/rich-description-spec.js
+++ b/packages/settings-view/spec/rich-description-spec.js
@@ -139,9 +139,9 @@ Description without table
it('handles inline code', () => expect(getSettingDescription('foo.inlineCode')).toEqual('Description
with
inline code'));
- it('handles line breaks', () => expect(getSettingDescription('foo.lineBreak')).toEqual('Description with
line break'));
+ it('handles line breaks', () => expect(getSettingDescription('foo.lineBreak')).toEqual('Description with
line break'));
- it('handles strikethrough', () => expect(getSettingDescription('foo.strikethrough')).toEqual('Description
with strikethrough'));
+ it('handles strikethrough', () => expect(getSettingDescription('foo.strikethrough')).toEqual('Description
with strikethrough'));
});
describe('unsupported Markdown', () => {
diff --git a/packages/solarized-dark-syntax/index.less b/packages/solarized-dark-syntax/index.less
index 66afd1ccf..b1a8c3699 100644
--- a/packages/solarized-dark-syntax/index.less
+++ b/packages/solarized-dark-syntax/index.less
@@ -12,6 +12,7 @@
@import "styles/syntax-legacy/css.less";
// @import "styles/syntax-legacy/go.less";
@import "styles/syntax-legacy/java.less";
+@import "styles/syntax-legacy/json.less";
// @import "styles/syntax-legacy/javascript.less";
@import "styles/syntax-legacy/markdown.less";
@import "styles/syntax-legacy/markup.less";
diff --git a/packages/solarized-dark-syntax/styles/syntax-legacy/json.less b/packages/solarized-dark-syntax/styles/syntax-legacy/json.less
new file mode 100644
index 000000000..cdfa507c0
--- /dev/null
+++ b/packages/solarized-dark-syntax/styles/syntax-legacy/json.less
@@ -0,0 +1,11 @@
+
+.syntax--source.syntax--json {
+
+ // Color JSON keys differently from other strings.
+ .syntax--meta.syntax--structure.syntax--key {
+ .syntax--string.syntax--quoted.syntax--double {
+ color: @blue;
+ }
+ }
+
+}
diff --git a/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less b/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less
index dd798c269..dc58be982 100644
--- a/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less
+++ b/packages/solarized-dark-syntax/styles/syntax-legacy/markup.less
@@ -37,4 +37,10 @@
color: @syntax-comment-color;
font-style: italic;
}
+
+ // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
+ // we assign the color of a comment to this new scope.
+ &.syntax--horizontal-rule {
+ color: @syntax-comment-color;
+ }
}
diff --git a/packages/solarized-light-syntax/index.less b/packages/solarized-light-syntax/index.less
index 66afd1ccf..b1a8c3699 100644
--- a/packages/solarized-light-syntax/index.less
+++ b/packages/solarized-light-syntax/index.less
@@ -12,6 +12,7 @@
@import "styles/syntax-legacy/css.less";
// @import "styles/syntax-legacy/go.less";
@import "styles/syntax-legacy/java.less";
+@import "styles/syntax-legacy/json.less";
// @import "styles/syntax-legacy/javascript.less";
@import "styles/syntax-legacy/markdown.less";
@import "styles/syntax-legacy/markup.less";
diff --git a/packages/solarized-light-syntax/styles/syntax-legacy/json.less b/packages/solarized-light-syntax/styles/syntax-legacy/json.less
new file mode 100644
index 000000000..cdfa507c0
--- /dev/null
+++ b/packages/solarized-light-syntax/styles/syntax-legacy/json.less
@@ -0,0 +1,11 @@
+
+.syntax--source.syntax--json {
+
+ // Color JSON keys differently from other strings.
+ .syntax--meta.syntax--structure.syntax--key {
+ .syntax--string.syntax--quoted.syntax--double {
+ color: @blue;
+ }
+ }
+
+}
diff --git a/packages/solarized-light-syntax/styles/syntax-legacy/markup.less b/packages/solarized-light-syntax/styles/syntax-legacy/markup.less
index dd798c269..dc58be982 100644
--- a/packages/solarized-light-syntax/styles/syntax-legacy/markup.less
+++ b/packages/solarized-light-syntax/styles/syntax-legacy/markup.less
@@ -37,4 +37,10 @@
color: @syntax-comment-color;
font-style: italic;
}
+
+ // Horizontal rules in GFM used to be scoped as `comment.hr`. For continuity,
+ // we assign the color of a comment to this new scope.
+ &.syntax--horizontal-rule {
+ color: @syntax-comment-color;
+ }
}
diff --git a/packages/spell-check/spec/.eslintrc.js b/packages/spell-check/spec/.eslintrc.js
new file mode 100644
index 000000000..5226d6921
--- /dev/null
+++ b/packages/spell-check/spec/.eslintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+ env: { jasmine: true },
+ rules: {
+ "node/no-unpublished-require": "off",
+ "node/no-extraneous-require": "off",
+ "no-unused-vars": "off",
+ "no-empty": "off"
+ }
+};
diff --git a/packages/spell-check/spec/spell-check-spec.js b/packages/spell-check/spec/spell-check-spec.js
index 5a1d2487a..cdc32c18f 100644
--- a/packages/spell-check/spec/spell-check-spec.js
+++ b/packages/spell-check/spec/spell-check-spec.js
@@ -12,7 +12,7 @@ const {
} = require('./async-spec-helpers');
describe('Spell check', function () {
- let workspaceElement, editor, editorElement, spellCheckModule;
+ let workspaceElement, editor, editorElement, spellCheckModule, languageMode;
const textForMarker = (marker) =>
editor.getTextInBufferRange(marker.getBufferRange());
@@ -42,9 +42,14 @@ describe('Spell check', function () {
jasmine.attachToDOM(workspaceElement);
editor = atom.workspace.getActiveTextEditor();
editorElement = atom.views.getView(editor);
+ languageMode = editor.getBuffer().getLanguageMode();
+ languageMode.useAsyncParsing = false;
});
- afterEach(() => SpellCheckTask.clear());
+ afterEach(async () => {
+ await languageMode.atTransactionEnd();
+ SpellCheckTask.clear();
+ });
it('decorates all misspelled words', async function () {
atom.config.set('spell-check.useLocales', true);
@@ -72,11 +77,11 @@ describe('Spell check', function () {
expect(textForMarker(misspellingMarkers[1])).toEqual('bok');
});
- it('allows certains scopes to be excluded from spell checking', async function () {
+ it('allows certain scopes to be excluded from spell checking', async function () {
editor.setText(
- 'speledWrong = 5;\n' +
- 'function speledWrong() {}\n' +
- 'class SpeledWrong {}'
+`speledWrong = 5;
+function speledWrong() {}
+class SpeledWrong {}`
);
atom.config.set('spell-check.useLocales', true);
atom.config.set('spell-check.grammars', [
@@ -121,7 +126,7 @@ describe('Spell check', function () {
}
{
- atom.config.set('spell-check.excludedScopes', ['.meta.class']);
+ atom.config.set('spell-check.excludedScopes', ['.entity.name.type.class']);
await conditionPromise(() => getMisspellingMarkers().length === 2);
const markers = getMisspellingMarkers();
expect(markers.map((marker) => marker.getBufferRange())).toEqual([
diff --git a/packages/styleguide/spec/.eslintrc.js b/packages/styleguide/spec/.eslintrc.js
new file mode 100644
index 000000000..5226d6921
--- /dev/null
+++ b/packages/styleguide/spec/.eslintrc.js
@@ -0,0 +1,9 @@
+module.exports = {
+ env: { jasmine: true },
+ rules: {
+ "node/no-unpublished-require": "off",
+ "node/no-extraneous-require": "off",
+ "no-unused-vars": "off",
+ "no-empty": "off"
+ }
+};
diff --git a/packages/symbol-provider-ctags/.eslintrc.js b/packages/symbol-provider-ctags/.eslintrc.js
new file mode 100644
index 000000000..00404ea92
--- /dev/null
+++ b/packages/symbol-provider-ctags/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+ env: {
+ browser: true,
+ commonjs: true,
+ es2021: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:node/recommended",
+ ],
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: "latest"
+ },
+ rules: {
+ "no-fallthrough": "off",
+ "no-case-declarations": "off",
+ "space-before-function-paren": ["error", {
+ anonymous: "always",
+ asyncArrow: "always",
+ named: "never"
+ }],
+ "node/no-unpublished-require": [
+ "error",
+ {
+ allowModules: ["electron"]
+ }
+ ],
+ "node/no-missing-require": [
+ "error",
+ {
+ allowModules: ["atom"]
+ }
+ ]
+ },
+ plugins: [
+ "jsdoc"
+ ],
+ globals: {
+ atom: "writeable"
+ }
+};
diff --git a/packages/symbol-provider-ctags/LICENSE.md b/packages/symbol-provider-ctags/LICENSE.md
new file mode 100644
index 000000000..fd1a7ace7
--- /dev/null
+++ b/packages/symbol-provider-ctags/LICENSE.md
@@ -0,0 +1,21 @@
+Copyright (c) 2023 Pulsar-Edit
+Original work copyright (c) 2011-2022 GitHub Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/symbol-provider-ctags/README.md b/packages/symbol-provider-ctags/README.md
new file mode 100644
index 000000000..478db1c10
--- /dev/null
+++ b/packages/symbol-provider-ctags/README.md
@@ -0,0 +1,24 @@
+# symbol-provider-ctags package
+
+Provides symbols to `symbols-view` via `ctags`.
+
+This is the approach historically used by `symbols-view` βΒ now spun out into its own βproviderβ package among several.
+
+This symbol provider will typically be used on non-Tree-sitter grammars, and possibly when performing a project-wide search. Symbol-based navigation on files with Tree-sitter grammars will typically be provided by `symbol-provider-tree-sitter`.
+
+## Language support
+
+This provider supports any language that is present in its config file, and detects any symbols that match the specified patterns. If your language isnβt supported and you can help add support, weβll happily accept a pull request.
+
+## Toggle file symbols
+
+For the **Symbols View: Toggle File Symbols** command, `ctags` will scan the file on disk and emit its tag information to stdout, where it is read by this package. You donβt need a `TAGS` file to do a symbol search within a single file.
+
+## Toggle Project Symbols, Go To Declaration
+
+These commands require a tags file, typically defined at `.tags`/`tags`/`.TAGS`/`TAGS` in the root of your project. This package cannot generate (or regenerate) your tags file, since it doesnβt know which files to include. You can run `ctags` regularly on your own to generate this file. Consult [the documentation for Exuberant Ctags](https://ctags.sourceforge.net/ctags.html) for more information.
+
+Once your tags file is present, these commands can be fulfilled by `symbol-provider-ctags`β¦
+
+* The **Symbols View: Toggle Project Symbols** command works like **Symbols View: Toggle File Symbols** described above, except itβll show you symbols from the entire project.
+* The **Symbols View: Go To Declaration** command works like **Symbols View: Toggle Project Symbols**, except the word under the cursor will be pre-filled in the search box, and a result will automatically be opened if it is the only result.
diff --git a/packages/symbol-provider-ctags/lib/ctags-config b/packages/symbol-provider-ctags/lib/ctags-config
new file mode 100644
index 000000000..a9a9d8d8f
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/ctags-config
@@ -0,0 +1,222 @@
+--langdef=CoffeeScript
+--langmap=CoffeeScript:.coffee
+--regex-CoffeeScript=/^[ \t]*(@?[a-zA-Z$_\.0-9]+)[ \t]*(=|\:)[ \t]*(\(.*\))?[ \t]*(-|=)>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*([a-zA-Z$_0-9]+\:\:[a-zA-Z$_\.0-9]+)[ \t]*(=|\:)[ \t]*(\(.*\))?[ \t]*(-|=)>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*describe[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*describe[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*it[ \t]"([^"]+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*it[ \t]'([^']+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-CoffeeScript=/^[ \t]*f+describe[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/focused\: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*f+describe[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/focused: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*f+it[ \t]"([^"]+)"[ \t]*,[ \t]+[-=]>/focused: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*f+it[ \t]'([^']+)'[ \t]*,[ \t]+[-=]>/focused: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*xdescribe[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/disabled\: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*xdescribe[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*xit[ \t]"([^"]+)"[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*xit[ \t]'([^']+)'[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+--regex-CoffeeScript=/^[ \t]*class[ \t]*([a-zA-Z$_\.0-9]+)[ \t]*/\1/f,function/
+
+--langdef=ColdFusion
+--langmap=ColdFusion:.cfc
+--langmap=ColdFusion:+.cfm
+--langmap=ColdFusion:+.cfml
+--regex-ColdFusion=/(,|(;|^)[ \t]*(var|([A-Za-z_$][A-Za-z0-9_$.]*\.)*))[ \t]*([A-Za-z0-9_$]+)[ \t]*=[ \t]*function[ \t]*\(/\5/,function/
+--regex-ColdFusion=/function[ \t]+([A-Za-z0-9_$]+)[ \t]*\([^)]*\)/\1/,function/
+--regex-ColdFusion=/cffunction[ \t]+([A-Za-z0-9_$]+)[ \t]*\([^)]*\)/\1/,cffunction/
+--regex-ColdFusion=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*:[ \t]*function[ \t]*\(/\2/,function/
+--regex-ColdFusion=/(,|^|\*\/)[ \t]*(static[ \t]+)?(while|if|for|function|switch|with|([A-Za-z_$][A-Za-z0-9_$]+))[ \t]*\(.*\)[ \t]*\{/\2\4/,function/
+--regex-ColdFusion=/(,|^|\*\/)[ \t]*get[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*\)[ \t]*\{/get \2/,function/
+--regex-ColdFusion=/(,|^|\*\/)[ \t]*set[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$][A-Za-z0-9_$]+)?[ \t]*\)[ \t]*\{/set \2/,function/
+--regex-ColdFusion=/(,|^|\*\/)[ \t]*async[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$].+)?[ \t]*\)[ \t]*\{/\2/,function/
+--regex-ColdFusion=/component[ \t]+([A-Za-z0-9._$]+)[ \t]*/\1/c,component/
+--regex-ColdFusion=/^[ \t]*given[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*given[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*story[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*story[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*feature[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*feature[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*when[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*when[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*then[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*then[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*describe[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*describe[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*it[ \t]"([^"]+)"[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*it[ \t]'([^']+)'[ \t]*,[ \t]+[-=]>/\1/f,function/
+--regex-ColdFusion=/^[ \t]*xdescribe[ \t]"(.+)"[ \t]*,[ \t]+[-=]>/disabled\: \1/f,function/
+--regex-ColdFusion=/^[ \t]*xdescribe[ \t]'(.+)'[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+--regex-ColdFusion=/^[ \t]*xit[ \t]"([^"]+)"[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+--regex-ColdFusion=/^[ \t]*xit[ \t]'([^']+)'[ \t]*,[ \t]+[-=]>/disabled: \1/f,function/
+
+--langdef=Css
+--langmap=Css:.css
+--langmap=Css:+.less
+--langmap=Css:+.scss
+--regex-Css=/^[ \t]*(.+)[ \t]*\{/\1/f,function/
+--regex-Css=/^[ \t]*(.+)[ \t]*,[ \t]*$/\1/f,function/
+--regex-Css=/^[ \t]*[@$]([a-zA-Z$_][-a-zA-Z$_0-9]*)[ \t]*:/\1/f,function/
+
+--langdef=Sass
+--langmap=Sass:.sass
+--regex-Sass=/^[ \t]*([#.]*[a-zA-Z_0-9]+)[ \t]*$/\1/f,function/
+
+--langdef=Yaml
+--langmap=Yaml:.yaml
+--langmap=Yaml:+.yml
+--regex-Yaml=/^[ \t]*([a-zA-Z_0-9 ]+)[ \t]*\:[ \t]*/\1/f,function/
+
+--regex-Html=/^[ \t]*<([a-zA-Z]+)[ \t]*.*>/\1/f,function/
+
+--langdef=Markdown
+--langmap=Markdown:.md
+--langmap=Markdown:+.markdown
+--langmap=Markdown:+.mdown
+--langmap=Markdown:+.mkd
+--langmap=Markdown:+.mkdown
+--langmap=Markdown:+.ron
+--regex-Markdown=/^#+[ \t]*([^#]+)/\1/f,function/
+
+--langdef=Json
+--langmap=Json:.json
+--regex-Json=/^[ \t]*"([^"]+)"[ \t]*\:/\1/f,function/
+
+--langdef=Cson
+--langmap=Cson:.cson
+--langmap=Cson:+.gyp
+--regex-Cson=/^[ \t]*'([^']+)'[ \t]*\:/\1/f,function/
+--regex-Cson=/^[ \t]*"([^"]+)"[ \t]*\:/\1/f,function/
+--regex-Cson=/^[ \t]*([^'"]+)[ \t]*\:/\1/f,function/
+
+--langmap=C++:+.mm
+
+--langmap=Ruby:+(Rakefile)
+
+--langmap=Php:+.module
+
+--langdef=Go
+--langmap=Go:.go
+--regex-Go=/func([ \t]+\([^)]+\))?[ \t]+([a-zA-Z0-9_]+)/\2/f,func/
+--regex-Go=/var[ \t]+([a-zA-Z_][a-zA-Z0-9_]*)/\1/v,var/
+--regex-Go=/type[ \t]+([a-zA-Z_][a-zA-Z0-9_]*)/\1/t,type/
+
+--langdef=Capnp
+--langmap=Capnp:.capnp
+--regex-Capnp=/struct[ \t]+([A-Za-z]+)/\1/s,struct/
+--regex-Capnp=/enum[ \t]+([A-Za-z]+)/\1/e,enum/
+--regex-Capnp=/using[ \t]+([A-Za-z]+)[ \t]+=[ \t]+import/\1/u,using/
+--regex-Capnp=/const[ \t]+([A-Za-z]+)/\1/c,const/
+
+--langmap=perl:+.pod
+--regex-perl=/with[ \t]+([^;]+)[ \t]*?;/\1/w,role,roles/
+--regex-perl=/extends[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/
+--regex-perl=/use[ \t]+base[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/
+--regex-perl=/use[ \t]+parent[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/
+--regex-perl=/Mojo::Base[ \t]+['"]([^'"]+)['"][ \t]*?;/\1/e,extends/
+--regex-perl=/^[ \t]*?use[ \t]+([^;]+)[ \t]*?;/\1/u,use,uses/
+--regex-perl=/^[ \t]*?require[ \t]+((\w|\:)+)/\1/r,require,requires/
+--regex-perl=/^[ \t]*?has[ \t]+['"]?(\w+)['"]?/\1/a,attribute,attributes/
+--regex-perl=/^[ \t]*?\*(\w+)[ \t]*?=/\1/a,alias,aliases/
+--regex-perl=/->helper\([ \t]?['"]?(\w+)['"]?/\1/h,helper,helpers/
+--regex-perl=/^[ \t]*?our[ \t]*?[\$@%](\w+)/\1/o,our,ours/
+--regex-perl=/^\=head1[ \t]+(.+)/\1/p,pod,Plain Old Documentation/
+--regex-perl=/^\=head2[ \t]+(.+)/-- \1/p,pod,Plain Old Documentation/
+--regex-perl=/^\=head[3-5][ \t]+(.+)/---- \1/p,pod,Plain Old Documentation/
+
+--regex-JavaScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)*))[ \t]*([A-Za-z0-9_$]+)[ \t]*=[ \t]*function[ \t]*\(/\5/,function/
+--regex-JavaScript=/function[ \t]+([A-Za-z0-9_$]+)[ \t]*\([^)]*\)/\1/,function/
+--regex-JavaScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*:[ \t]*function[ \t]*\(/\2/,function/
+--regex-JavaScript=/(,|^|\*\/)[ \t]*(static[ \t]+)?(while|if|for|function|switch|with|([A-Za-z_$][A-Za-z0-9_$]+))[ \t]*\(.*\)[ \t]*\{/\2\4/,function/
+--regex-JavaScript=/(,|^|\*\/)[ \t]*get[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*\)[ \t]*\{/get \2/,function/
+--regex-JavaScript=/(,|^|\*\/)[ \t]*set[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$][A-Za-z0-9_$]+)?[ \t]*\)[ \t]*\{/set \2/,function/
+--regex-JavaScript=/(,|^|\*\/)[ \t]*async[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$].+)?[ \t]*\)[ \t]*\{/\2/,function/
+--regex-JavaScript=/class[ \t]+([A-Za-z0-9._$]+)[ \t]*/\1/c,class/
+--regex-JavaScript=/^[ \t]*describe\("([^"]+)"[ \t]*,/\1/f,function/
+--regex-JavaScript=/^[ \t]*describe\('([^']+)'[ \t]*,/\1/f,function/
+--regex-JavaScript=/^[ \t]*it\("([^"]+)"[ \t]*,/\1/f,function/
+--regex-JavaScript=/^[ \t]*it\('([^']+)'[ \t]*,/\1/f,function/
+--regex-JavaScript=/^[ \t]*f+describe\('([^']+)'[ \t]*,/focused: \1/f,function/
+--regex-JavaScript=/^[ \t]*f+describe\("([^"]+)"[ \t]*,/focused: \1/f,function/
+--regex-JavaScript=/^[ \t]*f+it\('([^']+)'[ \t]*,/focused: \1/f,function/
+--regex-JavaScript=/^[ \t]*f+it\("([^"]+)"[ \t]*,/focused: \1/f,function/
+--regex-JavaScript=/^[ \t]*xdescribe\('([^']+)'[ \t]*,/disabled: \1/f,function/
+--regex-JavaScript=/^[ \t]*xdescribe\("([^"]+)"[ \t]*,/disabled: \1/f,function/
+--regex-JavaScript=/^[ \t]*xit\('([^']+)'[ \t]*,/disabled: \1/f,function/
+--regex-JavaScript=/^[ \t]*xit\("([^"]+)"[ \t]*,/disabled: \1/f,function/
+
+--langdef=TypeScript
+--langmap=TypeScript:.ts
+--langmap=TypeScript:+.tsx
+--regex-TypeScript=/(,|(;|^)[ \t]*(var|let|([A-Za-z_$][A-Za-z0-9_$.]*\.)*))[ \t]*([A-Za-z0-9_$]+)[ \t]*=[ \t]*function[ \t]*\(/\5/,function/
+--regex-TypeScript=/function[ \t]+([A-Za-z0-9_$]+)[ \t]*\([^)]*\)/\1/,function/
+--regex-TypeScript=/(,|^|\*\/)[ \t]*([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*:[ \t]*function[ \t]*\(/\2/,function/
+--regex-TypeScript=/(,|^|\*\/)[ \t]*(static[ \t]+)?(while|if|for|function|switch|with|([A-Za-z_$][A-Za-z0-9_$]+))[ \t]*\(.*\)[ \t]*\{/\2\4/,function/
+--regex-TypeScript=/(,|^|\*\/)[ \t]*get[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*\)[ \t]*\{/get \2/,function/
+--regex-TypeScript=/(,|^|\*\/)[ \t]*set[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$][A-Za-z0-9_$]+)?[ \t]*\)[ \t]*\{/set \2/,function/
+--regex-TypeScript=/(,|^|\*\/)[ \t]*async[ \t]+([A-Za-z_$][A-Za-z0-9_$]+)[ \t]*\([ \t]*([A-Za-z_$].+)?[ \t]*\)[ \t]*\{/\2/,function/
+--regex-TypeScript=/class[ \t]+([A-Za-z0-9._$]+)[ \t]*/\1/c,class/
+--regex-TypeScript=/^[ \t]*describe\("([^"]+)"[ \t]*,/\1/f,function/
+--regex-TypeScript=/^[ \t]*describe\('([^']+)'[ \t]*,/\1/f,function/
+--regex-TypeScript=/^[ \t]*it\("([^"]+)"[ \t]*,/\1/f,function/
+--regex-TypeScript=/^[ \t]*it\('([^']+)'[ \t]*,/\1/f,function/
+--regex-TypeScript=/^[ \t]*f+describe\('([^']+)'[ \t]*,/focused: \1/f,function/
+--regex-TypeScript=/^[ \t]*f+describe\("([^"]+)"[ \t]*,/focused: \1/f,function/
+--regex-TypeScript=/^[ \t]*f+it\('([^']+)'[ \t]*,/focused: \1/f,function/
+--regex-TypeScript=/^[ \t]*f+it\("([^"]+)"[ \t]*,/focused: \1/f,function/
+--regex-TypeScript=/^[ \t]*xdescribe\('([^']+)'[ \t]*,/disabled: \1/f,function/
+--regex-TypeScript=/^[ \t]*xdescribe\("([^"]+)"[ \t]*,/disabled: \1/f,function/
+--regex-TypeScript=/^[ \t]*xit\('([^']+)'[ \t]*,/disabled: \1/f,function/
+--regex-TypeScript=/^[ \t]*xit\("([^"]+)"[ \t]*,/disabled: \1/f,function/
+
+--langdef=haxe
+--langmap=haxe:.hx
+--regex-haxe=/^package[ \t]+([A-Za-z0-9_.]+)/\1/p,package/
+--regex-haxe=/^[ \t]*[(@:macro|private|public|static|override|inline|dynamic)( \t)]*function[ \t]+([A-Za-z0-9_]+)/\1/f,function/
+--regex-haxe=/^[ \t]*([private|public|static|protected|inline][ \t]*)+var[ \t]+([A-Za-z0-9_]+)/\2/v,variable/
+--regex-haxe=/^[ \t]*package[ \t]*([A-Za-z0-9_]+)/\1/p,package/
+--regex-haxe=/^[ \t]*(extern[ \t]*|@:native\([^)]*\)[ \t]*)*class[ \t]+([A-Za-z0-9_]+)[ \t]*[^\{]*/\2/c,class/
+--regex-haxe=/^[ \t]*(extern[ \t]+)?interface[ \t]+([A-Za-z0-9_]+)/\2/i,interface/
+--regex-haxe=/^[ \t]*typedef[ \t]+([A-Za-z0-9_]+)/\1/t,typedef/
+--regex-haxe=/^[ \t]*enum[ \t]+([A-Za-z0-9_]+)/\1/t,typedef/
+--regex-haxe=/^[ \t]*+([A-Za-z0-9_]+)(;|\([^)]*:[^)]*\))/\1/t,enum_field/
+
+--langdef=Elixir
+--langmap=Elixir:.ex.exs
+--regex-Elixir=/^[ \t]*def(p?)[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\2/f,functions,functions (def ...)/
+--regex-Elixir=/^[ \t]*defcallback[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\1/c,callbacks,callbacks (defcallback ...)/
+--regex-Elixir=/^[ \t]*defdelegate[ \t]+([a-z_][a-zA-Z0-9_?!]*)/\1/d,delegates,delegates (defdelegate ...)/
+--regex-Elixir=/^[ \t]*defexception[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/e,exceptions,exceptions (defexception ...)/
+--regex-Elixir=/^[ \t]*defimpl[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/i,implementations,implementations (defimpl ...)/
+--regex-Elixir=/^[ \t]*defmacro(p?)[ \t]+([a-z_][a-zA-Z0-9_?!]*)\(/\2/a,macros,macros (defmacro ...)/
+--regex-Elixir=/^[ \t]*defmacro(p?)[ \t]+([a-zA-Z0-9_?!]+)?[ \t]+([^ \tA-Za-z0-9_]+)[ \t]*[a-zA-Z0-9_!?!]/\3/o,operators,operators (e.g. "defmacro a <<< b")/
+--regex-Elixir=/^[ \t]*defmodule[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/m,modules,modules (defmodule ...)/
+--regex-Elixir=/^[ \t]*defprotocol[ \t]+([A-Z][a-zA-Z0-9_]*\.)*([A-Z][a-zA-Z0-9_?!]*)/\2/p,protocols,protocols (defprotocol...)/
+--regex-Elixir=/^[ \t]*Record\.defrecord[ \t]+:([a-zA-Z0-9_]+)/\1/r,records,records (defrecord...)/
+
+--langdef=Nim
+--langmap=Nim:.nim
+--regex-Nim=/^[\t\s]*proc\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/f,function/
+--regex-Nim=/^[\t\s]*iterator\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/i,iterator/
+--regex-Nim=/^[\t\s]*macro\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/m,macro/
+--regex-Nim=/^[\t\s]*method\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/h,method/
+--regex-Nim=/^[\t\s]*template\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/t,generics/
+--regex-Nim=/^[\t\s]*converter\s+([_A-Za-z0-9]+)\**(\[\w+(\:\s+\w+)?\])?\s*\(/\1/c,converter/
+
+--langdef=Fountain
+--langmap=Fountain:.fountain
+--langmap=Fountain:+.ftn
+--regex-Fountain=/^(([iI][nN][tT]|[eE][xX][tT]|[^\w][eE][sS][tT]|\.|[iI]\.?\/[eE]\.?)([^\n]+))/\1/f,function/
+
+--langdef=Julia
+--langmap=Julia:.jl
+--regex-Julia=/^[ \t]*(function|macro|abstract|type|typealias|immutable)[ \t]+([^ \t({[]+).*$/\2/f,function/
+--regex-Julia=/^[ \t]*(([^@#$ \t({[]+)|\(([^@#$ \t({[]+)\)|\((\$)\))[ \t]*(\{.*\})?[ \t]*\([^#]*\)[ \t]*=([^=].*$|$)/\2\3\4/f,function/
+
+--langdef=Latex
+--langmap=latex:.tex
+--regex-latex=/\\label\{([^}]*)\}/\1/l,label/
+--regex-latex=/\\section\{([^}]*)\}/\1/s,section/
+--regex-latex=/\\subsection\{([^}]*)\}/\1/t,subsection/
+--regex-latex=/\\subsubsection\{([^}]*)\}/\1/u,subsubsection/
+--regex-latex=/\\section\*\{([^}]*)\}/\1/s,section/
+--regex-latex=/\\subsection\*\{([^}]*)\}/\1/t,subsection/
+--regex-latex=/\\subsubsection\*\{([^}]*)\}/\1/u,subsubsection/
diff --git a/packages/symbol-provider-ctags/lib/ctags-provider.js b/packages/symbol-provider-ctags/lib/ctags-provider.js
new file mode 100644
index 000000000..6d587ce77
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/ctags-provider.js
@@ -0,0 +1,284 @@
+const {
+ BufferedProcess,
+ CompositeDisposable,
+ File,
+ Point
+} = require('atom');
+
+const TagReader = require('./tag-reader');
+const getTagsFile = require('./get-tags-file');
+const fs = require('fs-plus');
+const path = require('path');
+
+class CtagsProvider {
+ constructor() {
+ this.watchTagsFiles();
+ this.loadSymbols();
+ this.packageName = 'symbol-provider-ctags';
+ this.name = 'ctags';
+ this.isExclusive = true;
+ }
+
+ destroy() {
+ this.loadTask?.terminate?.();
+ this.unwatchTagsFiles();
+ }
+
+ canProvideSymbols(meta) {
+ let { editor } = meta;
+ // Can't provide symbols unless a file is saved.
+ if (editor.getPath() === undefined) return 0;
+
+ // We start off with a score less than 1 because `ctags` should always lose
+ // out when it's competing against the Tree-sitter provider.
+ let score = 0.99;
+
+ // If the file isn't saved on disk, this provider's results may be
+ // inaccurate, so it's a less attractive candidate.
+ if (editor.isModified()) score -= 0.1;
+
+ return score;
+ }
+
+ getEditor() {
+ return atom.workspace.getActiveTextEditor();
+ }
+
+ getPath() {
+ if (this.getEditor()) {
+ return this.getEditor().getPath();
+ }
+ return undefined;
+ }
+
+ getScopeName() {
+ if (this.getEditor() && this.getEditor().getGrammar()) {
+ return this.getEditor().getGrammar().scopeName;
+ }
+ return undefined;
+ }
+
+ getPackageRoot() {
+ const {resourcePath} = atom.getLoadSettings();
+ const currentFileWasRequiredFromSnapshot = !fs.isAbsolute(__dirname);
+ const packageRoot = currentFileWasRequiredFromSnapshot
+ ? path.join(resourcePath, 'node_modules', 'symbol-provider-ctags')
+ : path.resolve(__dirname, '..');
+
+ if (path.extname(resourcePath) === '.asar' && packageRoot.indexOf(resourcePath) === 0) {
+ return path.join(`${resourcePath}.unpacked`, 'node_modules', 'symbol-provider-ctags');
+ } else {
+ return packageRoot;
+ }
+ }
+
+ parseTagLine(line) {
+ let sections = line.split('\t');
+ if (sections.length > 3) {
+ return {
+ position: new Point(parseInt(sections[2], 10) - 1),
+ name: sections[0],
+ };
+ }
+ return null;
+ }
+
+ validateTags(tags) {
+ let symbols = [];
+ for (let tag of tags) {
+ let tagFilePath = path.join(tag.directory, tag.file);
+ if (!fs.existsSync(tagFilePath)) {
+ continue;
+ }
+ symbols.push(this.interpretTag(tag));
+ }
+ return symbols;
+ }
+
+ interpretTag(tag) {
+ return {
+ directory: tag.directory,
+ file: tag.file,
+ name: tag.name,
+ position: this.getTagPosition(tag)
+ };
+ }
+
+ getTagPosition(tag) {
+ if (!tag) {
+ return undefined;
+ }
+
+ if (tag.lineNumber) {
+ return new Point(tag.lineNumber - 1, 0);
+ }
+
+ // Remove leading /^ and trailing $/
+ if (!tag.pattern) {
+ return undefined;
+ }
+ const pattern = tag.pattern.replace(/(^\/\^)|(\$\/$)/g, '').trim();
+
+ if (!pattern) {
+ return undefined;
+ }
+ const file = path.join(tag.directory, tag.file);
+ if (!fs.isFileSync(file)) {
+ return undefined;
+ }
+ const iterable = fs.readFileSync(file, 'utf8').split('\n');
+ for (let index = 0; index < iterable.length; index++) {
+ let line = iterable[index];
+ if (pattern === line.trim()) {
+ return new Point(index, 0);
+ }
+ }
+
+ return undefined;
+ }
+
+ watchTagsFiles() {
+ this.unwatchTagsFiles();
+ this.tagsFileSubscriptions = new CompositeDisposable();
+ let reloadTags = () => {
+ this.reloadTags = true;
+ this.watchTagsFiles();
+ };
+
+ for (let projectPath of atom.project.getPaths()) {
+ let tagsFilePath = getTagsFile(projectPath);
+ if (!tagsFilePath) continue;
+ let tagsFile = new File(tagsFilePath);
+
+ this.tagsFileSubscriptions.add(
+ tagsFile.onDidChange(reloadTags),
+ tagsFile.onDidDelete(reloadTags),
+ tagsFile.onDidRename(reloadTags)
+ );
+ }
+ }
+
+ unwatchTagsFiles() {
+ this.tagsFileSubscriptions?.dispose();
+ }
+
+ getLanguage(editor) {
+ if (['.cson', '.gyp'].includes(path.extname(this.path))) {
+ return 'Cson';
+ }
+
+ let scopeName = this.getScopeName(editor);
+
+ switch (scopeName) {
+ case 'source.c': return 'C';
+ case 'source.cpp': return 'C++';
+ case 'source.clojure': return 'Lisp';
+ case 'source.capnp': return 'Capnp';
+ case 'source.cfscript': return 'ColdFusion';
+ case 'source.cfscript.embedded': return 'ColdFusion';
+ case 'source.coffee': return 'CoffeeScript';
+ case 'source.css': return 'Css';
+ case 'source.css.less': return 'Css';
+ case 'source.css.scss': return 'Css';
+ case 'source.elixir': return 'Elixir';
+ case 'source.fountain': return 'Fountain';
+ case 'source.gfm': return 'Markdown';
+ case 'source.go': return 'Go';
+ case 'source.java': return 'Java';
+ case 'source.js': return 'JavaScript';
+ case 'source.js.jsx': return 'JavaScript';
+ case 'source.jsx': return 'JavaScript';
+ case 'source.json': return 'Json';
+ case 'source.julia': return 'Julia';
+ case 'source.makefile': return 'Make';
+ case 'source.objc': return 'C';
+ case 'source.objcpp': return 'C++';
+ case 'source.python': return 'Python';
+ case 'source.ruby': return 'Ruby';
+ case 'source.sass': return 'Sass';
+ case 'source.ts': return 'TypeScript';
+ case 'source.ts.tsx': return 'TypeScript';
+ case 'source.yaml': return 'Yaml';
+ case 'text.html': return 'Html';
+ case 'text.html.php': return 'Php';
+ case 'text.tex.latex': return 'Latex';
+ case 'text.html.cfml': return 'ColdFusion';
+ }
+
+ // TODO: Fall back to a grammar registry lookup?
+ return undefined;
+ }
+
+
+ loadSymbols() {
+ return new Promise(resolve => {
+ this.loadTask = TagReader.getAllTags(resolve);
+ });
+ }
+
+ getSymbols(meta) {
+ if (meta.type === 'project') {
+ return this.getSymbolsInProject(meta);
+ } else if (meta.type === 'project-find') {
+ return this.findDefinitionsInProject(meta);
+ }
+ let { editor } = meta;
+
+ let tags = {};
+ let packageRoot = this.getPackageRoot();
+
+ let command = path.join(packageRoot, 'vendor', `ctags-${process.platform}`);
+ let defaultCtagsFile = path.join(packageRoot, 'lib', 'ctags-config');
+
+ const args = [
+ `--options=${defaultCtagsFile}`,
+ `--fields=+KS`
+ ];
+
+ if (atom.config.get('symbol-provider-ctags.useEditorGrammarAsCtagsLanguage')) {
+ let language = this.getLanguage(editor);
+ if (language) {
+ args.push(`--language-force=${language}`);
+ }
+ }
+
+ args.push('-nf', '-', editor.getPath());
+
+ return new Promise(resolve => {
+ let result, tag;
+ return new BufferedProcess({
+ command,
+ args,
+ stdout: lines => {
+ result = [];
+ for (let line of lines.split('\n')) {
+ let item;
+ tag = this.parseTagLine(line);
+ if (tag) {
+ item = tags[tag.position.row] ?
+ tags[tag.position.row] :
+ (tags[tag.position.row] = tag);
+ }
+ result.push(item);
+ }
+ return result;
+ },
+ stderr: () => {},
+ exit: () => {
+ return resolve(Object.values(tags));
+ }
+ })
+ });
+ }
+
+ async getSymbolsInProject() {
+ return TagReader.getAllTags();
+ }
+
+ async findDefinitionsInProject(meta) {
+ let tags = await TagReader.find(meta.editor);
+ return this.validateTags(tags);
+ }
+}
+
+module.exports = CtagsProvider;
diff --git a/packages/symbol-provider-ctags/lib/get-tags-file.js b/packages/symbol-provider-ctags/lib/get-tags-file.js
new file mode 100644
index 000000000..9a09e5c62
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/get-tags-file.js
@@ -0,0 +1,22 @@
+const fs = require('fs-plus');
+const path = require('path');
+
+const FILES = [
+ 'TAGS',
+ 'tags',
+ '.TAGS',
+ '.tags',
+ path.join('.git', 'tags'),
+ path.join('.git', 'TAGS')
+];
+
+function getTagsFile(directoryPath) {
+ if (!directoryPath) return;
+
+ for (let file of FILES) {
+ let tagsFile = path.join(directoryPath, file);
+ if (fs.isFileSync(tagsFile)) return tagsFile;
+ }
+}
+
+module.exports = getTagsFile;
diff --git a/packages/symbol-provider-ctags/lib/load-tags-handler.js b/packages/symbol-provider-ctags/lib/load-tags-handler.js
new file mode 100644
index 000000000..d7068cda0
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/load-tags-handler.js
@@ -0,0 +1,26 @@
+/* global emit */
+
+const async = require('async');
+const ctags = require('ctags');
+const getTagsFile = require('./get-tags-file');
+
+module.exports = function loadTags(directoryPaths) {
+ // TODO: I tried to remove the dependency on the `async` package but failed
+ // spectacularly. I should try again at some point.
+ return async.each(
+ directoryPaths,
+ (directoryPath, done) => {
+ let tagsFilePath = getTagsFile(directoryPath);
+ if (!tagsFilePath) { return done(); }
+
+ let stream = ctags.createReadStream(tagsFilePath);
+ stream.on('data', function(tags) {
+ for (const tag of Array.from(tags)) { tag.directory = directoryPath; }
+ return emit('tags', tags);
+ });
+ stream.on('end', done);
+ return stream.on('error', done);
+ }
+ , this.async()
+ );
+};
diff --git a/packages/symbol-provider-ctags/lib/main.js b/packages/symbol-provider-ctags/lib/main.js
new file mode 100644
index 000000000..69c7a9745
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/main.js
@@ -0,0 +1,16 @@
+
+const CtagsProvider = require('./ctags-provider');
+
+module.exports = {
+ activate() {
+ this.provider = new CtagsProvider();
+ },
+
+ deactivate() {
+ this.provider?.destroy?.();
+ },
+
+ provideSymbols() {
+ return this.provider;
+ }
+};
diff --git a/packages/symbol-provider-ctags/lib/tag-reader.js b/packages/symbol-provider-ctags/lib/tag-reader.js
new file mode 100644
index 000000000..51d199572
--- /dev/null
+++ b/packages/symbol-provider-ctags/lib/tag-reader.js
@@ -0,0 +1,131 @@
+const { Point, Task } = require('atom');
+const util = require('util');
+const ctags = require('ctags');
+const getTagsFile = require('./get-tags-file');
+const _ = require('underscore-plus');
+
+let handlerPath = require.resolve('./load-tags-handler');
+
+let findTagsWithPromise = util.promisify(ctags.findTags).bind(ctags);
+
+function wordAtCursor(text, cursorIndex, wordSeparator, noStripBefore) {
+ const beforeCursor = text.slice(0, cursorIndex);
+ const afterCursor = text.slice(cursorIndex);
+
+ const beforeCursorWordBegins = noStripBefore ? 0 :
+ beforeCursor.lastIndexOf(wordSeparator) + 1;
+ let afterCursorWordEnds = afterCursor.indexOf(wordSeparator);
+ if (afterCursorWordEnds === -1) {
+ afterCursorWordEnds = afterCursor.length;
+ }
+
+ return beforeCursor.slice(beforeCursorWordBegins) +
+ afterCursor.slice(0, afterCursorWordEnds);
+}
+
+module.exports = {
+ async find(editor) {
+ let symbol = editor.getSelectedText();
+ let symbols = [];
+
+ if (symbol) symbols.push(symbol);
+
+ if (!symbols.length) {
+ let nonWordCharacters;
+ let cursor = editor.getLastCursor();
+ let cursorPosition = cursor.getBufferPosition();
+ let scope = cursor.getScopeDescriptor();
+ const rubyScopes = scope.getScopesArray().filter(s => /^source\.ruby($|\.)/.test(s));
+
+ let hasRubyScope = rubyScopes.length > 0;
+
+ let wordRegExp = cursor.wordRegExp();
+ if (hasRubyScope) {
+ nonWordCharacters = atom.config.get('editor.nonWordCharacters', { scope });
+ nonWordCharacters = nonWordCharacters.replace(/:/g, '');
+ wordRegExp = new RegExp(
+ `[^\\s${_.escapeRegExp(nonWordCharacters)}]+([!?]|\\s*=>?)?|[<=>]+`,
+ 'g'
+ );
+ }
+
+ let addSymbol = (symbol) => {
+ if (hasRubyScope) {
+ // Normalize assignment syntax
+ if (/\s+=?$/.test(symbol)) {
+ symbols.push(symbol.replace(/\s+=$/, '='));
+ }
+ // Strip away assignment & hashrocket syntax
+ symbols.push(symbol.replace(/\s+=>?$/, ''));
+ } else {
+ symbols.push(symbol);
+ }
+ };
+
+ // Can't use `getCurrentWordBufferRange` here because we want to select
+ // the last match of the potential 2 matches under cursor.
+ editor.scanInBufferRange(wordRegExp, cursor.getCurrentLineBufferRange(), ({range, match}) => {
+ if (range.containsPoint(cursorPosition)) {
+ symbol = match[0];
+ if (rubyScopes.length && symbol.indexOf(':') > -1) {
+ const cursorWithinSymbol = cursorPosition.column - range.start.column;
+ // Add fully-qualified ruby constant up until the cursor position
+ addSymbol(wordAtCursor(symbol, cursorWithinSymbol, ':', true));
+ // Additionally, also look up the bare word under cursor
+ addSymbol(wordAtCursor(symbol, cursorWithinSymbol, ':'));
+ } else {
+ addSymbol(symbol);
+ }
+ }
+ });
+ }
+
+ if (symbols.length === 0) return [];
+
+ let results = [];
+ for (let projectPath of atom.project.getPaths()) {
+ let tagsFile = getTagsFile(projectPath);
+ if (!tagsFile) continue;
+
+ for (let symbol of symbols) {
+ let tags;
+ try {
+ tags = await findTagsWithPromise(tagsFile, symbol);
+ if (!tags) tags = [];
+ } catch (err) {
+ continue;
+ }
+ if (tags.length === 0) continue;
+
+ for (let tag of [...tags]) {
+ tag.directory = projectPath;
+ }
+
+ results.push(...tags);
+ }
+ }
+ return results;
+ },
+
+ getAllTags() {
+ let projectTags = [];
+ return new Promise(resolve => {
+ let task = Task.once(
+ handlerPath,
+ atom.project.getPaths(),
+ () => {
+ resolve(projectTags);
+ }
+ );
+
+ task.on('tags', tags => {
+ for (let tag of tags) {
+ if (tag.lineNumber) {
+ tag.position = new Point(tag.lineNumber - 1, 0);
+ }
+ }
+ projectTags.push(...tags);
+ });
+ });
+ },
+};
diff --git a/packages/symbol-provider-ctags/package-lock.json b/packages/symbol-provider-ctags/package-lock.json
new file mode 100644
index 000000000..9833a905c
--- /dev/null
+++ b/packages/symbol-provider-ctags/package-lock.json
@@ -0,0 +1,2248 @@
+{
+ "name": "symbol-provider-ctags",
+ "version": "0.0.5",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "symbol-provider-ctags",
+ "version": "0.0.5",
+ "license": "MIT",
+ "dependencies": {
+ "async": "^0.2.6",
+ "ctags": "^3.1.0",
+ "fs-plus": "^3.1.1"
+ },
+ "devDependencies": {
+ "eslint": "^8.39.0",
+ "temp": "^0.9.4"
+ },
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0",
+ "node": ">=14"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.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.55.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
+ "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.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": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "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/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "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": "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/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",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+ "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "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/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/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",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "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/ctags": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/ctags/-/ctags-3.1.0.tgz",
+ "integrity": "sha512-7/aiGLj8Ih7Ko3bAAg8bQUwHjOGXKQ7XC+bv+vLh84BtkVodPEOpOnr65FnWjX2oFWoKSaDuxe7jFHudD2Q0uw==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "event-stream": "~3.1.0",
+ "nan": "^2.14.0"
+ }
+ },
+ "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/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/duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
+ },
+ "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.55.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
+ "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.55.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "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.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "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",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "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.3",
+ "strip-ansi": "^6.0.1",
+ "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.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "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/event-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.1.7.tgz",
+ "integrity": "sha512-ddACn1VEffD+nvbofs8gs/0qJZC9gtEGLG+WykE//rinSpYLSaTsnN96eVQV+gHdUhV/nVtxUNKC3OjrApuEMw==",
+ "dependencies": {
+ "duplexer": "~0.1.1",
+ "from": "~0",
+ "map-stream": "~0.1.0",
+ "pause-stream": "0.0.11",
+ "split": "0.2",
+ "stream-combiner": "~0.0.4",
+ "through": "~2.3.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.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "node_modules/from": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="
+ },
+ "node_modules/fs-plus": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
+ "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
+ "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",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="
+ },
+ "node_modules/fs-plus/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "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.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "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",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "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-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-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-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "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/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "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/map-stream": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
+ },
+ "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": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "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/nan": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+ "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
+ },
+ "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/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.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",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "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/pause-stream": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+ "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
+ "dependencies": {
+ "through": "~2.3"
+ }
+ },
+ "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/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "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/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": "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/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/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/split": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz",
+ "integrity": "sha512-e0pKq+UUH2Xq/sXbYpZBZc3BawsfDZ7dgv+JtRTUPNcvF5CMR4Y9cvJqkMY0MoxWzTHvZuz1beg6pNEKlszPiQ==",
+ "dependencies": {
+ "through": "2"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/stream-combiner": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+ "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
+ "dependencies": {
+ "duplexer": "~0.1.1"
+ }
+ },
+ "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/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.9.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+ "dev": true,
+ "dependencies": {
+ "mkdirp": "^0.5.1",
+ "rimraf": "~2.6.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/temp/node_modules/rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "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/through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "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.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
+ },
+ "node_modules/underscore-plus": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
+ "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
+ "dependencies": {
+ "underscore": "^1.9.1"
+ }
+ },
+ "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/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "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": {
+ "@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true
+ },
+ "@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true
+ },
+ "@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.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.55.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
+ "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
+ "dev": true
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^2.0.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": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "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"
+ }
+ },
+ "@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "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": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "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",
+ "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
+ "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "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
+ },
+ "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"
+ }
+ },
+ "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",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
+ },
+ "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"
+ }
+ },
+ "ctags": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/ctags/-/ctags-3.1.0.tgz",
+ "integrity": "sha512-7/aiGLj8Ih7Ko3bAAg8bQUwHjOGXKQ7XC+bv+vLh84BtkVodPEOpOnr65FnWjX2oFWoKSaDuxe7jFHudD2Q0uw==",
+ "requires": {
+ "event-stream": "~3.1.0",
+ "nan": "^2.14.0"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "duplexer": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz",
+ "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg=="
+ },
+ "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.55.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
+ "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.55.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "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.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "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",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "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.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ }
+ },
+ "eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ },
+ "esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "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
+ },
+ "event-stream": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-3.1.7.tgz",
+ "integrity": "sha512-ddACn1VEffD+nvbofs8gs/0qJZC9gtEGLG+WykE//rinSpYLSaTsnN96eVQV+gHdUhV/nVtxUNKC3OjrApuEMw==",
+ "requires": {
+ "duplexer": "~0.1.1",
+ "from": "~0",
+ "map-stream": "~0.1.0",
+ "pause-stream": "0.0.11",
+ "split": "0.2",
+ "stream-combiner": "~0.0.4",
+ "through": "~2.3.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.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "from": {
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz",
+ "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g=="
+ },
+ "fs-plus": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
+ "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
+ "requires": {
+ "async": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.2",
+ "underscore-plus": "1.x"
+ },
+ "dependencies": {
+ "async": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="
+ },
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
+ },
+ "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.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "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",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
+ },
+ "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-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-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-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "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
+ },
+ "keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "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
+ },
+ "map-stream": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.1.0.tgz",
+ "integrity": "sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g=="
+ },
+ "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": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="
+ },
+ "mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "requires": {
+ "minimist": "^1.2.6"
+ }
+ },
+ "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
+ },
+ "nan": {
+ "version": "2.18.0",
+ "resolved": "https://registry.npmjs.org/nan/-/nan-2.18.0.tgz",
+ "integrity": "sha512-W7tfG7vMOGtD30sHoZSSc/JVYiyDPEyQVso/Zz+/uQd0B0L46gtC+pHha5FFMRpil6fm/AoEcRWyOVi4+E/f8w=="
+ },
+ "natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "requires": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ }
+ },
+ "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",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
+ },
+ "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
+ },
+ "pause-stream": {
+ "version": "0.0.11",
+ "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz",
+ "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==",
+ "requires": {
+ "through": "~2.3"
+ }
+ },
+ "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
+ },
+ "punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "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
+ },
+ "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": "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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
+ },
+ "split": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/split/-/split-0.2.10.tgz",
+ "integrity": "sha512-e0pKq+UUH2Xq/sXbYpZBZc3BawsfDZ7dgv+JtRTUPNcvF5CMR4Y9cvJqkMY0MoxWzTHvZuz1beg6pNEKlszPiQ==",
+ "requires": {
+ "through": "2"
+ }
+ },
+ "stream-combiner": {
+ "version": "0.0.4",
+ "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.0.4.tgz",
+ "integrity": "sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==",
+ "requires": {
+ "duplexer": "~0.1.1"
+ }
+ },
+ "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"
+ }
+ },
+ "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.9.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1",
+ "rimraf": "~2.6.2"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "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
+ },
+ "through": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
+ "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="
+ },
+ "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.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A=="
+ },
+ "underscore-plus": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
+ "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
+ "requires": {
+ "underscore": "^1.9.1"
+ }
+ },
+ "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"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
+ },
+ "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/symbol-provider-ctags/package.json b/packages/symbol-provider-ctags/package.json
new file mode 100644
index 000000000..16ab16de3
--- /dev/null
+++ b/packages/symbol-provider-ctags/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "symbol-provider-ctags",
+ "main": "./lib/main",
+ "version": "1.0.0",
+ "description": "Provides symbols to symbols-view via ctags",
+ "repository": "https://github.com/pulsar-edit/pulsar",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0",
+ "node": ">=14"
+ },
+ "providedServices": {
+ "symbol.provider": {
+ "description": "Allows external sources to suggest symbols for a given file or project.",
+ "versions": {
+ "1.0.0": "provideSymbols"
+ }
+ }
+ },
+ "configSchema": {
+ "useEditorGrammarAsCtagsLanguage": {
+ "default": true,
+ "type": "boolean",
+ "description": "Force `ctags` to use the name of the current file's language in Pulsar when generating tags. By default, `ctags` automatically selects the language of a source file, ignoring those files whose language cannot be determined. This option forces the specified language to be used instead of automatically selecting the language based upon its extension."
+ }
+ },
+ "dependencies": {
+ "async": "^0.2.6",
+ "fs-plus": "^3.1.1",
+ "ctags": "^3.1.0"
+ },
+ "devDependencies": {
+ "eslint": "^8.39.0",
+ "temp": "^0.9.4"
+ }
+}
diff --git a/packages/symbol-provider-ctags/spec/.eslintrc.js b/packages/symbol-provider-ctags/spec/.eslintrc.js
new file mode 100644
index 000000000..8ee8742b4
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/.eslintrc.js
@@ -0,0 +1,13 @@
+module.exports = {
+ env: { jasmine: true },
+ globals: {
+ waitsForPromise: true
+ },
+ rules: {
+ "node/no-unpublished-require": "off",
+ "node/no-extraneous-require": "off",
+ "no-unused-vars": "off",
+ "no-empty": "off",
+ "no-constant-condition": "off"
+ }
+};
diff --git a/packages/symbol-provider-ctags/spec/fixtures/c/sample.c b/packages/symbol-provider-ctags/spec/fixtures/c/sample.c
new file mode 100644
index 000000000..5a98bf4c1
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/c/sample.c
@@ -0,0 +1,6 @@
+#define UNUSED(x) (void)(x)
+
+static void f(int x)
+{
+ UNUSED(x);
+}
diff --git a/packages/symbol-provider-ctags/spec/fixtures/c/tags b/packages/symbol-provider-ctags/spec/fixtures/c/tags
new file mode 100644
index 000000000..25d845e75
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/c/tags
@@ -0,0 +1,8 @@
+!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
+!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
+!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/
+!_TAG_PROGRAM_NAME Exuberant Ctags //
+!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/
+!_TAG_PROGRAM_VERSION 5.9~svn20110310 //
+UNUSED sample.c 1;" d file:
+f sample.c /^static void f(int x)$/;" f file:
diff --git a/packages/symbol-provider-ctags/spec/fixtures/js/.eslintignore b/packages/symbol-provider-ctags/spec/fixtures/js/.eslintignore
new file mode 100644
index 000000000..c8078be87
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/.eslintignore
@@ -0,0 +1,3 @@
+tagged.js
+sample.js
+other-file.js
diff --git a/packages/symbol-provider-ctags/spec/fixtures/js/no-symbols.js b/packages/symbol-provider-ctags/spec/fixtures/js/no-symbols.js
new file mode 100644
index 000000000..9a1aba6f7
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/no-symbols.js
@@ -0,0 +1,6 @@
+// Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+// tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+// quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+// consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+// cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
+// non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
diff --git a/packages/symbol-provider-ctags/spec/fixtures/js/other-file.js b/packages/symbol-provider-ctags/spec/fixtures/js/other-file.js
new file mode 100644
index 000000000..4e79b3a98
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/other-file.js
@@ -0,0 +1,29 @@
+// Another file for symbols to exist in. Used for project search.
+
+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));
+};
+
+var quicksort2 = 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/symbol-provider-ctags/spec/fixtures/js/sample.js b/packages/symbol-provider-ctags/spec/fixtures/js/sample.js
new file mode 100644
index 000000000..15ab7c831
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/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/symbol-provider-ctags/spec/fixtures/js/tagged.js b/packages/symbol-provider-ctags/spec/fixtures/js/tagged.js
new file mode 100644
index 000000000..4adaac609
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/tagged.js
@@ -0,0 +1,11 @@
+var thisIsCrazy = true;
+
+function callMeMaybe() {
+ return "here's my number";
+}
+
+var iJustMetYou = callMeMaybe();
+
+function duplicate() {
+ return true;
+}
diff --git a/packages/symbol-provider-ctags/spec/fixtures/js/tags b/packages/symbol-provider-ctags/spec/fixtures/js/tags
new file mode 100644
index 000000000..eed641589
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/js/tags
@@ -0,0 +1,10 @@
+!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
+!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
+!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/
+!_TAG_PROGRAM_NAME Exuberant Ctags //
+!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/
+!_TAG_PROGRAM_VERSION 5.8 //
+callMeMaybe tagged.js /^function callMeMaybe() {$/;" f
+duplicate tagged-duplicate.js /^function duplicate() {$/;" f
+duplicate tagged.js /^function duplicate() {$/;" f
+thisIsCrazy tagged.js /^var thisIsCrazy = true;$/;" v
diff --git a/packages/symbol-provider-ctags/spec/fixtures/ruby/file1.rb b/packages/symbol-provider-ctags/spec/fixtures/ruby/file1.rb
new file mode 100644
index 000000000..62697a7b3
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/ruby/file1.rb
@@ -0,0 +1,33 @@
+module A::Foo
+ B = 'b'
+
+ def bar!
+
+ end
+
+ def bar?
+
+ end
+
+ def baz
+ end
+
+ def baz=(*)
+ end
+end
+
+if bar?
+ baz
+ bar!
+elsif !bar!
+ baz= 1
+ baz = 2
+ Foo = 3
+ { :baz => 4 }
+ A::Foo::B
+ C::Foo::B
+ D::Foo::E
+end
+
+module D::Foo
+end
diff --git a/packages/symbol-provider-ctags/spec/fixtures/ruby/tags b/packages/symbol-provider-ctags/spec/fixtures/ruby/tags
new file mode 100644
index 000000000..26cb7967a
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/fixtures/ruby/tags
@@ -0,0 +1,15 @@
+!_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/
+!_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/
+!_TAG_PROGRAM_AUTHOR Darren Hiebert /dhiebert@users.sourceforge.net/
+!_TAG_PROGRAM_NAME Exuberant Ctags //
+!_TAG_PROGRAM_URL http://ctags.sourceforge.net /official site/
+!_TAG_PROGRAM_VERSION 5.8 //
+A::Foo file1.rb /^module A::Foo$/;" m
+A::Foo::B file1.rb /^ B = 'b'$/;" C
+B file1.rb /^ B = 'b'$/;" C
+D::Foo file1.rb /^module D::Foo$/;" m
+Foo file1.rb /^module A::Foo$/;" m
+bar! file1.rb /^ def bar!$/;" f class:Foo
+bar? file1.rb /^ def bar?$/;" f class:Foo
+baz file1.rb /^ def baz$/;" f class:Foo
+baz= file1.rb /^ def baz=(*)$/;" f class:Foo
diff --git a/packages/symbol-provider-ctags/spec/symbol-provider-ctags-spec.js b/packages/symbol-provider-ctags/spec/symbol-provider-ctags-spec.js
new file mode 100644
index 000000000..c63d1b8bc
--- /dev/null
+++ b/packages/symbol-provider-ctags/spec/symbol-provider-ctags-spec.js
@@ -0,0 +1,305 @@
+
+const path = require('path');
+const fs = require('fs-plus');
+const temp = require('temp');
+const CTagsProvider = require('../lib/ctags-provider');
+
+function getEditor() {
+ return atom.workspace.getActiveTextEditor();
+}
+
+async function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+async function getProjectSymbols(provider, editor) {
+ let symbols = await provider.getSymbols({
+ type: 'project',
+ editor,
+ paths: atom.project.getPaths()
+ });
+ return symbols;
+}
+
+async function findDeclarationInProject(provider, editor) {
+ let symbols = await provider.getSymbols({
+ type: 'project-find',
+ editor,
+ paths: atom.project.getPaths(),
+ word: editor.getWordUnderCursor()
+ });
+ return symbols;
+}
+
+describe('CTagsProvider', () => {
+ let provider, directory, editor;
+
+ beforeEach(() => {
+ jasmine.unspy(global, 'setTimeout');
+ jasmine.unspy(Date, 'now');
+
+ provider = new CTagsProvider();
+
+ atom.project.setPaths([
+ temp.mkdirSync('other-dir-'),
+ temp.mkdirSync('atom-symbols-view-')
+ ]);
+
+ directory = atom.project.getDirectories()[1];
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'js'),
+ atom.project.getPaths()[1]
+ );
+ });
+
+ it('identifies its project root correctly', () => {
+ let root = provider.getPackageRoot();
+ expect(root).toContain("symbol-provider-ctags");
+ expect(
+ fs.existsSync(path.join(root, "vendor", "ctags-darwin"))
+ ).toBe(true);
+ });
+
+ describe('when tags can be generated for a file', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ });
+
+ it('provides all JavaScript functions', async () => {
+ let symbols = await provider.getSymbols({
+ type: 'file',
+ editor
+ });
+
+ expect(symbols[0].name).toBe('quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('quicksort.sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('when the buffer is new and unsaved', () => {
+ beforeEach(async () => {
+ await atom.workspace.open();
+ editor = getEditor();
+ });
+
+ it('does not try to provide symbols', () => {
+ let meta = { type: 'file', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(0);
+ });
+ });
+
+ describe('when the buffer is modified', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ });
+
+ it('returns a lower match score', () => {
+ editor.insertText("\n");
+ let meta = { type: 'file', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(0.89);
+ });
+ });
+
+ describe('when no tags can be generated for a file', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('no-symbols.js'));
+ editor = getEditor();
+ });
+
+ it('returns an empty array', async () => {
+ let symbols = await provider.getSymbols({ type: 'file', editor });
+ expect(Array.isArray(symbols)).toBe(true);
+ expect(symbols.length).toBe(0);
+ });
+ });
+
+ describe('go to declaration', () => {
+ it("returns nothing when no declaration is found", async () => {
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = getEditor();
+ editor.setCursorBufferPosition([0, 2]);
+
+ let symbols = await provider.getSymbols({
+ type: 'project-find',
+ editor,
+ paths: atom.project.getPaths(),
+ word: editor.getWordUnderCursor()
+ });
+
+ expect(symbols.length).toBe(0);
+ });
+
+ it("returns one result when there is a single matching declaration", async () => {
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = getEditor();
+
+ editor.setCursorBufferPosition([6, 24]);
+ let symbols = await provider.getSymbols({
+ type: 'project-find',
+ editor,
+ paths: atom.project.getPaths(),
+ word: editor.getWordUnderCursor()
+ });
+
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([2, 0]);
+ });
+
+ it("correctly identifies the tag for a C preprocessor macro", async () => {
+ atom.project.setPaths([temp.mkdirSync('atom-symbols-view-c-')]);
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'c'),
+ atom.project.getPaths()[0]
+ );
+
+ await atom.packages.activatePackage('language-c');
+ await atom.workspace.open('sample.c');
+
+ editor = getEditor();
+ editor.setCursorBufferPosition([4, 4]);
+
+ let symbols = await findDeclarationInProject(provider, editor);
+
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([0, 0]);
+ });
+
+ it('ignores results that reference nonexistent files', async () => {
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = getEditor();
+ editor.setCursorBufferPosition([8, 14]);
+
+ let symbols = await findDeclarationInProject(provider, editor);
+
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([8, 0]);
+ });
+
+ it('includes ? and ! characters in ruby symbols', async () => {
+ atom.project.setPaths([temp.mkdirSync('atom-symbols-view-ruby-')]);
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'ruby'),
+ atom.project.getPaths()[0]
+ );
+
+ await atom.packages.activatePackage('language-ruby');
+ await atom.workspace.open('file1.rb');
+ let symbols;
+
+ editor = getEditor();
+
+ editor.setCursorBufferPosition([18, 4]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([7, 0]);
+
+ editor.setCursorBufferPosition([19, 2]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([11, 0]);
+
+ editor.setCursorBufferPosition([20, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([3, 0]);
+
+ editor.setCursorBufferPosition([21, 7]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([3, 0]);
+ });
+
+ it('understands assignment ruby method definitions', async () => {
+ atom.project.setPaths([temp.mkdirSync('atom-symbols-view-ruby-')]);
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'ruby'),
+ atom.project.getPaths()[0]
+ );
+
+ await atom.packages.activatePackage('language-ruby');
+ await atom.workspace.open('file1.rb');
+ let symbols;
+
+ editor = getEditor();
+
+ editor.setCursorBufferPosition([22, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([14, 0]);
+
+ editor.setCursorBufferPosition([23, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(2);
+ expect(symbols[0].position).toEqual([14, 0]);
+
+ editor.setCursorBufferPosition([24, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([0, 0]);
+
+ editor.setCursorBufferPosition([25, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([11, 0]);
+ });
+
+ it('understands fully qualified ruby constant definitions', async () => {
+ atom.project.setPaths([temp.mkdirSync('atom-symbols-view-ruby-')]);
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'ruby'),
+ atom.project.getPaths()[0]
+ );
+
+ await atom.packages.activatePackage('language-ruby');
+ await atom.workspace.open('file1.rb');
+ let symbols;
+
+ editor = getEditor();
+
+ editor.setCursorBufferPosition([26, 10]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(2);
+ expect(symbols[0].position).toEqual([1, 0]);
+
+ editor.setCursorBufferPosition([27, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(1);
+ expect(symbols[0].position).toEqual([0, 0]);
+
+ editor.setCursorBufferPosition([28, 5]);
+ symbols = await findDeclarationInProject(provider, editor);
+ expect(symbols.length).toBe(2);
+ expect(symbols[0].position).toEqual([31, 0]);
+ });
+ });
+
+ describe('project symbols', () => {
+ it('displays all tags', async () => {
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = getEditor();
+
+ let symbols = await getProjectSymbols(provider, editor);
+
+ expect(symbols.length).toBe(4);
+
+ expect(symbols[0].name).toBe('callMeMaybe');
+ expect(symbols[0].directory).toBe(directory.getPath());
+ expect(symbols[0].file).toBe('tagged.js');
+
+ expect(symbols[3].name).toBe('thisIsCrazy');
+ expect(symbols[3].directory).toBe(directory.getPath());
+ expect(symbols[3].file).toBe('tagged.js');
+
+ fs.removeSync(directory.resolve('tags'));
+ await wait(50);
+
+ symbols = await getProjectSymbols(provider, editor);
+ expect(symbols.length).toBe(0);
+ });
+ });
+});
diff --git a/packages/symbol-provider-ctags/vendor/ctags-darwin b/packages/symbol-provider-ctags/vendor/ctags-darwin
new file mode 100755
index 000000000..86576c957
Binary files /dev/null and b/packages/symbol-provider-ctags/vendor/ctags-darwin differ
diff --git a/packages/symbol-provider-ctags/vendor/ctags-linux b/packages/symbol-provider-ctags/vendor/ctags-linux
new file mode 100755
index 000000000..0e229e3ef
Binary files /dev/null and b/packages/symbol-provider-ctags/vendor/ctags-linux differ
diff --git a/packages/symbol-provider-ctags/vendor/ctags-win32.exe b/packages/symbol-provider-ctags/vendor/ctags-win32.exe
new file mode 100644
index 000000000..b688c2936
Binary files /dev/null and b/packages/symbol-provider-ctags/vendor/ctags-win32.exe differ
diff --git a/packages/symbol-provider-tree-sitter/.eslintrc.js b/packages/symbol-provider-tree-sitter/.eslintrc.js
new file mode 100644
index 000000000..00404ea92
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+ env: {
+ browser: true,
+ commonjs: true,
+ es2021: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:node/recommended",
+ ],
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: "latest"
+ },
+ rules: {
+ "no-fallthrough": "off",
+ "no-case-declarations": "off",
+ "space-before-function-paren": ["error", {
+ anonymous: "always",
+ asyncArrow: "always",
+ named: "never"
+ }],
+ "node/no-unpublished-require": [
+ "error",
+ {
+ allowModules: ["electron"]
+ }
+ ],
+ "node/no-missing-require": [
+ "error",
+ {
+ allowModules: ["atom"]
+ }
+ ]
+ },
+ plugins: [
+ "jsdoc"
+ ],
+ globals: {
+ atom: "writeable"
+ }
+};
diff --git a/packages/symbol-provider-tree-sitter/LICENSE.md b/packages/symbol-provider-tree-sitter/LICENSE.md
new file mode 100644
index 000000000..ed5b10bc1
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/LICENSE.md
@@ -0,0 +1,20 @@
+Copyright (c) 2023 Andrew Dupont
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/packages/symbol-provider-tree-sitter/README.md b/packages/symbol-provider-tree-sitter/README.md
new file mode 100644
index 000000000..e5a5dc204
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/README.md
@@ -0,0 +1,189 @@
+# symbol-provider-tree-sitter package
+
+Provides symbols to `symbols-view` via Tree-sitter queries.
+
+Tree-sitter grammars [with tags queries](https://tree-sitter.github.io/tree-sitter/code-navigation-systems) can very easily give us a list of all the symbols in a file without the drawbacks of a `ctags`-based approach. For instance, they operate on the contents of the buffer, not the contents of the file on disk, so they work just fine in brand-new files and in files that have been modified since the last save.
+
+This provider does not currently support project-wide symbol search, but possibly could do so in the future.
+
+## Tags queries
+
+This provider expects for a grammar to have specified a tags query in its grammar definition file. All the built-in Tree-sitter grammars will have such a file. If youβre using a third-party Tree-sitter grammar that hasnβt defined one, file an issue on Pulsar and weβll see what we can do.
+
+If youβre writing your own grammar, or contributing a `tags.scm` to a grammar without one, keep reading.
+
+### Query syntax
+
+The query syntax starts as a subset of what is described [on this page](https://tree-sitter.github.io/tree-sitter/code-navigation-systems). Hereβs what this package can understand:
+
+* A query that consists of a `@definition.THING` capture with a `@name` capture inside will properly be understood as a symbol with a tag corresponding to `THING` and a name corresponding to the `@name` captureβs text.
+* A query that consists of a `@reference.THING` capture with a `@name` capture inside will be ignored by default. If the proper setting is enabled, each of these references will become a symbol with a tag corresponding to `THING` and a name corresponding to the `@name` captureβs text.
+* All other `@name` captures that are not within either a `@definition` or a `@reference` will be considered as a symbol in isolation. (These symbols can still specify a tag via a `#set!` predicate.)
+
+To match the current behavior of the `symbols-view` package, you can usually take a `queries/tags.scm` file from a Tree-sitter repository β many parsers define them βΒ and paste it straight into your grammarβs `tags.scm` file.
+
+#### Advanced features
+
+The text of the captured node is what will be displayed as the symbolβs name, but a few predicates are available to alter that field and others. Symbol predicates use `#set!` and the `symbol` namespace.
+
+##### Node position descriptors
+
+Several predicates take a **node position descriptor** as an argument. Itβs a string that resembles an object lookup chain in JavaScript:
+
+```scm
+(#set! symbol.prependTextForNode parent.parent.firstNamedChild)
+```
+
+Starting at the captured node, it describes a path to take within the tree in order to get to another meaningful node.
+
+In all these examples, if the descriptor is invalid and does not return a node, the predicate will be ignored.
+
+##### Changing the symbolβs name
+
+There are several ways to add text to the beginning or end of the symbolβs name:
+
+###### symbol.prepend
+
+```scm
+(class_declaration
+ name: (identifier) @name
+ (#set! symbol.prepend "Class: "))
+```
+
+The `symbol.prepend` predicate adds a constant string to the beginning of a symbol name. For a class `Foo` in JavaScript, this predicate would result in a symbol called `Class: Foo`.
+
+###### symbol.append
+
+```scm
+(class_declaration
+ name: (identifier) @name
+ (#set! symbol.append " (class)"))
+```
+
+The `symbol.append` predicate adds a constant string to the end of a symbol name. For a class `Foo`, this predicate would result in a symbol called `Foo (class)`.
+
+
+###### symbol.strip
+
+```scm
+(class_declaration
+ name: (identifier) @name
+ (#set! symbol.strip "^\\s+|\\s+$"))
+```
+
+The `symbol.strip` predicate will replace everything matched by the regular expression with an empty string. The pattern given is compiled into a JavaScript `RegExp` with an implied `g` (global) flag.
+
+In this example, _if_ the `identifier` node included whitespace on either side of the symbol, the symbolβs name would be stripped of that whitespace before being shown in the UI.
+
+###### symbol.prependTextForNode
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.prependTextForNode "parent.parent.previousNamedSibling")
+ (#set! symbol.joiner "#")
+))
+```
+
+The `symbol.prependTextForNode` predicate will look up the text of the node referred to by the provided _node position descriptor_, then prepend that text to the symbol name. If `symbol.joiner` is provided, it will be inserted in between the two.
+
+In this example, a `bar` method on a class named `Foo` would have a symbol name of `Foo#bar`.
+
+###### symbol.prependSymbolForNode
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.prependSymbolForNode "parent.parent.previousNamedSibling")
+ (#set! symbol.joiner "#")
+))
+```
+
+The `symbol.prependSymbolForNode` predicate will look up the symbol name of the node referred to by the provided _node position descriptor_, then prepend that name to the symbol name. If `symbol.joiner` is provided, it will be inserted in between the two.
+
+Unlike `symbol.prependTextForNode`, the node referred to with the descriptor must have its own symbol name, and it must have been processed already β that is, it must be a symbol whose name was determined earlier than that of the current node.
+
+This allows us to incorporate any transformations that were applied to the other nodeβs symbol name. We can use this to build βrecursiveβ symbol names β for instance, JSON keys whose symbols consist of their entire key path from the root.
+
+##### Adding the `context` field
+
+The `context` field of a symbol is a short piece of text meant to give context. For instance, a symbol that represents a class method could have a `context` field that contains the name of the class it belongs to. The `context` field is not filtered on.
+
+###### symbol.contextNode
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.contextNode "parent.parent.previousNamedSibling")
+))
+```
+
+The `symbol.contextNode` predicate will set the value of a symbolβs `context` property to the text of a node based on the provided _node position descriptor_.
+
+###### symbol.context
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.context "class")
+))
+```
+
+The `symbol.context` predicate will set the value of a symbolβs `context` property to a fixed string.
+
+The point of `context` is to provide information to help you tell symbols apart, so you probably donβt want to set it to a fixed value. But this predicate is available just in case.
+
+##### Adding a tag
+
+The `tag` field is a string that indicates a symbolβs kind or type. It should be a single word wherever possible. A `tag` for a class methodβs symbol would typically be `method`, whereas the symbol for the class itself would typically have a `tag` of `class`. These tags will be indicated in the UI with a badge, an icon, or both.
+
+If youβre not sure what to call something, consult [this list from the Language Server Protocol spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind). But some symbols may not fit any of those, so ultimately itβs up to the author. (For example, headings in Markdown files are assigned a kind of `heading`.)
+
+For consistency, tags should be all lowercase. The interface will apply its own casing effect through CSS (`text-transform: capitalize` by default, but customizable in UI themes).
+
+The preferred method of adding a tag is to leverage the `@definition.` captures that are typically present in a tags file. For instance, in this excerpt from the JavaScript grammarβs `tags.scm` fileβ¦
+
+```scm
+(assignment_expression
+ left: [
+ (identifier) @name
+ (member_expression
+ property: (property_identifier) @name)
+ ]
+ right: [(arrow_function) (function)]
+) @definition.function
+```
+
+β¦the resulting symbol will infer a `tag` value of `function`.
+
+In cases where this is impractical, you can provide the tag explicitly with a predicate.
+
+Nearly all the tags on [the aforementioned list](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind) will also apply an appropriate `icon` to their symbol when assigned. If you choose a tag name not on that list, or want to override the default, you can use the `symbol.icon` predicate described below.
+
+###### symbol.tag
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.tag "class")
+))
+```
+
+The `symbol.tag` predicate will set the value of a symbolβs `tag` property to a fixed string.
+
+The `tag` property is used to supply a word that represents the symbol in some way. For conventional symbols, this will often be something like `class` or `function`.
+
+This provider will attempt to match certain common tag values to icons. This can be overridden by specifying an explicit `symbol.icon` value.
+
+###### symbol.icon
+
+```scm
+(class_body (method_definition
+ name: (property_identifier) @name
+ (#set! symbol.icon "package")
+))
+```
+
+The icon to be shown alongside the symbol in a list. Will only be shown if the user has enabled the βShow Icons in Symbols Viewβ option in the `symbols-view` settings. You can see the full list of available icons by invoking the **Styleguide: Show** command and browsing the βIconsβ section. The value can include the preceding `icon-` or can omit it; e.g., `icon-package` and `package` are both valid values.
+
+If this value is omitted, this provider will still attempt to match certain common tag values to icons. If `tag` is not present on the symbol, or is an uncommon value, there will be a blank space instead of an icon.
diff --git a/packages/symbol-provider-tree-sitter/lib/capture-organizer.js b/packages/symbol-provider-tree-sitter/lib/capture-organizer.js
new file mode 100644
index 000000000..64e3f38e2
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/lib/capture-organizer.js
@@ -0,0 +1,437 @@
+const { Point } = require('atom');
+
+function resolveNodeDescriptor(node, descriptor) {
+ let parts = descriptor.split('.');
+ let result = node;
+ while (result !== null && parts.length > 0) {
+ let part = parts.shift();
+ if (!result[part]) { return null; }
+ result = result[part];
+ }
+ return result;
+}
+
+const PatternCache = {
+ getOrCompile(pattern) {
+ this.patternCache ??= new Map();
+ let regex = this.patternCache.get(pattern);
+ if (!regex) {
+ regex = new RegExp(pattern, 'g');
+ this.patternCache.set(pattern, regex);
+ }
+ return regex;
+ },
+
+ clear() {
+ this.patternCache?.clear();
+ },
+};
+
+
+// Assign a default icon type for each tag β or what LSP calls βkind.β This
+// list is copied directly from the LSP spec's exhaustive list of potential
+// symbol kinds:
+//
+// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind
+function iconForTag(tag) {
+ switch (tag) {
+ case 'file':
+ return 'icon-file';
+ case 'module':
+ return 'icon-database';
+ case 'namespace':
+ return 'icon-tag';
+ case 'package':
+ return 'icon-package';
+ case 'class':
+ return 'icon-puzzle';
+ case 'method':
+ return 'icon-gear';
+ case 'property':
+ return 'icon-primitive-dot';
+ case 'field':
+ return 'icon-primitive-dot';
+ case 'constructor':
+ return 'icon-tools';
+ case 'enum':
+ return 'icon-list-unordered';
+ case 'interface':
+ return 'icon-key';
+ case 'function':
+ return 'icon-gear';
+ case 'variable':
+ return 'icon-code';
+ case 'constant':
+ return 'icon-primitive-square';
+ case 'string':
+ return 'icon-quote';
+ case 'number':
+ return 'icon-plus';
+ case 'boolean':
+ return 'icon-question';
+ case 'array':
+ return 'icon-list-ordered';
+ case 'object':
+ return 'icon-file-code';
+ case 'key':
+ return 'icon-key';
+ case 'null':
+ return null;
+ case 'enum-member':
+ return 'icon-primitive-dot';
+ case 'struct':
+ return 'icon-book';
+ case 'event':
+ return 'icon-calendar';
+ case 'operator':
+ return 'icon-plus';
+ case 'type-parameter':
+ return null;
+ default:
+ return null;
+ }
+}
+
+
+/**
+ * A container capture. When another capture's node is contained by the
+ * definition capture's node, it gets added to this instance.
+ */
+class Container {
+ constructor(capture, organizer) {
+ this.captureFields = new Map();
+ this.captureFields.set(capture.name, capture);
+ this.capture = capture;
+ this.node = capture.node;
+ this.organizer = organizer;
+ this.props = capture.setProperties || {};
+
+ this.tag = capture.name.substring(capture.name.indexOf('.') + 1);
+ this.icon = this.resolveIcon();
+ this.position = capture.node.range.start;
+ }
+
+ getCapture(name) {
+ return this.captureFields.get(name);
+ }
+
+ hasCapture(capture) {
+ return this.captureFields.has(capture.name);
+ }
+
+ endsBefore(range) {
+ let containerRange = this.node.range;
+ return containerRange.end.compare(range.start) === -1;
+ }
+
+ add(capture) {
+ if (this.captureFields.has(capture.name)) {
+ console.warn(`Name already exists:`, capture.name);
+ }
+ // Any captures added to this definition need to be checked to make sure
+ // their nodes are actually descendants of this definition's node.
+ if (!this.node.range.containsRange(capture.node.range)) {
+ return false;
+ }
+ this.captureFields.set(capture.name, capture);
+ if (capture.name === 'name') {
+ this.nameCapture = new Name(capture, this.organizer);
+ }
+ return true;
+ }
+
+ isValid() {
+ return (
+ this.nameCapture &&
+ this.position instanceof Point
+ );
+ }
+
+ resolveIcon() {
+ let icon = this.props['symbol.icon'] ?? iconForTag(this.tag);
+ if (icon && !icon.startsWith('icon-'))
+ icon = `icon-${icon}`;
+ return icon;
+ }
+
+ toSymbol() {
+ if (!this.nameCapture) return null;
+ let nameSymbol = this.nameCapture.toSymbol();
+ let symbol = {
+ name: nameSymbol.name,
+ shortName: nameSymbol.shortName,
+ tag: nameSymbol.tag ?? this.tag,
+ icon: nameSymbol.icon ?? this.icon,
+ position: this.position
+ };
+
+ if (nameSymbol.context) {
+ symbol.context = nameSymbol.context;
+ }
+
+ return symbol;
+ }
+}
+
+class Definition extends Container {
+ constructor(...args) {
+ super(...args);
+ this.type = 'definition';
+ }
+}
+
+class Reference extends Container {
+ constructor(...args) {
+ super(...args);
+ this.type = 'reference';
+ }
+}
+
+class Name {
+ constructor(capture, organizer) {
+ this.type = 'name';
+ this.organizer = organizer;
+ this.props = capture.setProperties ?? {};
+ this.capture = capture;
+ this.node = capture.node;
+ this.position = capture.node.range.start;
+ this.name = this.resolveName(capture);
+ this.shortName = this.resolveName(capture, { short: true });
+ this.context = this.resolveContext(capture);
+ this.tag = this.resolveTag(capture);
+ this.icon = this.resolveIcon(capture);
+ }
+
+ getSymbolNameForNode(node) {
+ return this.organizer.nameCache.get(node.id);
+ }
+
+ resolveName(capture, { short = false } = {}) {
+ let { node, props } = this;
+ let base = node.text;
+ if (props['symbol.strip']) {
+ let pattern = PatternCache.getOrCompile(props['symbol.strip']);
+ base = base.replace(pattern, '');
+ }
+
+ // The βshort nameβ is the symbol's base name before we prepend or append
+ // any text.
+ if (short) return base;
+
+ // TODO: Regex-based replacement?
+ if (props['symbol.prepend']) {
+ base = `${props['symbol.prepend']}${base}`;
+ }
+ if (props['symbol.append']) {
+ base = `${base}${props['symbol.append']}`;
+ }
+
+ let prefix = this.resolvePrefix(capture);
+ if (prefix) {
+ let joiner = props['symbol.joiner'] ?? '';
+ base = `${prefix}${joiner}${base}`;
+ }
+ this.organizer.nameCache.set(node.id, base);
+ return base;
+ }
+
+ resolveContext() {
+ let { node, props } = this;
+ let result = null;
+ if (props['symbol.contextNode']) {
+ let contextNode = resolveNodeDescriptor(node, props['symbol.contextNode']);
+ if (contextNode) {
+ result = contextNode.text;
+ }
+ }
+
+ if (props['symbol.context']) {
+ result = props['symbol.context'];
+ }
+
+ return result;
+ }
+
+ resolvePrefix() {
+ let { node, props } = this;
+ let symbolDescriptor = props['symbol.prependSymbolForNode'];
+ let textDescriptor = props['symbol.prependTextForNode'];
+
+ // Prepending with a symbol name requires that we already have determined
+ // the name for another node, which means the other node must have a
+ // corresponding symbol. But it allows for recursion.
+ if (symbolDescriptor) {
+ let other = resolveNodeDescriptor(node, symbolDescriptor);
+ if (other) {
+ let symbolName = this.getSymbolNameForNode(other);
+ if (symbolName) return symbolName;
+ }
+ }
+
+ // A simpler option is to prepend with a node's text. This works on any
+ // arbitrary node, even nodes that don't have their own symbol names.
+ if (textDescriptor) {
+ let other = resolveNodeDescriptor(node, textDescriptor);
+ if (other) {
+ return other.text;
+ }
+ }
+
+ return null;
+ }
+
+ resolveTag() {
+ return this.props['symbol.tag'] ?? null;
+ }
+
+ resolveIcon() {
+ let icon = this.props['symbol.icon'] ?? null;
+ if (icon && !icon.startsWith('icon-'))
+ icon = `icon-${icon}`;
+ return icon;
+ }
+
+ toSymbol() {
+ let { name, shortName, position, context, tag, icon } = this;
+ let symbol = { name, shortName, position, icon };
+ if (tag) {
+ symbol.tag = tag;
+ symbol.icon ??= iconForTag(tag);
+ }
+ if (context) symbol.context = context;
+
+ return symbol;
+ }
+
+}
+
+/**
+ * Keeps track of @definition.* captures and the captures they may contain.
+ */
+class CaptureOrganizer {
+ clear() {
+ this.nameCache ??= new Map();
+ this.nameCache.clear();
+ this.activeContainers = [];
+
+ this.definitions = [];
+ this.references = [];
+ this.names = [];
+ this.extraCaptures = [];
+ }
+
+ destroy() {
+ PatternCache.clear();
+ this.clear();
+ }
+
+ isDefinition(capture) {
+ return capture.name.startsWith('definition.');
+ }
+
+ isReference(capture) {
+ return capture.name.startsWith('reference.');
+ }
+
+ isName(capture) {
+ return capture.name === 'name';
+ }
+
+ finish(container) {
+ if (!container) return;
+ if (container instanceof Definition) {
+ this.definitions.push(container);
+ } else if (container instanceof Reference) {
+ this.references.push(container);
+ }
+ let index = this.activeContainers.indexOf(container);
+ if (index === -1) return;
+ this.activeContainers.splice(index, 1);
+ }
+
+ addToContainer(capture) {
+ let index = this.activeContainers.length - 1;
+ let added = false;
+ while (index >= 0) {
+ let container = this.activeContainers[index];
+ if (!container.hasCapture(capture)) {
+ if (container.add(capture)) {
+ added = true;
+ break;
+ }
+ }
+ index--;
+ }
+ return added;
+ }
+
+ pruneActiveContainers(capture) {
+ let { range } = capture.node;
+ let indices = [];
+ for (let [index, container] of this.activeContainers.entries()) {
+ if (container.endsBefore(range)) {
+ indices.unshift(index);
+ }
+ }
+
+ for (let index of indices) {
+ this.finish(this.activeContainers[index]);
+ }
+ }
+
+ process(captures, scopeResolver) {
+ scopeResolver.reset();
+
+ this.clear();
+
+ for (let capture of captures) {
+ if (!scopeResolver.store(capture)) continue;
+ this.pruneActiveContainers(capture);
+
+ if (this.isDefinition(capture)) {
+ this.activeContainers.push(new Definition(capture, this));
+ } else if (this.isReference(capture)) {
+ this.activeContainers.push(new Reference(capture, this));
+ } else if (this.isName(capture)) {
+ // See if this @name capture belongs with the most recent @definition
+ // capture.
+ if (this.addToContainer(capture)) {
+ continue;
+ }
+ this.names.push(new Name(capture, this));
+ } else {
+ if (!this.addToContainer(capture)) {
+ continue;
+ } else {
+ this.extraCaptures.push(capture);
+ }
+ }
+ }
+ while (this.activeContainers.length) {
+ this.finish(this.activeContainers[0]);
+ }
+
+ let symbols = [];
+ for (let definition of this.definitions) {
+ if (!definition.isValid()) continue;
+ symbols.push(definition.toSymbol());
+ }
+
+ if (atom.config.get('symbol-provider-tree-sitter.includeReferences')) {
+ for (let reference of this.references) {
+ if (!reference.isValid()) continue;
+ symbols.push(reference.toSymbol());
+ }
+ }
+
+ for (let name of this.names) {
+ symbols.push(name.toSymbol());
+ }
+
+ scopeResolver.reset();
+
+ return symbols;
+ }
+}
+
+module.exports = CaptureOrganizer;
diff --git a/packages/symbol-provider-tree-sitter/lib/main.js b/packages/symbol-provider-tree-sitter/lib/main.js
new file mode 100644
index 000000000..ec7a35d60
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/lib/main.js
@@ -0,0 +1,16 @@
+
+const TreeSitterProvider = require('./tree-sitter-provider');
+
+module.exports = {
+ activate () {
+ this.provider = new TreeSitterProvider();
+ },
+
+ deactivate () {
+ this.provider?.destroy?.();
+ },
+
+ provideSymbols () {
+ return this.provider;
+ }
+};
diff --git a/packages/symbol-provider-tree-sitter/lib/tree-sitter-provider.js b/packages/symbol-provider-tree-sitter/lib/tree-sitter-provider.js
new file mode 100644
index 000000000..215a7b49a
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/lib/tree-sitter-provider.js
@@ -0,0 +1,88 @@
+const CaptureOrganizer = require('./capture-organizer');
+const { Emitter } = require('atom');
+
+class TreeSitterProvider {
+ constructor() {
+ this.packageName = 'symbol-provider-tree-sitter';
+ this.name = 'Tree-sitter';
+ this.isExclusive = true;
+ this.captureOrganizer = new CaptureOrganizer();
+ this.emitter = new Emitter();
+ this.disposable = atom.config.onDidChange('symbol-provider-tree-sitter', () => {
+ // Signal the consumer to clear its cache whenever we change the package
+ // config.
+ this.emitter.emit('should-clear-cache', { provider: this });
+ });
+ }
+
+ destroy() {
+ this.captureOrganizer.destroy();
+ this.disposable.dispose();
+ }
+
+ onShouldClearCache(callback) {
+ return this.emitter.on('should-clear-cache', callback);
+ }
+
+ canProvideSymbols(meta) {
+ let { editor, type } = meta;
+
+ // This provider can't crawl the whole project.
+ if (type === 'project' || type === 'project-find') return false;
+
+ // This provider works only for editors with Tree-sitter grammars.
+ let languageMode = editor?.getBuffer()?.getLanguageMode();
+ if (!languageMode?.atTransactionEnd) {
+ return false;
+ }
+
+ // This provider needs at least one layer to have a tags query.
+ let layers = languageMode.getAllLanguageLayers(l => !!l.tagsQuery);
+ if (layers.length === 0) {
+ return false;
+ }
+
+ // Return a value that will beat the built-in `ctags` provider, but which
+ // will (by default) lose to any provider from a community package.
+ return 0.999;
+ }
+
+ async getSymbols(meta) {
+ let { editor, signal } = meta;
+ let languageMode = editor?.getBuffer()?.getLanguageMode();
+ if (!languageMode) return null;
+
+ let scopeResolver = languageMode?.rootLanguageLayer?.scopeResolver;
+ if (!scopeResolver) return null;
+
+ let results = [];
+
+ // Wait for the buffer to be at rest so we know we're capturing against
+ // clean trees.
+ await languageMode.atTransactionEnd();
+
+ // The symbols-view package might've cancelled us in the interim.
+ if (signal.aborted) return null;
+
+ let layers = languageMode.getAllLanguageLayers(l => !!l.tagsQuery);
+ if (layers.length === 0) return null;
+
+ for (let layer of layers) {
+ let extent = layer.getExtent();
+
+ let captures = layer.tagsQuery.captures(
+ layer.tree.rootNode,
+ extent.start,
+ extent.end
+ );
+
+ results.push(
+ ...this.captureOrganizer.process(captures, scopeResolver)
+ );
+ }
+ results.sort((a, b) => a.position.compare(b.position));
+ return results;
+ }
+}
+
+module.exports = TreeSitterProvider;
diff --git a/packages/symbol-provider-tree-sitter/package-lock.json b/packages/symbol-provider-tree-sitter/package-lock.json
new file mode 100644
index 000000000..a2fa93a56
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/package-lock.json
@@ -0,0 +1,2122 @@
+{
+ "name": "symbol-provider-tree-sitter",
+ "version": "0.0.4",
+ "lockfileVersion": 2,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "symbol-provider-tree-sitter",
+ "version": "0.0.4",
+ "license": "MIT",
+ "devDependencies": {
+ "eslint": "^8.44.0",
+ "fs-plus": "^3.1.1",
+ "temp": "^0.9.4"
+ },
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0",
+ "node": ">=14"
+ }
+ },
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.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.55.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
+ "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.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": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "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/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/acorn": {
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "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": "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/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": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==",
+ "dev": true
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "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/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/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",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "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/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/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/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.55.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
+ "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.55.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "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.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "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",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "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.3",
+ "strip-ansi": "^6.0.1",
+ "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.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "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/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.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "node_modules/fs-plus": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
+ "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
+ "dev": true,
+ "dependencies": {
+ "async": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.2",
+ "underscore-plus": "1.x"
+ }
+ },
+ "node_modules/fs-plus/node_modules/rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ }
+ },
+ "node_modules/fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "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.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "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",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "dependencies": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "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-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-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-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "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/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.6"
+ },
+ "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/once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.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",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true,
+ "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/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/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "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/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": "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/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/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/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/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.9.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+ "dev": true,
+ "dependencies": {
+ "mkdirp": "^0.5.1",
+ "rimraf": "~2.6.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/temp/node_modules/rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "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.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
+ "dev": true
+ },
+ "node_modules/underscore-plus": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
+ "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
+ "dev": true,
+ "dependencies": {
+ "underscore": "^1.9.1"
+ }
+ },
+ "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/wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "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": {
+ "@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true
+ },
+ "@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "requires": {
+ "eslint-visitor-keys": "^3.3.0"
+ }
+ },
+ "@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true
+ },
+ "@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "requires": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.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.55.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.55.0.tgz",
+ "integrity": "sha512-qQfo2mxH5yVom1kacMtZZJFVdW+E70mqHMJvVg6WTLo+VBuQJ4TojZlfWBjK0ve5BdEeNAVxOsl/nvNMpJOaJA==",
+ "dev": true
+ },
+ "@humanwhocodes/config-array": {
+ "version": "0.11.13",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.13.tgz",
+ "integrity": "sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==",
+ "dev": true,
+ "requires": {
+ "@humanwhocodes/object-schema": "^2.0.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": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz",
+ "integrity": "sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw==",
+ "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"
+ }
+ },
+ "@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "acorn": {
+ "version": "8.11.2",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz",
+ "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==",
+ "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": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true
+ },
+ "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": "1.5.2",
+ "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
+ "integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w==",
+ "dev": true
+ },
+ "balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true
+ },
+ "brace-expansion": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
+ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "dev": true,
+ "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
+ },
+ "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"
+ }
+ },
+ "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",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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.55.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.55.0.tgz",
+ "integrity": "sha512-iyUUAM0PCKj5QpwGfmCAG9XXbZCWsqP/eWAWrG/W0umvjuLRBECwSFdt+rCntju0xEH7teIABPwXpahftIaTdA==",
+ "dev": true,
+ "requires": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.55.0",
+ "@humanwhocodes/config-array": "^0.11.13",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "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.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "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",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "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.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ }
+ },
+ "eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "requires": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ }
+ },
+ "eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true
+ },
+ "espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "requires": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ }
+ },
+ "esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "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
+ },
+ "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.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "requires": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ }
+ },
+ "flatted": {
+ "version": "3.2.9",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz",
+ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==",
+ "dev": true
+ },
+ "fs-plus": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.1.1.tgz",
+ "integrity": "sha512-Se2PJdOWXqos1qVTkvqqjb0CSnfBnwwD+pq+z4ksT+e97mEShod/hrNg0TRCCsXPbJzcIq+NuzQhigunMWMJUA==",
+ "dev": true,
+ "requires": {
+ "async": "^1.5.2",
+ "mkdirp": "^0.5.1",
+ "rimraf": "^2.5.2",
+ "underscore-plus": "1.x"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "fs.realpath": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
+ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==",
+ "dev": true
+ },
+ "glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "dev": true,
+ "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.23.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz",
+ "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==",
+ "dev": true,
+ "requires": {
+ "type-fest": "^0.20.2"
+ }
+ },
+ "graphemer": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+ "dev": true
+ },
+ "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.3.0",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
+ "integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
+ "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",
+ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
+ "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
+ "dev": true,
+ "requires": {
+ "once": "^1.3.0",
+ "wrappy": "1"
+ }
+ },
+ "inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "dev": true
+ },
+ "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-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-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-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true
+ },
+ "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
+ },
+ "keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "requires": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "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==",
+ "dev": true,
+ "requires": {
+ "brace-expansion": "^1.1.7"
+ }
+ },
+ "minimist": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+ "dev": true
+ },
+ "mkdirp": {
+ "version": "0.5.6",
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
+ "dev": true,
+ "requires": {
+ "minimist": "^1.2.6"
+ }
+ },
+ "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
+ },
+ "once": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
+ "requires": {
+ "wrappy": "1"
+ }
+ },
+ "optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "requires": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ }
+ },
+ "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",
+ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
+ "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
+ "dev": true
+ },
+ "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
+ },
+ "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
+ },
+ "punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "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
+ },
+ "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": "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"
+ }
+ },
+ "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"
+ }
+ },
+ "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
+ },
+ "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"
+ }
+ },
+ "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.9.4",
+ "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
+ "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
+ "dev": true,
+ "requires": {
+ "mkdirp": "^0.5.1",
+ "rimraf": "~2.6.2"
+ },
+ "dependencies": {
+ "rimraf": {
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
+ "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
+ "dev": true,
+ "requires": {
+ "glob": "^7.1.3"
+ }
+ }
+ }
+ },
+ "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.13.6",
+ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz",
+ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
+ "dev": true
+ },
+ "underscore-plus": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.7.0.tgz",
+ "integrity": "sha512-A3BEzkeicFLnr+U/Q3EyWwJAQPbA19mtZZ4h+lLq3ttm9kn8WC4R3YpuJZEXmWdLjYP47Zc8aLZm9kwdv+zzvA==",
+ "dev": true,
+ "requires": {
+ "underscore": "^1.9.1"
+ }
+ },
+ "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"
+ }
+ },
+ "wrappy": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true
+ },
+ "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/symbol-provider-tree-sitter/package.json b/packages/symbol-provider-tree-sitter/package.json
new file mode 100644
index 000000000..41cfe1614
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/package.json
@@ -0,0 +1,32 @@
+{
+ "name": "symbol-provider-tree-sitter",
+ "main": "./lib/main",
+ "version": "1.0.0",
+ "description": "Provides symbols to symbols-view based on tree-sitter queries",
+ "repository": "https://github.com/pulsar-edit/pulsar",
+ "license": "MIT",
+ "engines": {
+ "atom": ">=1.0.0 <2.0.0",
+ "node": ">=14"
+ },
+ "providedServices": {
+ "symbol.provider": {
+ "description": "Allows external sources to suggest symbols for a given file or project.",
+ "versions": {
+ "1.0.0": "provideSymbols"
+ }
+ }
+ },
+ "configSchema": {
+ "includeReferences": {
+ "default": false,
+ "type": "boolean",
+ "description": "Whether to make symbols out of references (for example, function calls) in addition to definitions."
+ }
+ },
+ "devDependencies": {
+ "eslint": "^8.44.0",
+ "fs-plus": "^3.1.1",
+ "temp": "^0.9.4"
+ }
+}
diff --git a/packages/symbol-provider-tree-sitter/spec/.eslintrc.js b/packages/symbol-provider-tree-sitter/spec/.eslintrc.js
new file mode 100644
index 000000000..8ee8742b4
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/.eslintrc.js
@@ -0,0 +1,13 @@
+module.exports = {
+ env: { jasmine: true },
+ globals: {
+ waitsForPromise: true
+ },
+ rules: {
+ "node/no-unpublished-require": "off",
+ "node/no-extraneous-require": "off",
+ "no-unused-vars": "off",
+ "no-empty": "off",
+ "no-constant-condition": "off"
+ }
+};
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/c/sample.c b/packages/symbol-provider-tree-sitter/spec/fixtures/c/sample.c
new file mode 100644
index 000000000..5a98bf4c1
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/c/sample.c
@@ -0,0 +1,6 @@
+#define UNUSED(x) (void)(x)
+
+static void f(int x)
+{
+ UNUSED(x);
+}
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/js/.eslintignore b/packages/symbol-provider-tree-sitter/spec/fixtures/js/.eslintignore
new file mode 100644
index 000000000..c8078be87
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/js/.eslintignore
@@ -0,0 +1,3 @@
+tagged.js
+sample.js
+other-file.js
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/js/no-symbols.js b/packages/symbol-provider-tree-sitter/spec/fixtures/js/no-symbols.js
new file mode 100644
index 000000000..9a1aba6f7
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/js/no-symbols.js
@@ -0,0 +1,6 @@
+// Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
+// tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
+// quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo
+// consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
+// cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
+// non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/js/other-file.js b/packages/symbol-provider-tree-sitter/spec/fixtures/js/other-file.js
new file mode 100644
index 000000000..4e79b3a98
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/js/other-file.js
@@ -0,0 +1,29 @@
+// Another file for symbols to exist in. Used for project search.
+
+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));
+};
+
+var quicksort2 = 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/symbol-provider-tree-sitter/spec/fixtures/js/sample.js b/packages/symbol-provider-tree-sitter/spec/fixtures/js/sample.js
new file mode 100644
index 000000000..15ab7c831
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/js/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/symbol-provider-tree-sitter/spec/fixtures/js/tagged.js b/packages/symbol-provider-tree-sitter/spec/fixtures/js/tagged.js
new file mode 100644
index 000000000..4adaac609
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/js/tagged.js
@@ -0,0 +1,11 @@
+var thisIsCrazy = true;
+
+function callMeMaybe() {
+ return "here's my number";
+}
+
+var iJustMetYou = callMeMaybe();
+
+function duplicate() {
+ return true;
+}
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/embed.rb b/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/embed.rb
new file mode 100644
index 000000000..e5a6e1cb2
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/embed.rb
@@ -0,0 +1,10 @@
+
+def foo
+
+ return <<-js
+ function bar () {
+ return 'aha!';
+ }
+ js
+
+end
diff --git a/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/file1.rb b/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/file1.rb
new file mode 100644
index 000000000..62697a7b3
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/fixtures/ruby/file1.rb
@@ -0,0 +1,33 @@
+module A::Foo
+ B = 'b'
+
+ def bar!
+
+ end
+
+ def bar?
+
+ end
+
+ def baz
+ end
+
+ def baz=(*)
+ end
+end
+
+if bar?
+ baz
+ bar!
+elsif !bar!
+ baz= 1
+ baz = 2
+ Foo = 3
+ { :baz => 4 }
+ A::Foo::B
+ C::Foo::B
+ D::Foo::E
+end
+
+module D::Foo
+end
diff --git a/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js b/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js
new file mode 100644
index 000000000..ad8b7fdf3
--- /dev/null
+++ b/packages/symbol-provider-tree-sitter/spec/symbol-provider-tree-sitter-spec.js
@@ -0,0 +1,551 @@
+
+const path = require('path');
+const fs = require('fs-plus');
+const temp = require('temp');
+const TreeSitterProvider = require('../lib/tree-sitter-provider');
+
+// Just for syntax highlighting.
+function scm(strings) {
+ return strings.join('');
+}
+
+function getEditor() {
+ return atom.workspace.getActiveTextEditor();
+}
+
+async function wait(ms) {
+ return new Promise(r => setTimeout(r, ms));
+}
+
+let provider;
+
+async function getSymbols(editor, type = 'file') {
+ let controller = new AbortController();
+ let symbols = await provider.getSymbols({
+ type,
+ editor,
+ signal: controller.signal
+ });
+
+ return symbols;
+}
+
+describe('TreeSitterProvider', () => {
+ let directory, editor;
+
+ beforeEach(async () => {
+ jasmine.unspy(global, 'setTimeout');
+ jasmine.unspy(Date, 'now');
+
+ atom.config.set('core.useTreeSitterParsers', true);
+ atom.config.set('core.useExperimentalModernTreeSitter', true);
+ await atom.packages.activatePackage('language-javascript');
+
+ atom.config.set('symbol-provider-tree-sitter.includeReferences', false);
+
+ provider = new TreeSitterProvider();
+
+ atom.project.setPaths([
+ temp.mkdirSync('other-dir-'),
+ temp.mkdirSync('atom-symbols-view-')
+ ]);
+
+ directory = atom.project.getDirectories()[1];
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'js'),
+ atom.project.getPaths()[1]
+ );
+
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'ruby'),
+ atom.project.getPaths()[1]
+ );
+ });
+
+ describe('when a tree-sitter grammar is used for a file', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ let languageMode = editor.getBuffer().getLanguageMode();
+ await languageMode.ready;
+ });
+
+ it('is willing to provide symbols for the current file', () => {
+ let meta = { type: 'file', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(0.999);
+ });
+
+ it('is not willing to provide symbols for an entire project', () => {
+ let meta = { type: 'project', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(false);
+ });
+
+ it('provides all JavaScript functions', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('when a non-tree-sitter grammar is used for a file', () => {
+ beforeEach(async () => {
+ atom.config.set('core.useTreeSitterParsers', false);
+ atom.config.set('core.useExperimentalModernTreeSitter', false);
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ });
+
+ it('is not willing to provide symbols for the current file', () => {
+ expect(editor.getGrammar().rootLanguageLayer).toBe(undefined);
+ let meta = { type: 'file', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(false);
+ });
+ });
+
+ // TODO: Test that `canProvideSymbols` returns `false` when no layer has a
+ // tags query.
+
+ describe('when the buffer is new and unsaved', () => {
+ let grammar;
+ beforeEach(async () => {
+ await atom.workspace.open();
+ editor = getEditor();
+ grammar = atom.grammars.grammarForId('source.js');
+ editor.setGrammar(grammar);
+ await editor.getBuffer().getLanguageMode().ready;
+ });
+
+ it('is willing to provide symbols', () => {
+ let meta = { type: 'file', editor };
+ expect(provider.canProvideSymbols(meta)).toBe(0.999);
+ });
+
+ describe('and has content', () => {
+ beforeEach(async () => {
+ let text = fs.readFileSync(
+ path.join(__dirname, 'fixtures', 'js', 'sample.js')
+ );
+ editor.setText(text);
+ await editor.getBuffer().getLanguageMode().atTransactionEnd();
+ });
+
+ it('provides symbols just as if the file were saved on disk', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+ });
+
+ describe('when the file has multiple language layers', () => {
+ beforeEach(async () => {
+ await atom.packages.activatePackage('language-ruby');
+ await atom.workspace.open(directory.resolve('embed.rb'));
+ editor = getEditor();
+ await editor.getBuffer().getLanguageMode().ready;
+ });
+
+ it('detects symbols across several layers', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('foo');
+ expect(symbols[0].position.row).toEqual(1);
+
+ expect(symbols[1].name).toBe('bar');
+ expect(symbols[1].position.row).toEqual(4);
+ });
+ });
+
+ describe('when the tags query contains @definition captures', () => {
+ let grammar;
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ let languageMode = editor.getBuffer().getLanguageMode();
+ await languageMode.ready;
+ grammar = editor.getGrammar();
+ await grammar.setQueryForTest(
+ 'tagsQuery',
+ scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ ) @definition.function
+ `
+ );
+ });
+
+ it('can infer tag names from those captures', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicksort');
+ expect(symbols[0].tag).toBe('function');
+
+ expect(symbols[1].name).toBe('sort');
+ expect(symbols[1].tag).toBe('function');
+ });
+ });
+
+ describe('when the tags query contains @reference captures', () => {
+ let grammar;
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ let languageMode = editor.getBuffer().getLanguageMode();
+ await languageMode.ready;
+ grammar = editor.getGrammar();
+ await grammar.setQueryForTest(
+ 'tagsQuery',
+ scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ ) @definition.function
+
+ (
+ (call_expression
+ function: (identifier) @name) @reference.call
+ (#not-match? @name "^(require)$"))
+ `
+ );
+ });
+
+ it('skips references when they are disabled in settings', async () => {
+ let symbols = await getSymbols(editor, 'file');
+ expect(symbols.length).toBe(2);
+ });
+
+ it('includes references when they are enabled in settings', async () => {
+ atom.config.set('symbol-provider-tree-sitter.includeReferences', true);
+ let symbols = await getSymbols(editor, 'file');
+ expect(symbols.length).toBe(5);
+ expect(symbols.map(s => s.tag)).toEqual(
+ ['function', 'function', 'call', 'call', 'call']
+ );
+ });
+
+ });
+
+ describe('when the tags query uses the predicate', () => {
+ let grammar;
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = getEditor();
+ let languageMode = editor.getBuffer().getLanguageMode();
+ await languageMode.ready;
+ grammar = editor.getGrammar();
+ });
+
+ describe('symbol.context', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.context "something")
+ )
+ `);
+ });
+
+ it('assigns a `context` property on each symbol', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].context).toBe('something');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].context).toBe('something');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.contextNode', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (property_identifier) @name
+ (#eq? @name "push")
+ (#set! symbol.contextNode "parent.firstNamedChild")
+ )
+ `);
+ });
+
+ it('assigns a `context` property on each symbol containing the text of the referenced node', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('push');
+ expect(symbols[0].context).toBe('left');
+ expect(symbols[0].position.row).toEqual(6);
+
+ expect(symbols[1].name).toBe('push');
+ expect(symbols[1].context).toBe('right');
+ expect(symbols[1].position.row).toEqual(6);
+ });
+ });
+
+ describe('symbol.icon', () => {
+ it('defines an `icon` property on each symbol', async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.icon "book")
+ )
+
+ `);
+
+ let symbols = await getSymbols(editor, 'file');
+ console.log('symbols:', symbols);
+
+ expect(symbols[0].icon).toBe('icon-book');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].icon).toBe('icon-book');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+
+ it('supersedes an `icon` property assigned by a tag', async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.tag "class")
+ (#set! symbol.icon "book")
+ )
+ `);
+
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].icon).toBe('icon-book');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].icon).toBe('icon-book');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+
+ it('supersedes an `icon` property inferred by its container', async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.tag "class")
+ (#set! symbol.icon "book")
+ ) @definition.namespace
+ `);
+
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].icon).toBe('icon-book');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].icon).toBe('icon-book');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.tag', () => {
+ it('defines a `tag` property on each symbol', async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.tag "class")
+ )
+ `);
+
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].tag).toBe('class');
+ expect(symbols[0].icon).toBe('icon-puzzle');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].tag).toBe('class');
+ expect(symbols[1].icon).toBe('icon-puzzle');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+
+ it('supersedes the `tag` property inferred by its container', async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.tag "class")
+ ) @definition.namespace
+ `);
+
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].tag).toBe('class');
+ expect(symbols[0].icon).toBe('icon-puzzle');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].tag).toBe('class');
+ expect(symbols[1].icon).toBe('icon-puzzle');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.strip', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.strip "ort$")
+ )
+ `);
+ });
+ it('strips the given text from each symbol', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicks');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('s');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.prepend', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.prepend "Foo: ")
+ )
+ `);
+ });
+ it('prepends the given text to each symbol', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('Foo: quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('Foo: sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.append', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! symbol.append " (foo)")
+ )
+
+ `);
+ });
+ it('appends the given text to each symbol', async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicksort (foo)');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('sort (foo)');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.prependTextForNode', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! test.onlyIfDescendantOfType function)
+ (#set! symbol.prependTextForNode "parent.parent.parent.parent.parent.firstNamedChild")
+ (#set! symbol.joiner ".")
+ (#set! test.final true)
+ )
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ )
+ `);
+ });
+ it(`prepends the associated node's text to each symbol`, async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('quicksort.sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ describe('symbol.prependSymbolForNode', () => {
+ beforeEach(async () => {
+ await grammar.setQueryForTest('tagsQuery', scm`
+ ; Outer function has prepended text...
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! test.onlyIfNotDescendantOfType function)
+ (#set! symbol.prepend "ROOT: ")
+ (#set! test.final true)
+ )
+ ; β¦which the inner function picks up on.
+ (
+ (variable_declaration
+ (variable_declarator
+ name: (identifier) @name
+ value: [(arrow_function) (function)]))
+ (#set! test.onlyIfDescendantOfType function)
+ (#set! symbol.prependSymbolForNode "parent.parent.parent.parent.parent.firstNamedChild")
+ (#set! symbol.joiner ".")
+ (#set! test.final true)
+ )
+ `);
+ });
+ it(`prepends the associated node's symbol name to each symbol`, async () => {
+ let symbols = await getSymbols(editor, 'file');
+
+ expect(symbols[0].name).toBe('ROOT: quicksort');
+ expect(symbols[0].position.row).toEqual(0);
+
+ expect(symbols[1].name).toBe('ROOT: quicksort.sort');
+ expect(symbols[1].position.row).toEqual(1);
+ });
+ });
+
+ });
+});
diff --git a/packages/symbols-view/.eslintrc.js b/packages/symbols-view/.eslintrc.js
new file mode 100644
index 000000000..00404ea92
--- /dev/null
+++ b/packages/symbols-view/.eslintrc.js
@@ -0,0 +1,42 @@
+module.exports = {
+ env: {
+ browser: true,
+ commonjs: true,
+ es2021: true,
+ },
+ extends: [
+ "eslint:recommended",
+ "plugin:node/recommended",
+ ],
+ overrides: [],
+ parserOptions: {
+ ecmaVersion: "latest"
+ },
+ rules: {
+ "no-fallthrough": "off",
+ "no-case-declarations": "off",
+ "space-before-function-paren": ["error", {
+ anonymous: "always",
+ asyncArrow: "always",
+ named: "never"
+ }],
+ "node/no-unpublished-require": [
+ "error",
+ {
+ allowModules: ["electron"]
+ }
+ ],
+ "node/no-missing-require": [
+ "error",
+ {
+ allowModules: ["atom"]
+ }
+ ]
+ },
+ plugins: [
+ "jsdoc"
+ ],
+ globals: {
+ atom: "writeable"
+ }
+};
diff --git a/packages/symbols-view/README.md b/packages/symbols-view/README.md
new file mode 100644
index 000000000..1975dc318
--- /dev/null
+++ b/packages/symbols-view/README.md
@@ -0,0 +1,27 @@
+# symbols-view
+
+Display a list of symbols in the editor. Typically, a symbol will correspond to a meaningful part of a source code file (like a function definition) but can refer to other important parts of files depending on context.
+
+## Providers
+
+`symbols-view` uses a provider/subscriber model similar to that of `autocomplete-plus`. This package implements the UI, but it relies on other packages to suggest symbols.
+
+### Built-in providers
+
+The original symbol provider, `ctags`, now lives in its own provider package called `symbol-provider-ctags`. Another package, `symbol-provider-tree-sitter`, is the preferred provider (by default) in buffers that use a Tree-sitter grammar.
+
+### Community package providers
+
+Any package can act as a symbol provider. [These are the packages on the Pulsar Package Repository that provide the `symbol.provider` service.](https://web.pulsar-edit.dev/packages?service=symbol.provider&serviceType=provided)
+
+## Commands
+
+|Command|Description|Keybinding (Linux/Windows)|Keybinding (macOS)|
+|-------|-----------|------------------|-----------------|
+|`symbols-view:toggle-file-symbols`|Show all symbols in current file|
ctrl-r |
cmd-r |
+|`symbols-view:toggle-project-symbols`|Show all symbols in the project|
ctrl-shift-r |
cmd-shift-r |
+|`symbols-view:go-to-declaration`|Jump to the symbol under the cursor|
ctrl-alt-down |
cmd-alt-down |
+|`symbols-view:return-from-declaration`|Return from the jump|
ctrl-alt-up |
cmd-alt-up |
+|`symbols-view:show-active-providers`|Display a list of all known symbol providers|||
+
+Commands relating to project-wide symbols may fail if no provider can satisfy a request for project-wide symbols. See `symbol-provider-ctags` for more information.
diff --git a/packages/symbols-view/keymaps/symbols-view.cson b/packages/symbols-view/keymaps/symbols-view.cson
new file mode 100644
index 000000000..b24b685bd
--- /dev/null
+++ b/packages/symbols-view/keymaps/symbols-view.cson
@@ -0,0 +1,20 @@
+'.platform-darwin atom-text-editor:not([mini])':
+ 'cmd-r': 'symbols-view:toggle-file-symbols'
+ 'cmd-alt-down': 'symbols-view:go-to-declaration'
+ 'cmd-alt-up': 'symbols-view:return-from-declaration'
+
+'.platform-win32 atom-text-editor:not([mini])':
+ 'ctrl-r': 'symbols-view:toggle-file-symbols'
+ 'ctrl-alt-shift-down': 'symbols-view:go-to-declaration'
+ 'ctrl-alt-shift-up': 'symbols-view:return-from-declaration'
+
+'.platform-linux atom-text-editor:not([mini])':
+ 'ctrl-r': 'symbols-view:toggle-file-symbols'
+ 'ctrl-alt-shift-down': 'symbols-view:go-to-declaration'
+ 'ctrl-alt-shift-up': 'symbols-view:return-from-declaration'
+
+'.platform-darwin':
+ 'cmd-shift-r': 'symbols-view:toggle-project-symbols'
+
+'.platform-win32, .platform-linux':
+ 'ctrl-shift-r': 'symbols-view:toggle-project-symbols'
diff --git a/packages/symbols-view/lib/config.js b/packages/symbols-view/lib/config.js
new file mode 100644
index 000000000..2de180149
--- /dev/null
+++ b/packages/symbols-view/lib/config.js
@@ -0,0 +1,44 @@
+const { CompositeDisposable, Emitter } = require('atom');
+
+const Config = {
+ activate() {
+ if (this.activated) return;
+ this.emitter ??= new Emitter();
+ this.subscriptions = new CompositeDisposable();
+
+ this.subscriptions.add(
+ atom.config.onDidChange('symbols-view', config => {
+ this.emitter.emit('did-change-config', config);
+ })
+ );
+ this.activated = true;
+ },
+
+ deactivate() {
+ this.activated = false;
+ this.subscriptions?.dispose();
+ },
+
+ getForEditor(editor, key) {
+ let grammar = editor.getGrammar();
+ return atom.config.get(`symbols-view.${key}`, { scope: [grammar?.scopeName] });
+ },
+
+ get(key) {
+ return atom.config.get(`symbols-view.${key}`);
+ },
+
+ set(key, value) {
+ return atom.config.set(`symbols-view.${key}`, value);
+ },
+
+ observe(key, callback) {
+ return atom.config.observe(`symbols-view.${key}`, callback);
+ },
+
+ onDidChange(callback) {
+ return this.emitter.on('did-change-config', callback);
+ }
+};
+
+module.exports = Config;
diff --git a/packages/symbols-view/lib/element-builder.js b/packages/symbols-view/lib/element-builder.js
new file mode 100644
index 000000000..88abc5fae
--- /dev/null
+++ b/packages/symbols-view/lib/element-builder.js
@@ -0,0 +1,52 @@
+
+function parseTagName (selector) {
+ if (!selector.includes('.')) {
+ return [selector, null];
+ }
+ let tagName = selector.substring(0, selector.indexOf('.'));
+ let classes = selector.substring(selector.indexOf('.') + 1);
+ let classList = classes.split('.');
+ return [tagName ?? 'div', classList];
+}
+
+function el (selector, ...args) {
+ let attributes = null;
+
+ if (typeof args[0] === 'object' && !args[0].nodeType) {
+ attributes = args.shift();
+ }
+
+ // Extract a tag name and any number of class names from the first argument.
+ let [tagName, classList] = parseTagName(selector);
+
+ let element = document.createElement(tagName);
+ if (attributes) {
+ // If an object is given as a second argument, it's a list of
+ // attribute/value pairs.
+ for (let [attr, value] of Object.entries(attributes)) {
+ element.setAttribute(attr, value);
+ }
+ }
+ // Any further arguments are children of the element.
+ for (let item of args) {
+ if (!item) continue;
+ if (typeof item === 'string') {
+ item = document.createTextNode(item);
+ } else if (Array.isArray(item)) {
+ // This is an array; append its children, but do not append it.
+ for (let n of item) {
+ element.appendChild(n);
+ }
+ continue;
+ }
+ element.appendChild(item);
+ }
+
+ if (classList) {
+ element.classList.add(...classList);
+ }
+
+ return element;
+}
+
+module.exports = el;
diff --git a/packages/symbols-view/lib/file-view.js b/packages/symbols-view/lib/file-view.js
new file mode 100644
index 000000000..b5dcad410
--- /dev/null
+++ b/packages/symbols-view/lib/file-view.js
@@ -0,0 +1,335 @@
+const { CompositeDisposable, Point } = require('atom');
+const { match } = require('fuzzaldrin');
+
+const Config = require('./config');
+const SymbolsView = require('./symbols-view');
+const el = require('./element-builder');
+const { badge, isIterable, timeout } = require('./util');
+
+class FileView extends SymbolsView {
+ constructor(stack, broker) {
+ super(stack, broker);
+ this.cachedResults = new Map();
+ // Cached results can be partially invalidated. If a provider wants to
+ // clear only its own cached results, keep track of it so that we know to
+ // ask it for new symbols in spite of the presence of other results in the
+ // cache.
+ this.providersWithInvalidatedCaches = new Map();
+ this.watchedEditors = new WeakSet();
+
+ this.editorsSubscription = atom.workspace.observeTextEditors(editor => {
+ if (this.watchedEditors.has(editor)) return;
+
+ const removeFromCache = (provider = null) => {
+ if (!provider) {
+ this.cachedResults.delete(editor);
+ this.providersWithInvalidatedCaches.delete(editor);
+ return;
+ }
+ let results = this.cachedResults.get(editor);
+ if (!results || results.length === 0) return;
+ results = results.filter(sym => {
+ return sym.providerId !== provider.packageName;
+ });
+ if (results.length === 0) {
+ // No other providers had cached any symbols, so we can do the simple
+ // thing here.
+ this.cachedResults.delete(editor);
+ this.providersWithInvalidatedCaches.delete(editor);
+ return;
+ }
+ // There's at least one remaining cached symbol. When we fetch this
+ // cache result, we need a way of knowing whether this cache entry is
+ // comprehensive. So we'll add this provider to a list of providers
+ // that will need re-querying.
+ this.cachedResults.set(editor, results);
+ let providers = this.providersWithInvalidatedCaches.get(editor);
+ if (!providers) {
+ providers = new Set();
+ this.providersWithInvalidatedCaches.set(editor, providers);
+ }
+ providers.add(provider);
+ };
+ const removeAllFromCache = () => removeFromCache(null);
+
+ const editorSubscriptions = new CompositeDisposable();
+ let buffer = editor.getBuffer();
+
+ // All the core actions that can invalidate the symbol cache.
+ editorSubscriptions.add(
+ // Some of them invalidate the entire cacheβ¦
+ editor.onDidChangeGrammar(removeAllFromCache),
+ editor.onDidSave(removeAllFromCache),
+ editor.onDidChangePath(removeAllFromCache),
+ buffer.onDidReload(removeAllFromCache),
+ buffer.onDidDestroy(removeAllFromCache),
+ buffer.onDidStopChanging(removeAllFromCache),
+ Config.onDidChange(removeAllFromCache),
+
+ // β¦and others invalidate only the cache for one specific provider.
+ this.broker.onDidAddProvider(removeFromCache),
+ this.broker.onDidRemoveProvider(removeFromCache),
+ this.broker.onShouldClearCache((bundle = {}) => {
+ let { provider = null, editor: someEditor = null } = bundle;
+ if (someEditor && editor.id !== someEditor.id) return;
+ removeFromCache(provider);
+ })
+ );
+
+ editorSubscriptions.add(
+ editor.onDidDestroy(() => {
+ this.watchedEditors.delete(editor);
+ editorSubscriptions.dispose();
+ })
+ );
+
+ this.watchedEditors.add(editor);
+ });
+ }
+
+ destroy() {
+ this.editorsSubscription.dispose();
+ return super.destroy();
+ }
+
+ elementForItem({ position, name, tag, icon, context, providerName }) {
+ // Style matched characters in search results.
+ const matches = match(name, this.selectListView.getFilterQuery());
+
+ let badges = [];
+ if (providerName && this.shouldShowProviderName) {
+ badges.push(providerName);
+ }
+ if (tag) {
+ badges.push(tag);
+ }
+
+ let primaryLineClasses = ['primary-line'];
+ if (this.showIconsInSymbolsView) {
+ if (icon) {
+ primaryLineClasses.push('icon', icon);
+ } else {
+ primaryLineClasses.push('no-icon');
+ }
+ }
+
+ // The βprimaryβ results line shows the symbol's name and its tag, if any.
+ let primary = el(`div.${primaryLineClasses.join('.')}`,
+ el('div.name',
+ SymbolsView.highlightMatches(this, name, matches)
+ ),
+ badges && el('div.badge-container',
+ ...badges.map(b => badge(b, { variant: this.useBadgeColors }))
+ )
+ );
+
+ // The βsecondaryβ results line shows the symbolβs row number and its
+ // context, if any.
+ let secondaryLineClasses = ['secondary-line'];
+ if (this.showIconsInSymbolsView) {
+ secondaryLineClasses.push('no-icon');
+ }
+ let secondary = el(`div.${secondaryLineClasses.join('.')}`,
+ el('span.location', `Line ${position.row + 1}`),
+ context && el('span.context', context)
+ );
+
+ return el('li.two-lines', primary, secondary);
+ }
+
+ didChangeSelection(item) {
+ let quickJump = Config.get('quickJumpToFileSymbol');
+ if (quickJump && item) this.openTag(item);
+ }
+
+ async didCancelSelection() {
+ this.abortController?.abort();
+ await this.cancel();
+ let editor = this.getEditor();
+ if (editor && this.initialState) {
+ this.deserializeEditorState(editor, this.initialState);
+ }
+ this.initialState = null;
+ }
+
+ didConfirmEmptySelection() {
+ this.abortController?.abort();
+ super.didConfirmEmptySelection();
+ }
+
+ async toggle(filterTerm = '') {
+ if (this.panel.isVisible()) await this.cancel();
+ let editor = this.getEditor();
+ // Remember exactly where the editor is so that we can restore that state
+ // if the user cancels.
+ let quickJump = Config.get('quickJumpToFileSymbol');
+ if (quickJump && editor) {
+ this.initialState = this.serializeEditorState(editor);
+ }
+
+ let populated = this.populate(editor);
+ if (!populated) return;
+ this.attach();
+ this.selectListView.update({ query: filterTerm, selectQuery: true });
+ }
+
+ serializeEditorState(editor) {
+ let editorElement = atom.views.getView(editor);
+ let scrollTop = editorElement.getScrollTop();
+
+ return {
+ bufferRanges: editor.getSelectedBufferRanges(),
+ scrollTop
+ };
+ }
+
+ deserializeEditorState(editor, { bufferRanges, scrollTop }) {
+ let editorElement = atom.views.getView(editor);
+
+ editor.setSelectedBufferRanges(bufferRanges);
+ editorElement.setScrollTop(scrollTop);
+ }
+
+ getEditor() {
+ return atom.workspace.getActiveTextEditor();
+ }
+
+ getPath() {
+ return this.getEditor()?.getPath();
+ }
+
+ getScopeName() {
+ return this.getEditor()?.getGrammar()?.scopeName;
+ }
+
+ isValidSymbol(symbol) {
+ if (!symbol.position || !(symbol.position instanceof Point)) return false;
+ if (typeof symbol.name !== 'string') return false;
+ return true;
+ }
+
+ async populate(editor) {
+ let result = this.cachedResults.get(editor);
+ let providersToQuery = this.providersWithInvalidatedCaches.get(editor);
+ if (result && !providersToQuery?.size) {
+ let symbols = result;
+ await this.updateView({
+ items: symbols
+ });
+ return true;
+ } else {
+ await this.updateView({
+ items: [],
+ loadingMessage: 'Generating symbols\u2026'
+ });
+ result = this.generateSymbols(editor, result, providersToQuery);
+ if (result?.then) result = await result;
+ this.providersWithInvalidatedCaches.delete(editor);
+
+ if (result == null) {
+ this.cancel();
+ return false;
+ }
+ result.sort((a, b) => a.position.compare(b.position));
+ await this.updateView({
+ items: result,
+ loadingMessage: null
+ });
+ return true;
+ }
+ }
+
+ async generateSymbols(editor, existingSymbols = null, onlyProviders = null) {
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+
+ let meta = { type: 'file', editor, timeout: this.timeoutMs };
+
+ // The signal is how a provider can stop doing work if it's going async,
+ // since it'll be able to tell if we've cancelled this command and no
+ // longer need the symbols we asked for.
+ let signal = this.abortController.signal;
+
+ let providers = await this.broker.select(meta);
+ // If our last cache result was only partially invalidated, `onlyProviders`
+ // will be a `Set` of providers that need re-querying β but only if the
+ // broker selected them again in the first place.
+ //
+ // When re-using a cache result that was preserved in its entirety, we
+ // don't give the broker a chance to assemble another list of providers. We
+ // should act similarly in the event of partial invalidation, and ignore
+ // any providers _except_ the ones whose caches were invalidated.
+ if (onlyProviders) {
+ providers = providers.filter(p => onlyProviders.has(p));
+ }
+
+ if (providers?.length === 0) {
+ // TODO: Either show the user a notification or just log a warning to the
+ // console, depending on the user's settings and whether we've notified
+ // about this already during this session.
+ return existingSymbols;
+ }
+
+ let done = (symbols, provider) => {
+ if (signal.aborted) return;
+ // If these don't match up, our results are stale.
+ if (signal !== this.abortController.signal) return;
+ if (!isIterable(symbols)) {
+ error(`Provider did not return a list of symbols`, provider);
+ return;
+ }
+ this.addSymbols(allSymbols, symbols, provider);
+ };
+
+ let error = (err, provider) => {
+ if (signal.aborted) return;
+ let message = typeof err === 'string' ? err : err.message;
+ console.error(`Error in retrieving symbols from provider ${provider.name}: ${message}`);
+ };
+
+ let tasks = [];
+ let allSymbols = existingSymbols ? [...existingSymbols] : [];
+ for (let provider of providers) {
+ try {
+ // Each provider can return a list of symbols directly or a promise.
+ let symbols = this.getSymbolsFromProvider(provider, signal, meta);
+ if (symbols?.then) {
+ // This is a promise, so we'll add it to the list of tasks that we
+ // need to wait for.
+ let task = symbols
+ .then((result) => done(result, provider))
+ .catch(err => error(err, provider));
+ tasks.push(task);
+ } else if (isIterable(symbols)) {
+ // This is a valid list of symbols, so the provider acted
+ // synchronously. Add it to the results list.
+ done(symbols, provider);
+ } else {
+ error(`Provider did not return a list of symbols`, provider);
+ }
+ } catch (err) {
+ error(err, provider);
+ }
+ }
+
+ if (tasks.length > 0) {
+ await Promise.race([Promise.allSettled(tasks), timeout(this.timeoutMs)]);
+ }
+
+ await this.updateView({ loadingMessage: null });
+
+ if (signal.aborted) {
+ // This means the user cancelled the task. No cleanup necessary; the
+ // `didCancelSelection` handler would've taken care of that.
+ return null;
+ }
+
+ if (allSymbols.length > 0) {
+ // Only cache non-empty results.
+ this.cachedResults.set(editor, allSymbols);
+ }
+ return allSymbols;
+ }
+}
+
+module.exports = FileView;
diff --git a/packages/symbols-view/lib/go-back-view.js b/packages/symbols-view/lib/go-back-view.js
new file mode 100644
index 000000000..113a19661
--- /dev/null
+++ b/packages/symbols-view/lib/go-back-view.js
@@ -0,0 +1,29 @@
+
+const SymbolsView = require('./symbols-view');
+
+// TODO: Does this really need to extend SymbolsView?
+module.exports = class GoBackView extends SymbolsView {
+ toggle() {
+ let previous = this.stack.pop();
+ if (!previous) return;
+
+ let restorePosition = () => {
+ if (!previous.position) return;
+ this.moveToPosition(previous.position, { beginningOfLine: false });
+ };
+
+ let allEditors = atom.workspace.getTextEditors();
+ let previousEditor = allEditors.find(e => e.id === previous.editorId);
+
+ if (previousEditor) {
+ let pane = atom.workspace.paneForItem(previousEditor);
+ pane.setActiveItem(previousEditor);
+ restorePosition();
+ } else if (previous.file) {
+ // The editor is not there anymore; e.g., a package like `zentabs` might
+ // have automatically closed it when a new editor view was opened. So we
+ // should restore it if we can.
+ atom.workspace.open(previous.file).then(restorePosition);
+ }
+ }
+}
diff --git a/packages/symbols-view/lib/go-to-view.js b/packages/symbols-view/lib/go-to-view.js
new file mode 100644
index 000000000..02a3d2d2f
--- /dev/null
+++ b/packages/symbols-view/lib/go-to-view.js
@@ -0,0 +1,114 @@
+const SymbolsView = require('./symbols-view');
+const { timeout } = require('./util');
+
+module.exports = class GoToView extends SymbolsView {
+ constructor (stack, broker) {
+ super(stack, broker);
+ }
+
+ toggle () {
+ if (this.panel.isVisible()) {
+ this.cancel();
+ } else {
+ this.populate();
+ }
+ }
+
+ detached () {
+ // TODO
+ this.abortController?.abort();
+ }
+
+ async populate () {
+ let editor = atom.workspace.getActiveTextEditor();
+ if (!editor) return;
+
+ let symbols = await this.generateSymbols(editor);
+
+ if (symbols?.length === 0) {
+ // TODO
+ console.warn('No symbols!');
+ return;
+ }
+
+ if (symbols.length === 1) {
+ if (this.openTag(symbols[0], { pending: true })) return;
+ }
+
+ // There must be multiple tags.
+ await this.updateView({ items: symbols });
+ this.attach();
+ }
+
+ shouldBePending () {
+ return true;
+ }
+
+ async generateSymbols (editor, range = null) {
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+
+ let meta = {
+ type: 'project-find',
+ editor,
+ paths: atom.project.getPaths()
+ };
+
+ if (range) {
+ meta.range = range;
+ meta.query = editor.getTextInBufferRange(range);
+ }
+
+ let signal = this.abortController.signal;
+
+ let providers = await this.broker.select(meta);
+ if (providers?.length === 0) {
+ // TODO
+ return [];
+ }
+
+ let allSymbols = [];
+
+ let done = (symbols, provider) => {
+ if (signal.aborted) return;
+ if (!Array.isArray(symbols)) {
+ error(`Provider did not return a list of symbols`, provider);
+ return;
+ }
+ this.addSymbols(allSymbols, symbols, provider);
+ };
+
+ let error = (err, provider) => {
+ if (signal.aborted) return;
+ let message = typeof err === 'string' ? err : err.message;
+ console.error(`Error in retrieving symbols from provider ${provider.name}: ${message}`);
+ };
+
+ let tasks = [];
+ for (let provider of providers) {
+ try {
+ let symbols = this.getSymbolsFromProvider(provider, signal, meta);
+ if (symbols?.then) {
+ let task = symbols
+ .then(result => done(result, provider))
+ .catch(err => error(err, provider));
+ tasks.push(task);
+ } else {
+ done(symbols, provider);
+ }
+ } catch (err) {
+ error(err, provider);
+ }
+ }
+
+ if (tasks.length > 0) {
+ await Promise.race([Promise.allSettled(tasks), timeout(this.timeoutMs)]);
+ }
+
+ if (signal.aborted) {
+ return null;
+ }
+
+ return allSymbols;
+ }
+};
diff --git a/packages/symbols-view/lib/main.d.ts b/packages/symbols-view/lib/main.d.ts
new file mode 100644
index 000000000..40f142537
--- /dev/null
+++ b/packages/symbols-view/lib/main.d.ts
@@ -0,0 +1,333 @@
+import type { TextEditor, Point, Range as AtomRange } from 'atom';
+
+type MaybePromise
= T | Promise;
+
+// The properties that a provider is allowed to modify on a `SelectList`
+// instance during symbol retrieval.
+//
+// A provider that marks itself as `isExclusive: true` will be allowed to set
+// certain UI messages on the `SelectList`. This allows the provider to offer
+// UI guidance messages.
+//
+// For instance, if a project-wide symbol search is taking place, the provider
+// could set an `emptyMessage` of βQuery must be at least X characters longβ to
+// explain why no results are present at first.
+type ListControllerParams = Partial<{
+ errorMessage: string,
+ emptyMessage: string,
+ loadingMessage: string,
+ laodingBadge: string
+}>;
+
+type ListControllerParamName = keyof ListControllerParams;
+
+// An object given to the exclusive provider during symbol retrieval. The
+// provider can use this to control certain aspects of the symbol list's UI.
+type ListController = {
+ // Set props on the `SelectList` instance.
+ set (params: ListControllerParams): void,
+
+ // Clear any number of props on the `SelectList` instance.
+ clear(...propNames: ListControllerParamName[]): void
+};
+
+export type SymbolPosition = {
+ // An instance of `Point` describing the symbol's location. The `column`
+ // value of the point may be ignored, depending on the user's settings. At
+ // least one of `position` and `range` must exist.
+ position: Point;
+};
+
+export type SymbolRange = {
+ // An exact range describing the bounds of a given token. If present, might
+ // be used to highlight the token when selected by the user, though that
+ // depends on the user's settings. At least one of `position` and `range`
+ // must exist.
+ range: AtomRange
+};
+
+export type SymbolDirectoryAndFile = {
+ // The name of the file that contains the symbol. Will be shown in the UI.
+ file: string,
+ // The path of to the directory of the file that contains the symbol. Should
+ // not contain the file name.
+ directory: string
+};
+
+export type SymbolPath = {
+ // The full path to the file that contains the symbol.
+ path: string
+};
+
+// The only required fields in a file symbol are (a) the `name` of the symbol,
+// and (b) a reference to its row in the buffer (via either `position` or
+// `range`). Other fields are optional, but make for a richer UI presentation.
+export type FileSymbol = (SymbolPosition | SymbolRange) & {
+ // The name of the symbol. This value will be shown in the UI and will be
+ // filtered against if the user types in the text box. Required.
+ name: string,
+
+ // A word representing the symbol in some way. Typically this would describe
+ // the symbol β function, constant, et cetera β but can be used however the
+ // provider sees fit. If present, will be included in the symbol list as a
+ // badge.
+ tag?: string
+
+ // A _short_ string of explanatory text. Optional. Can be used for text that
+ // is contexually significant to the symbol; for instance, a method or field
+ // might describe the class that owns it. Symbol consumers will expect this
+ // field to be short, and will not devote much space to it in the interface,
+ // so this field _should not_ contain unbounded text.
+ context?: string
+
+ // POSSIBLE ENHANCEMENTS (UNIMPLEMENTED!):
+ //
+ // I don't necessarily find these useful myself, or at least not useful
+ // enough to warrant their inclusion in a space-constrained list of symbols,
+ // but some people might want these to be present.
+
+ // A description of the symbol in code or pseudocode. For functions, this
+ // could be a function signature, along with parameter names and (if known)
+ // types.
+ //
+ // This field would receive its own line in a symbol list.
+ signature?: string
+
+ // The literal line of code containing the symbol. A symbol consumer could
+ // try to retrieve this information itself, but some symbol providers would
+ // be able to supply it much more simply.
+ //
+ // This field would receive its own line in a symbol list.
+ source?: string
+};
+
+// A project symbol has the additional requirement of specifying the file in
+// which each symbol is located, via either the `path` property or both
+// `directory` and `file`.
+export type ProjectSymbol = FileSymbol & (SymbolDirectoryAndFile | SymbolPath);
+
+// Metadata received by a symbol provider as part of a call to
+// `canProvideSymbols` or `getSymbols`.
+export type SymbolMeta = {
+ // The type of action being performed:
+ //
+ // * `file`: A symbol search within the current file.
+ // * `project`: A project-wide symbol search.
+ // * `project-find`: A project-wide attempt to resolve a reference based on
+ // (a) the position of the cursor, (b) the value of the editor's current
+ // text selection, or (c) whatever word was clicked on in the IDE.
+ type: 'file' | 'project' | 'project-find',
+
+ // The current text editor.
+ editor: TextEditor,
+
+ // The relevant search term, if any.
+ //
+ // When `type` is `project`, this will represent the text that the user has
+ // typed into a search field in order to filter the list of symbols.
+ //
+ // When `type` is `project-find`, this will represent the text that the IDE
+ // wants to resolve.
+ //
+ // When `type` is `file`, this field will be absent, because file symbols are
+ // queried only initially, before the user has typed anything; all winnowing
+ // is done on the frontend as the user types.
+ query?: string,
+
+ // The relevant range in the buffer.
+ //
+ // This may be present when `type` is `project-find` and the consumer wants
+ // to resolve an arbitrary buffer range instead of the word under the cursor.
+ range?: Range,
+
+ // An `AbortSignal` that represents whether the task has been cancelled. This
+ // will happen if the user cancels out of the symbol UI while waiting for
+ // symbols, or if they type a new character in the query field before the
+ // results have returned for the previous typed character. It will also
+ // happen if the provider exceeds the amount of time allotted to it (see
+ // `timeoutMs` below).
+ //
+ // If the provider goes async at any point, it should check the signal after
+ // resuming. If the signal has aborted, then there is no point in continuing.
+ // The provider should immediately return/resolve with `null` and avoid doing
+ // unnecessary further work.
+ signal: AbortSignal,
+
+ // The amount of time, in milliseconds, the provider has before it must
+ // return results. This value is configurable by the user. If the provider
+ // doesn't return anything after this amount of time, it will be ignored.
+ //
+ // The provider is not in charge of ensuring that it returns results within
+ // this amount of time; `symbols-view` enforces that on its own. This value
+ // is given to providers so that they can act wisely when faced with a choice
+ // between βsearch for more symbolsβ and βreturn what we have.β
+ //
+ // The `timeoutMs` property is only present when the appropriate symbol list
+ // UI is not yet present. Its purpose is to show the UI within a reasonable
+ // amount of time. If the UI is already present β for instance, when
+ // winnowing results in a project-wide symbol search β `timeoutMs` will be
+ // omitted, and the provider can take as much time as it deems appropriate.
+ timeoutMs?: number
+};
+
+type FileSymbolMeta = SymbolMeta & { type: 'file' };
+type ProjectSymbolMeta = SymbolMeta & { type: 'project' | 'project-find' };
+
+// Symbol metadata that will be passed to the `canProvideSymbols` method.
+export type PreliminarySymbolMeta = Omit;
+
+export interface SymbolProvider {
+ // A human-readable name for your provider. This name may be displayed to the
+ // user, and it's how they can configure `symbols-view` to prefer
+ // certain providers over others.
+ name: string,
+
+ // The name of your package. This is present so that the user can find out
+ // where a given provider comes from. In the settings for preferring certain
+ // providers over others, a package name can be specified instead of a
+ // specific provider name βΒ either because that's the value the user
+ // remembers, or because the package contains several providers and the user
+ // wishes to express a preference for all those providers at once.
+ packageName: string,
+
+ // If present, will be called on window teardown, or if `symbols-view`
+ // or the provider's own package is disabled.
+ destroy?(): void,
+
+ // An optional method. If it exists, the main package will use it to register
+ // a callback so that it can clear the cache of this provider's symbols.
+ //
+ // The main package will automatically clear its cache for these reasons:
+ //
+ // * when the main package's config changes (entire cache);
+ // * when any provider is activated or deactivated (single provider's cache);
+ // * when the buffer is modified in any of several ways, including grammar
+ // change, save, or buffer change (entire cache).
+ //
+ // If your provider may have its cache invalidated for reasons not in this
+ // list, you should implement `onShouldClearCache` and invoke any callback
+ // that registers for it. The `EventEmitter` pattern found throughout Pulsar
+ // is probably how you want to pull this off.
+ onShouldClearCache?(callback: () => TextEditor): void,
+
+ // Whether this provider aims to be the main symbol provider for a given
+ // file. The βexclusiveβ provider competes with the other workhorse providers
+ // of symbols like `ctags` and Tree-sitter to offer typical symbols like
+ // classes, method names, and the like. A maximum of one exclusive provider
+ // will be chosen for any task, depending on which one scores highest.
+ //
+ // βSupplementalβ providers are those that contribute more specialized kinds
+ // of symbols. These providers generally do not compete with exclusive
+ // providers, or with each other, and can add symbols to any exclusive
+ // providerβs results.
+ isExclusive?: boolean,
+
+ // Indicates whether the provider can provide symbols for a given task. Can
+ // return either a boolean or a number; boolean `true` is equivalent to a
+ // score of `1`, and boolean `false` is equivalent to a score of `0`.
+ //
+ // This method receives the same metadata bundle that will be present in the
+ // subsequent call to `getSymbols`. The provider can inspect this metadata
+ // and decide whether it can fulfill the given symbols request. It _should
+ // not_ start the task of gathering symbols; the point of this method is to
+ // determine which provider is best for the task without starting the work.
+ //
+ // Examples:
+ //
+ // * A provider that can analyze the current file, but not the entire
+ // project, should return `false` for any requests where `type` does not
+ // equal `file`.
+ // * A provider that works by analyzing code on disk, rather than looking at
+ // the current unsaved contents of buffers, could return a slightly lower
+ // score if asked to complete symbols for a file that has been modified.
+ // This would indicate that itβd be a slightly worse than usual candidate.
+ // * If my provider can do project-wide symbol search but _can't_ do a
+ // go-to-definition lookup, it can still serve as a fallback provider when
+ // `type` is `project-find`. But it should return a lower score to reflect
+ // that it's not the ideal choice.
+ //
+ // Since language server providers will have to ask their servers about
+ // capabilities, this method can go async, though itβs strongly suggested to
+ // keep it synchronous if possible. (The `timeoutMs` argument isn't yet
+ // enforced on `canProvideSymbols`, but we reserve the right to start
+ // enforcing it at any point without a bump in the service's version number.)
+ //
+ // To avoid a number war, any numeric value greater than `1` returned from
+ // `canProvideSymbols` will be clamped to `1`. The user can break ties by
+ // choosing their preferred providers in the package settings.
+ canProvideSymbols(meta: PreliminarySymbolMeta): MaybePromise,
+
+ // Returns a list of symbols.
+ //
+ // If there are no results, you should return an empty array. If the request
+ // is invalid or cannot be completed β for instance, if the user cancels the
+ // task β you should return `null`.
+ //
+ // The second argument, `listController`, will be present _only_ for the
+ // provider marked with `isExclusive: true`. It allows the provider to set
+ // and clear UI messages if needed. Supplemental providers don't receive this
+ // argument.
+ //
+ // This method can go async if needed. Whenever it performs an async task, it
+ // should check `meta.signal` afterward to see if it should cancel.
+ //
+ // The `type` property of `meta` affects which symbols this method should
+ // return:
+ //
+ // * If `type` is `file`, this method should return a full list of symbols
+ // for the current file.
+ //
+ // * If `type` is `project`, this method should return an _appropriate_ list
+ // of symbols for the project. The ideal approach would be to return only
+ // those results that match `meta.query`; you may choose not to return any
+ // symbols at all until `meta.query` is of a minimum length. But you may
+ // also return a full list of project symbols and rely on `symbols-view` to
+ // do all of the filtering as the user types. (In the latter case,
+ // `getSymbols` will still be called after each new keystroke; a future
+ // version of this service may offer a way to control that behavior.)
+ //
+ // If you return an empty list when `meta.query` is too short, you should
+ // use `listController` to set a message in the UI so that users understand
+ // why.
+ //
+ // * If `type` is `project-find`, the user is trying to find where
+ // `meta.query` is defined (a go-to-definition request). If this provider
+ // knows how to do that, it should find the answer and return it as the
+ // _only_ symbol in the returned list. If it doesn't, it is allowed to
+ // treat this similarly to a project-wide symbol search and return more
+ // than one result.
+ //
+ getSymbols(meta: FileSymbolMeta, listController?: ListController): MaybePromise
+ getSymbols(meta: ProjectSymbolMeta, listController?: ListController): MaybePromise
+}
+
+export type SymbolProviderMainModule = {
+ activate(): void,
+ deactivate(): void,
+
+ // No business logic should go in here. If a package wants to provide symbols
+ // only under certain circumstances, it should decide those circumstances on
+ // demand, rather than return this provider only conditionally.
+ //
+ // A provider author may argue that they should be allowed to inspect the
+ // environment before deciding what (or if) to return βΒ but anything they'd
+ // inspect is something that can change mid-session. Too complicated. All
+ // provider decisions can get decided at runtime.
+ //
+ // So, for instance, if a certain provider only works with PHP files, it
+ // should return its instance here no matter what, and that instance can
+ // respond to `canProvideSymbols` with `false` if the given editor isn't
+ // using a PHP grammar. It shouldn't try to get clever and bail out entirely
+ // if, say, the project doesn't have any PHP files on load β because, of
+ // course, it _could_ add a PHP file at any point, and we're not going to
+ // revisit the decision later.
+ //
+ // Likewise, if a provider depends upon a language server that may or may not
+ // be running, it should not try to be clever about what it returns from
+ // `provideSymbols`. Instead, it should return early from `canProvideSymbols`
+ // when the language server isn't running.
+ //
+ // A single package can supply multiple providers if need be.
+ //
+ provideSymbols(): SymbolProvider | SymbolProvider[],
+};
diff --git a/packages/symbols-view/lib/main.js b/packages/symbols-view/lib/main.js
new file mode 100644
index 000000000..a8aedeed7
--- /dev/null
+++ b/packages/symbols-view/lib/main.js
@@ -0,0 +1,250 @@
+const { Disposable, TextEditor } = require('atom');
+const Config = require('./config');
+const ProviderBroker = require('./provider-broker');
+const Path = require('path');
+const { shell } = require('electron');
+const { migrateOldConfigIfNeeded } = require('./util');
+
+const NO_PROVIDERS_MESSAGE = `You donβt have any symbol providers installed.`;
+const NO_PROVIDERS_DESCRIPTION = `The button below will show all packages that can provide symbols.
+
+At minimum, we recommend you install the following packages to get an experience similar to that of the built-in \`symbols-view\` package:
+
+* \`symbol-provider-tree-sitter\`
+* \`symbol-provider-ctags\`
+`;
+
+const NO_PROVIDERS_BUTTONS = [
+ {
+ text: 'Find providers',
+ onDidClick: () => {
+ shell.openExternal(`https://web.pulsar-edit.dev/packages?service=symbol.provider&serviceType=provided`);
+ }
+ }
+];
+
+module.exports = {
+ activate() {
+ Config.activate();
+ this.stack = [];
+ this.broker = new ProviderBroker();
+
+ this.workspaceSubscription = atom.commands.add(
+ 'atom-workspace',
+ {
+ 'symbols-view:toggle-project-symbols': (event) => {
+ if (!this.ensureProvidersExist()) {
+ event.abortKeyBinding();
+ return;
+ }
+ let text = this.getSelectedTextIfEnabled(event);
+ this.createProjectView().toggle(text);
+ },
+ 'symbols-view:show-active-providers': () => {
+ this.showActiveProviders();
+ }
+ }
+ );
+
+ this.editorSubscription = atom.commands.add(
+ 'atom-text-editor:not([mini])',
+ {
+ 'symbols-view:toggle-file-symbols': (event) => {
+ if (!this.ensureProvidersExist()) {
+ event.abortKeyBinding();
+ return;
+ }
+ let text = this.getSelectedTextIfEnabled(event);
+ this.createFileView().toggle(text);
+ },
+ 'symbols-view:go-to-declaration': () => {
+ if (!this.ensureProvidersExist()) return;
+ this.createGoToView().toggle();
+ },
+ 'symbols-view:return-from-declaration': () => {
+ if (!this.ensureProvidersExist()) return;
+ this.createGoBackView().toggle();
+ }
+ }
+ );
+
+ migrateOldConfigIfNeeded();
+ },
+
+ getSelectedTextIfEnabled(event) {
+ let editorView = event.target.closest('atom-text-editor');
+ if (!editorView) return '';
+ let editor = editorView.getModel();
+ let selection = editor.getLastSelection();
+
+ // Don't use the selection if it spans more than one buffer line.
+ let range = selection.getBufferRange();
+ if (range.start.row !== range.end.row) return '';
+
+ // Don't use the selection unless the associated config option is enabled.
+ let prefill = atom.config.get(
+ 'symbols-view.prefillSelectedText',
+ { scope: [editor.getGrammar()?.scopeName] }
+ );
+ return prefill ? editor.getSelectedText() : '';
+ },
+
+ deactivate() {
+ this.fileView?.destroy();
+ this.fileView = null;
+
+ this.projectView?.destroy();
+ this.projectView = null;
+
+ this.goToView?.destroy();
+ this.goToView = null;
+
+ this.goBackView?.destroy();
+ this.goBackView = null;
+
+ this.workspaceSubscription?.dispose();
+ this.workspaceSubscription = null;
+
+ this.editorSubscription?.dispose();
+ this.editorSubscription = null;
+
+ this.broker?.destroy();
+ this.broker = null;
+
+ this.subscriptions?.dispose();
+ this.subscriptions = null;
+ },
+
+ consumeSymbolProvider(provider) {
+ if (Array.isArray(provider)) {
+ this.broker.add(...provider);
+ } else {
+ this.broker.add(provider);
+ }
+
+ return new Disposable(() => {
+ if (Array.isArray(provider)) {
+ this.broker?.remove(...provider);
+ } else {
+ this.broker?.remove(provider);
+ }
+ });
+ },
+
+ createFileView() {
+ if (this.fileView) return this.fileView;
+
+ const FileView = require('./file-view');
+ this.fileView = new FileView(this.stack, this.broker);
+ return this.fileView;
+ },
+
+ createProjectView() {
+ if (this.projectView) return this.projectView;
+
+ const ProjectView = require('./project-view');
+ this.projectView = new ProjectView(this.stack, this.broker);
+ return this.projectView;
+ },
+
+ createGoToView() {
+ if (this.goToView) return this.goToView;
+
+ const GoToView = require('./go-to-view');
+ this.goToView = new GoToView(this.stack, this.broker);
+ return this.goToView;
+ },
+
+ createGoBackView() {
+ if (this.goBackView) return this.goBackView;
+
+ const GoBackView = require('./go-back-view');
+ this.goBackView = new GoBackView(this.stack, this.broker);
+ return this.goBackView;
+ },
+
+ showActiveProviders() {
+ let providerList = [];
+ for (let provider of this.broker.providers) {
+ providerList.push({
+ name: provider.name,
+ packageName: provider.packageName
+ });
+ }
+
+ let message = providerList.map(
+ p => `* **${p.name}** provided by \`${p.packageName}\``
+ ).join('\n');
+
+ atom.notifications.addInfo(
+ 'Symbols View Redux providers',
+ {
+ description: message,
+ dismissable: true,
+ buttons: [
+ {
+ text: 'Copy',
+ onDidClick() {
+ atom.clipboard.write(message);
+ }
+ }
+ ]
+ }
+ );
+ },
+
+ ensureProvidersExist() {
+ if (this.broker.providers.length > 0) return true;
+
+ atom.notifications.addWarning(
+ NO_PROVIDERS_MESSAGE,
+ {
+ description: NO_PROVIDERS_DESCRIPTION,
+ buttons: NO_PROVIDERS_BUTTONS,
+ dismissable: true
+ }
+ );
+
+ return false;
+ },
+
+ ensureActiveTextEditorExists() {
+ let editor = atom.workspace.getActiveTextEditor();
+ return !!editor;
+ },
+
+ // A `hyperclick` consumer that works similarly to the
+ // `symbols-view:go-to-definition` command.
+ provideHyperclick() {
+ return {
+ priority: 1,
+ getSuggestionForWord: async (editor, _text, range) => {
+ let goto = this.createGoToView();
+ let symbols = await goto.generateSymbols(editor, range);
+ let editorPath = editor.getPath();
+ if (!symbols || symbols.length === 0) return;
+
+ // If we're at the definition site, the only result will be a symbol
+ // whose position is identical to the position we asked about. Filter
+ // it out. In that situation, we don't want a hyperclick affordance at
+ // all.
+ symbols = symbols.filter(sym => {
+ let { path, directory, file } = sym;
+ if (!path) {
+ path = Path.join(directory, file);
+ }
+ return path !== editorPath || sym.position.compare(range.start) !== 0;
+ });
+ if (symbols.length === 0) return;
+
+ return {
+ range,
+ callback: () => {
+ editor.setSelectedBufferRange(range);
+ goto.toggle();
+ }
+ };
+ }
+ };
+ }
+};
diff --git a/packages/symbols-view/lib/project-view.js b/packages/symbols-view/lib/project-view.js
new file mode 100644
index 000000000..7988cdb3d
--- /dev/null
+++ b/packages/symbols-view/lib/project-view.js
@@ -0,0 +1,199 @@
+// const { Point } = require('atom');
+
+const SymbolsView = require('./symbols-view');
+const { isIterable, timeout } = require('./util');
+
+module.exports = class ProjectView extends SymbolsView {
+ constructor(stack, broker) {
+ // TODO: Do these defaults make sense? Should we allow a provider to
+ // override them?
+ super(stack, broker, {
+ emptyMessage: 'Project has no symbols or is empty',
+ maxResults: 20,
+ isDynamic: true
+ });
+
+ this.shouldReload = true;
+ }
+
+ destroy() {
+ return super.destroy();
+ }
+
+ toggle(filterTerm = '') {
+ if (this.panel.isVisible()) {
+ this.cancel();
+ } else {
+ this.populate();
+ this.attach();
+ this.selectListView.update({ query: filterTerm, selectQuery: true });
+ }
+ }
+
+ didCancelSelection() {
+ this.abortController?.abort();
+ super.didCancelSelection();
+ }
+
+ didConfirmEmptySelection() {
+ this.abortController?.abort();
+ super.didConfirmEmptySelection();
+ }
+
+ isValidSymbol(symbol) {
+ if (!super.isValidSymbol(symbol)) return false;
+ if (
+ !(typeof symbol.file === 'string' && typeof symbol.directory === 'string') &&
+ !(typeof symbol.path === 'string')
+ ) {
+ return false;
+ }
+ return true;
+ }
+
+ shouldUseCache() {
+ let query = this.selectListView?.getQuery();
+ if (query && query.length > 0) return false;
+ if (this.shouldReload) return false;
+ return !!this.cachedSymbols;
+ }
+
+ didChangeQuery() {
+ this.populate({ retain: true });
+ }
+
+ clear() {
+
+ }
+
+ async populate({ retain = false } = {}) {
+ if (this.shouldUseCache()) {
+ await this.updateView({ items: this.cachedSymbols });
+ return true;
+ }
+
+ let query = this.selectListView?.getQuery();
+
+ let listViewOptions = {
+ loadingMessage: this.cachedSymbols ?
+ `Reloading project symbols\u2026` :
+ `Loading project symbols\u2026`
+ };
+
+ if (!this.cachedSymbols) {
+ listViewOptions.loadingBadge = 0;
+ }
+
+ // await this.updateView(listViewOptions);
+ let editor = atom.workspace.getActiveTextEditor();
+
+ let start = performance.now();
+ this._lastTimestamp = start;
+ let anySymbolsLoaded = false;
+ let result = this.generateSymbols(editor, query, (symbols) => {
+ anySymbolsLoaded = symbols.length > 0;
+
+ // TODO: Should we sort by buffer position? Should we leave it up to the
+ // provider? Should we make it configurable?
+ let options = {
+ ...listViewOptions,
+ items: symbols,
+ loadingMessage: null
+ };
+ this.updateView(options);
+ });
+
+ let loadingTimeout;
+ if (retain) {
+ loadingTimeout = setTimeout((timestamp) => {
+ if (timestamp !== this._lastTimestamp) return;
+ if (anySymbolsLoaded) return;
+ this.updateView({ loadingMessage: `Reloading project symbols\u2026` });
+ }, 500, start);
+ }
+
+ if (result?.then) result = await result;
+ clearTimeout(loadingTimeout);
+ if (result == null) {
+ result = [];
+ }
+
+ this.cachedSymbols = result;
+
+ // TODO: We assume that project-wide symbol search will involve re-querying
+ // the language server whenever the user types another character. This
+ // distinguishes it from searching within one buffer β where typing in the
+ // query field just filters a static list.
+ //
+ // This is a safe assumption to make, but we could at least make it
+ // possible for a provider to return a static list and somehow indicate
+ // that itβs static so that we donβt have to keep re-querying.
+ this.shouldReload = true;
+ return true;
+ }
+
+ async generateSymbols(editor, query = '', callback) {
+ this.abortController?.abort();
+ this.abortController = new AbortController();
+
+ let meta = { type: 'project', editor, query };
+
+ // The signal is how a provider can stop doing work if it's going async,
+ // since it'll be able to tell if we've cancelled this command and no
+ // longer need the symbols we asked for.
+ let signal = this.abortController.signal;
+
+ let providers = await this.broker.select(meta);
+ if (providers?.length === 0) {
+ console.warn('No providers found!');
+ return null;
+ }
+
+ let allSymbols = [];
+ let done = (symbols, provider) => {
+ if (signal.aborted) return;
+ if (!isIterable(symbols)) {
+ error(`Provider did not return a list of symbols`, provider);
+ return;
+ }
+ this.addSymbols(allSymbols, symbols, provider);
+ callback(allSymbols);
+ };
+
+ let error = (err, provider) => {
+ if (signal.aborted) return;
+ let message = typeof err === 'string' ? err : err.message;
+ console.error(`Error in retrieving symbols from provider ${provider.name}: ${message}`);
+ };
+
+ let tasks = [];
+ for (let provider of providers) {
+ try {
+ let symbols = this.getSymbolsFromProvider(provider, signal, meta);
+ if (symbols?.then) {
+ let task = symbols
+ .then((result) => done(result, provider))
+ .catch(err => error(err, provider));
+ tasks.push(task);
+ } else if (isIterable(symbols)) {
+ done(symbols, provider);
+ } else {
+ error(`Provider did not return a list of symbols`, provider);
+ }
+ } catch (err) {
+ error(err, provider);
+ }
+ }
+
+ if (tasks.length > 0) {
+ await Promise.race([Promise.allSettled(tasks), timeout(this.timeoutMs)]);
+ }
+
+ // Since we might've gone async here, we should check our own signal. If
+ // it's aborted, that means the user has cancelled.
+ if (signal.aborted) return null;
+
+ this.cachedSymbols = allSymbols;
+ return allSymbols;
+ }
+};
diff --git a/packages/symbols-view/lib/provider-broker.js b/packages/symbols-view/lib/provider-broker.js
new file mode 100644
index 000000000..f70233627
--- /dev/null
+++ b/packages/symbols-view/lib/provider-broker.js
@@ -0,0 +1,244 @@
+const { CompositeDisposable, Emitter } = require('atom');
+const Config = require('./config');
+
+/**
+ * An error thrown when a newly added symbol provider does not conform to its
+ * contract.
+ *
+ * @extends Error
+ */
+class InvalidProviderError extends Error {
+ constructor(faults, provider) {
+ let packageName = provider.packageName ?
+ `the ${provider.packageName} provider`
+ : 'a symbol provider';
+ let message = `symbols-view failed to consume ${packageName} because certain properties are invalid: ${faults.join(', ')}. Please fix these faults or contact the package author.`;
+ super(message);
+ this.name = 'InvalidProviderError';
+ }
+}
+
+/**
+ * A class that keeps track of various symbol providers and selects which ones
+ * should respond for a given request.
+ *
+ * @type {ProviderBroker}
+ * @class
+ */
+module.exports = class ProviderBroker {
+ constructor() {
+ this.providers = [];
+ this.providerSubscriptions = new Map();
+ this.subscriptions = new CompositeDisposable();
+ this.emitter = new Emitter();
+ }
+
+ /**
+ * Add one or more symbol providers.
+ *
+ * @param {SymbolProvider[]} providers Any number of symbol providers.
+ */
+ add(...providers) {
+ for (let provider of providers) {
+ try {
+ this.validateSymbolProvider(provider);
+ } catch (err) {
+ console.warn(err.message);
+ continue;
+ }
+ this.providers.push(provider);
+ this.emitter.emit('did-add-provider', provider);
+ this.observeProvider(provider);
+ }
+ }
+
+ /**
+ * Remove one or more symbol providers.
+ *
+ * @param {SymbolProvider[]} providers Any number of symbol providers.
+ */
+ remove(...providers) {
+ for (let provider of providers) {
+ let index = this.providers.indexOf(provider);
+ // Providers that were invalid may not have been added. Not a problem.
+ if (index === -1) continue;
+
+ this.providers.splice(index, 1);
+ this.emitter.emit('did-remove-provider', provider);
+ this.stopObservingProvider(provider);
+ }
+ }
+
+ /**
+ * Ensure a symbol provider abides by the contract.
+ *
+ * @param {SymbolProvider} provider A symbol provider to validate.
+ */
+ validateSymbolProvider(provider) {
+ let faults = [];
+ if (typeof provider.name !== 'string') faults.push('name');
+ if (typeof provider.packageName !== 'string') faults.push('packageName');
+ if (typeof provider.canProvideSymbols !== 'function')
+ faults.push('canProvideSymbols');
+ if (typeof provider.getSymbols !== 'function')
+ faults.push('getSymbols');
+
+ if (faults.length > 0) {
+ throw new InvalidProviderError(faults, provider);
+ }
+ }
+
+ /**
+ * Observe a symbol provider so that we can clear our symbol cache if needed.
+ *
+ * @param {SymbolProvider} provider A symbol provider.
+ */
+ observeProvider(provider) {
+ let disposable = new CompositeDisposable();
+ this.providerSubscriptions.set(provider, disposable);
+
+ // Providers can implement `onShouldClearCache` when they want to control
+ // when symbols they provide are no longer valid.
+ if (!provider.onShouldClearCache) return;
+
+ disposable.add(
+ provider.onShouldClearCache((bundle) => {
+ this.emitter.emit('should-clear-cache', { ...bundle, provider });
+ })
+ );
+ }
+
+ /**
+ * Stop observing a symbol provider.
+ *
+ * @param {SymbolProvider} provider A symbol provider.
+ */
+ stopObservingProvider(provider) {
+ let disposable = this.providerSubscriptions.get(provider);
+ this.providerSubscriptions.delete(provider);
+ disposable?.dispose;
+ }
+
+ destroy() {
+ for (let provider of this.providers) {
+ provider?.destroy?.();
+ this.emitter.emit('did-remove-provider', provider);
+ }
+ }
+
+ onDidAddProvider(callback) {
+ return this.emitter.on('did-add-provider', callback);
+ }
+
+ onDidRemoveProvider(callback) {
+ return this.emitter.on('did-remove-provider', callback);
+ }
+
+ onShouldClearCache(callback) {
+ return this.emitter.on('should-clear-cache', callback);
+ }
+
+ /**
+ * Boost the relevance score of certain providers based on their position in
+ * the settings value. If there are 5 providers listed, the first one gets a
+ * five-point boost; the second a four-point boost; and so on.
+ *
+ * @param {String} name The provider's name.
+ * @param {String} packageName The provider's package name.
+ * @param {Array} preferredProviders A list of preferred providers from
+ * configuration; each entry can refer to a provider's name or its package
+ * name.
+ *
+ * @returns {Number} The amount by which to boost the provider's relevance
+ * score.
+ */
+ getScoreBoost(name, packageName, preferredProviders = []) {
+ let shouldLog = Config.get('enableDebugLogging');
+ if (packageName === 'unknown') return 0;
+ let index = preferredProviders.indexOf(packageName);
+ if (index === -1) {
+ index = preferredProviders.indexOf(name);
+ }
+ if (index === -1) return 0;
+ let scoreBoost = preferredProviders.length - index;
+ if (shouldLog)
+ console.log('Score boost for provider', name, packageName, 'is', scoreBoost);
+ return scoreBoost;
+ }
+
+ /**
+ * Given metadata about a symbol request, choose the best provider(s) for the
+ * job.
+ *
+ * @param {SymbolMeta} meta Metadata about the symbol request.
+ *
+ * @returns {Promise} A promise that resolves with a list
+ * of symbol providers.
+ */
+ async select(meta) {
+ let shouldLog = Config.get('enableDebugLogging');
+ let exclusivesByScore = [];
+ let results = [];
+
+ let preferredProviders = Config.getForEditor(meta.editor, 'preferCertainProviders');
+
+ if (shouldLog) {
+ console.debug(`Provider broker choosing among ${this.providers.length} candidates:`, this.providers);
+ console.debug('Metadata is:', meta);
+ }
+
+ let answers = this.providers.map(provider => {
+ // TODO: This method can reluctantly go async because language clients
+ // might have to ask their servers about capabilities. We must introduce
+ // a timeout value here so that we don't wait indefinitely for providers
+ // to respond.
+ if (shouldLog)
+ console.debug(`Asking provider:`, provider.name, provider);
+ return provider.canProvideSymbols(meta);
+ // return timeout(provider.canProvideSymbols(meta), 500);
+ });
+
+ let outcomes = await Promise.allSettled(answers);
+
+ for (let [index, provider] of this.providers.entries()) {
+ let outcome = outcomes[index];
+
+ if (shouldLog)
+ console.debug(`Outcome for provider`, provider.name, 'is', outcome);
+
+ if (outcome.status === 'rejected') continue;
+ let { value: score } = outcome;
+ let name = provider.name ?? 'unknown';
+ let packageName = provider?.packageName ?? 'unknown';
+ let isExclusive = provider?.isExclusive ?? false;
+
+ if (shouldLog)
+ console.debug('Score for', provider.name, 'is:', score);
+
+ if (!score) continue;
+ if (score === true) score = 1;
+ score += this.getScoreBoost(name, packageName, preferredProviders);
+
+ if (isExclusive) {
+ // βExclusiveβ providers get put aside until the end. We'll pick the
+ // _one_ that has the highest score.
+ exclusivesByScore.push({ provider, score });
+ } else {
+ // Non-exclusive providers go into the pile because we know we'll be
+ // using them all.
+ results.push(provider);
+ }
+ }
+
+ if (exclusivesByScore.length > 0) {
+ exclusivesByScore.sort((a, b) => b.score - a.score);
+ let exclusive = exclusivesByScore[0].provider;
+ results.unshift(exclusive);
+ }
+
+ if (shouldLog)
+ console.debug('Returned providers:', results);
+
+ return results;
+ }
+};
diff --git a/packages/symbols-view/lib/symbols-view.js b/packages/symbols-view/lib/symbols-view.js
new file mode 100644
index 000000000..0f5a8fb67
--- /dev/null
+++ b/packages/symbols-view/lib/symbols-view.js
@@ -0,0 +1,467 @@
+const Path = require('path');
+const fs = require('fs-plus');
+const Config = require('./config');
+const { CompositeDisposable, Point } = require('atom');
+const SelectListView = require('atom-select-list');
+const { match } = require('fuzzaldrin');
+
+const el = require('./element-builder');
+const { badge } = require('./util');
+
+// Properties that we allow a provider to set on a `SelectListView` via a
+// `ListController` instance.
+const ALLOWED_PROPS_IN_LIST_CONTROLLER = new Set([
+ 'errorMessage',
+ 'emptyMessage',
+ 'loadingMessage',
+ 'loadingBadge'
+]);
+
+function validateListControllerProps(props) {
+ return Object.keys(props).every(k => (
+ ALLOWED_PROPS_IN_LIST_CONTROLLER.has(k)
+ ));
+}
+
+// Assign a default icon type for each tag β or what LSP calls βkind.β This
+// list is copied directly from the LSP spec's exhaustive list of potential
+// symbol kinds:
+//
+// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#symbolKind
+function iconForTag(tag) {
+ switch (tag) {
+ case 'file':
+ return 'icon-file';
+ case 'module':
+ return 'icon-database';
+ case 'namespace':
+ return 'icon-tag';
+ case 'package':
+ return 'icon-package';
+ case 'class':
+ return 'icon-puzzle';
+ case 'method':
+ return 'icon-gear';
+ case 'property':
+ return 'icon-primitive-dot';
+ case 'field':
+ return 'icon-primitive-dot';
+ case 'constructor':
+ return 'icon-tools';
+ case 'enum':
+ return 'icon-list-unordered';
+ case 'interface':
+ return 'icon-key';
+ case 'function':
+ return 'icon-gear';
+ case 'variable':
+ return 'icon-code';
+ case 'constant':
+ return 'icon-primitive-square';
+ case 'string':
+ return 'icon-quote';
+ case 'number':
+ return 'icon-plus';
+ case 'boolean':
+ return 'icon-question';
+ case 'array':
+ return 'icon-list-ordered';
+ case 'object':
+ return 'icon-file-code';
+ case 'key':
+ return 'icon-key';
+ case 'null':
+ return null;
+ case 'enum-member':
+ return 'icon-primitive-dot';
+ case 'struct':
+ return 'icon-book';
+ case 'event':
+ return 'icon-calendar';
+ case 'operator':
+ return 'icon-plus';
+ case 'type-parameter':
+ return null;
+ default:
+ return null;
+ }
+}
+
+/**
+ * A class for setting various UI properties on a symbol list palette. This is a
+ * privilege given to the βmainβ (or _exclusive_) provider for a given task.
+ *
+ * This is how we allow a provider to communicate its state to the UI without
+ * giving it full control over the `SelectListView` used to show results.
+ */
+class ListController {
+ constructor(view) {
+ this.view = view;
+ }
+
+ set(props) {
+ if (!validateListControllerProps(props)) {
+ console.warn('Provider gave invalid properties to symbol list UI:', props);
+ }
+ return this.view.update(props);
+ }
+
+ clear(...propNames) {
+ let props = {};
+ for (let propName of propNames) {
+ if (!ALLOWED_PROPS_IN_LIST_CONTROLLER.has(propName)) continue;
+ props[propName] = null;
+ }
+ return this.view.update(props);
+ }
+}
+
+
+class SymbolsView {
+ static highlightMatches(_context, name, matches, offsetIndex = 0) {
+ let lastIndex = 0;
+ let matchedChars = [];
+
+ const fragment = document.createDocumentFragment();
+
+ for (let matchIndex of [...matches]) {
+ matchIndex -= offsetIndex;
+ if (matchIndex < 0) continue;
+
+ let unmatched = name.substring(lastIndex, matchIndex);
+ if (unmatched) {
+ if (matchedChars.length) {
+ let span = document.createElement('span');
+ span.classList.add('character-match');
+ span.textContent = matchedChars.join('');
+ fragment.appendChild(span);
+ }
+ matchedChars = [];
+ fragment.appendChild(document.createTextNode(unmatched));
+ }
+ matchedChars.push(name[matchIndex]);
+ lastIndex = matchIndex + 1;
+ }
+
+ if (matchedChars.length) {
+ const span = document.createElement('span');
+ span.classList.add('character-match');
+ span.textContent = matchedChars.join('');
+ fragment.appendChild(span);
+ }
+
+ // Remaining characters are plain text.
+ fragment.appendChild(
+ document.createTextNode(name.substring(lastIndex))
+ );
+
+ return fragment;
+ }
+
+ constructor(stack, broker, options = {}) {
+ this.stack = stack;
+ this.broker = broker;
+
+ options = {
+ emptyMessage: 'No symbols found',
+ maxResults: null,
+ ...options
+ };
+
+ this.selectListView = new SelectListView({
+ ...options,
+ items: [],
+ filterKeyForItem: (item) => item.name,
+ elementForItem: this.elementForItem.bind(this),
+ didChangeQuery: this.didChangeQuery.bind(this),
+ didChangeSelection: this.didChangeSelection.bind(this),
+ didConfirmSelection: this.didConfirmSelection.bind(this),
+ didConfirmEmptySelection: this.didConfirmEmptySelection.bind(this),
+ didCancelSelection: this.didCancelSelection.bind(this)
+ });
+
+ this.selectListViewOptions = options;
+
+ this.listController = new ListController(this.selectListView);
+
+ this.element = this.selectListView.element;
+ this.element.classList.add('symbols-view');
+
+ this.panel = atom.workspace.addModalPanel({ item: this, visible: false });
+
+ this.configDisposable = new CompositeDisposable();
+
+ this.configDisposable.add(
+ atom.config.observe(
+ `symbols-view`,
+ (value) => {
+ this.shouldShowProviderName = value.showProviderNamesInSymbolsView;
+ this.useBadgeColors = value.useBadgeColors;
+ }
+ ),
+ Config.observe('providerTimeout', (ms) => this.timeoutMs = ms),
+ Config.observe('showIconsInSymbolsView', (show) => this.showIconsInSymbolsView = show)
+ );
+
+ }
+
+ async destroy() {
+ await this.cancel();
+ this.configDisposable.dispose();
+ this.panel.destroy();
+ return this.selectListView.destroy();
+ }
+
+ getFilterKey() {
+ return 'name';
+ }
+
+ elementForItem({ position, name, file, icon, tag, context, directory, providerName }) {
+ name = name.replace(/\n/g, ' ');
+
+ if (atom.project.getPaths().length > 1) {
+ // More than one project root β we need to disambiguate the file paths.
+ file = Path.join(Path.basename(directory), file);
+ }
+
+ let badges = [];
+
+ if (providerName && this.shouldShowProviderName) {
+ badges.push(providerName);
+ }
+ if (tag) badges.push(tag);
+
+ let primaryLineClasses = ['primary-line'];
+ if (this.showIconsInSymbolsView) {
+ if (icon) {
+ primaryLineClasses.push('icon', icon);
+ } else {
+ primaryLineClasses.push('no-icon');
+ }
+ }
+
+ let matches = match(name, this.selectListView.getFilterQuery());
+ let primary = el(`div.${primaryLineClasses.join('.')}`,
+ el('div.name',
+ SymbolsView.highlightMatches(this, name, matches)
+ ),
+ badges && el('div.badge-container',
+ ...badges.map(
+ b => badge(b, { variant: this.useBadgeColors })
+ )
+ )
+ );
+
+ let secondaryLineClasses = ['secondary-line'];
+ if (this.showIconsInSymbolsView) {
+ secondaryLineClasses.push('no-icon');
+ }
+ let secondary = el(`div.${secondaryLineClasses.join('.')}`,
+ el('span.location',
+ position ? `${file}:${position.row + 1}` : file
+ ),
+ context ? el('span.context', context) : null
+ );
+
+ return el('li.two-lines', primary, secondary);
+ }
+
+ async cancel() {
+ if (!this.isCanceling) {
+ this.isCanceling = true;
+ await this.updateView({ items: [] });
+ this.panel.hide();
+ if (this.previouslyFocusedElement) {
+ this.previouslyFocusedElement.focus();
+ this.previouslyFocusedElement = null;
+ }
+ this.isCanceling = false;
+ }
+ }
+
+ async updateView(options) {
+ this.selectListView.update(options);
+ }
+
+ didChangeQuery() {
+ // no-op
+ }
+
+ didCancelSelection() {
+ this.cancel();
+ }
+
+ didConfirmEmptySelection() {
+ this.cancel();
+ }
+
+ async didConfirmSelection(tag) {
+ if (tag.file && !fs.isFileSync(Path.join(tag.directory, tag.file))) {
+ await this.updateView({
+ errorMessage: `Selected file does not exist`
+ });
+ setTimeout(() => {
+ this.updateView({ errorMessage: null });
+ }, 2000);
+ } else {
+ await this.cancel();
+ this.openTag(tag, { pending: this.shouldBePending() });
+ }
+ }
+
+ // Whether a pane opened by a view should be treated as a pending pane.
+ shouldBePending() {
+ return false;
+ }
+
+ didChangeSelection() {
+ // no-op
+ }
+
+ openTag(tag, { pending } = {}) {
+ pending ??= this.shouldBePending();
+ let editor = atom.workspace.getActiveTextEditor();
+ let previous;
+ if (editor) {
+ previous = {
+ editorId: editor.id,
+ position: editor.getCursorBufferPosition(),
+ file: editor.getURI()
+ };
+ }
+
+ let { position, range } = tag;
+ if (!position) position = this.getTagLine(tag);
+
+ let result = false;
+ if (tag.file) {
+ // Open a different file, then jump to a position.
+ atom.workspace.open(
+ Path.join(tag.directory, tag.file),
+ { pending, activatePane: false }
+ ).then(() => {
+ if (position) {
+ return this.moveToPosition(position, { range });
+ }
+ return undefined;
+ });
+ result = true;
+ } else if (position && previous && !previous.position.isEqual(position)) {
+ // Jump to a position in the same file.
+ this.moveToPosition(position, { range });
+ result = true;
+ }
+ if (result) this.stack.push(previous);
+ return result;
+ }
+
+ moveToPosition(position, { beginningOfLine = true } = {}) {
+ let editor = atom.workspace.getActiveTextEditor();
+ if (editor) {
+ editor.setCursorBufferPosition(position, { autoscroll: false });
+ if (beginningOfLine) {
+ editor.moveToFirstCharacterOfLine();
+ }
+ editor.scrollToCursorPosition({ center: true });
+ }
+ }
+
+ attach() {
+ this.previouslyFocusedElement = document.activeElement;
+ this.panel.show();
+ this.selectListView.reset();
+ this.selectListView.focus();
+ }
+
+ isValidSymbol(symbol) {
+ if (typeof symbol.name !== 'string') return false;
+ if (!symbol.position && !symbol.range) return false;
+ if (symbol.position && !(symbol.position instanceof Point)) {
+ return false;
+ }
+ return true;
+ }
+
+ normalizeSymbol(symbol, provider) {
+ // We enforce these so that (a) we can show a human-readable name of the
+ // provider for each symbol (if the user opts into it), and (b) we can
+ // selectively clear cached results for certain providers without
+ // affecting others.
+ symbol.providerName ??= provider.name;
+ symbol.providerId ??= provider.packageName;
+ if (symbol.path) {
+ let parts = Path.parse(symbol.path);
+ symbol.directory = `${parts.dir}${Path.sep}`;
+ symbol.file = parts.base;
+ }
+ symbol.name = symbol.name.replace(/[\n\r\t]/, ' ');
+
+ if (symbol.tag && !symbol.icon) {
+ symbol.icon = iconForTag(symbol.tag);
+ }
+ }
+
+ addSymbols(allSymbols, newSymbols, provider) {
+ for (let symbol of newSymbols) {
+ if (!this.isValidSymbol(symbol)) {
+ console.warn('Invalid symbol:', symbol);
+ continue;
+ }
+
+ this.normalizeSymbol(symbol, provider);
+ allSymbols.push(symbol);
+ }
+ }
+
+ // TODO: What on earth is this? Can we possibly still need it?
+ getTagLine(tag) {
+ if (!tag) return undefined;
+
+ if (tag.lineNumber) {
+ return new Point(tag.lineNumber - 1, 0);
+ }
+
+ if (!tag.pattern) return undefined;
+ let pattern = tag.pattern.replace(/(^\/\^)|(\$\/$)/g, '').trim();
+ if (!pattern) return undefined;
+
+ const file = Path.join(tag.directory, tag.file);
+ if (!fs.isFileSync(file)) return undefined;
+
+ let iterable = fs.readFileSync(file, 'utf8').split('\n');
+ for (let index = 0; index < iterable.length; index++) {
+ let line = iterable[index];
+ if (pattern === line.trim()) {
+ return new Point(index, 0);
+ }
+ }
+
+ return undefined;
+ }
+
+ getSymbolsFromProvider(provider, signal, meta) {
+ let controller = new AbortController();
+
+ // If the user cancels the task, propagate that cancellation to this
+ // provider's AbortController.
+ signal.addEventListener('abort', () => controller.abort());
+
+ // Cancel this job automatically if it times out.
+ setTimeout(() => controller.abort(), this.timeoutMs);
+
+ if (provider.isExclusive) {
+ // The exclusive provider is the only one that gets an instance of
+ // `ListController` so that it can set UI messages.
+ return provider.getSymbols(
+ { signal: controller.signal, ...meta },
+ this.listController
+ );
+ } else {
+ return provider.getSymbols(
+ { signal: controller.signal, ...meta }
+ );
+ }
+ }
+}
+
+module.exports = SymbolsView;
diff --git a/packages/symbols-view/lib/util.js b/packages/symbols-view/lib/util.js
new file mode 100644
index 000000000..bc9808e37
--- /dev/null
+++ b/packages/symbols-view/lib/util.js
@@ -0,0 +1,161 @@
+const el = require('./element-builder');
+
+const murmur = require('murmurhash-js');
+const BADGE_TEXT_HASH_MAP = new Map();
+
+/**
+ * Ensures an object can be iterated over.
+ *
+ * The contract with the symbol providers is that they return an object that
+ * gives us symbol objects when we iterate over it. It'll probably be an array,
+ * but we're cool with anything iterable.
+ *
+ * @param {?} obj Anything.
+ * @returns {Boolean} Whether the item will respond correctly to a `for..of`
+ * loop.
+ */
+function isIterable(obj) {
+ if (obj === null || obj === undefined) return false;
+ return typeof obj[Symbol.iterator] === 'function';
+}
+
+/**
+ * Returns a promise that resolves after a given number of milliseconds.
+ * @param {Number} ms Number of milliseconds after which to resolve.
+ * @returns {Promise} A promise that resolves with `true` as its argument.
+ */
+function timeout(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms, true));
+}
+
+/**
+ * Given a string of text, returns a hexadecimal character from `0` to `f` to
+ * represent a classification βbucket.β This is used when assigning colors to
+ * various symbol badges.
+ *
+ * @param {String} text The text of the badge.
+ * @returns {String} A single character that represents a hexadecimal digit.
+ */
+function getBadgeTextVariant(text) {
+ // The goal here is to give each tag a color such that (a) two things with the
+ // same tag will have badges of identical colors, and (b) two things with
+ // different tags are very likely to have badges of different colors. We use a
+ // fast (non-cryptographic) hashing algorithm, convert its return integer to
+ // hex, then take the final character; this, in effect, gives a piece of text
+ // an equal chance of being sorted into any of sixteen random buckets.
+ //
+ // In the CSS, we generate sixteen badge colors based on the user's UI theme;
+ // they are identical in saturation and brightness and vary only in hue.
+ if (BADGE_TEXT_HASH_MAP.has(text)) {
+ return BADGE_TEXT_HASH_MAP.get(text);
+ }
+ let hash = murmur.murmur3(text, 'symbols-view').toString(16);
+ let variantType = hash.charAt(hash.length - 1);
+ BADGE_TEXT_HASH_MAP.set(text, variantType);
+ return variantType;
+}
+
+/**
+ * Return a DOM element for a badge for a given symbol tag name.
+ *
+ * @param {String} text The text of the tag.
+ * @param {Object} options Options. Defaults to an empty object.
+ * @param {Boolean} options.variant Whether to add a class name for the badge's
+ * βvariant.β If enabled, this will attempt to assign a different badge color
+ * for each kind of tag. Optional; defaults to `false`.
+ * @returns {Element} An element for adding to an `atom-select-view` entry.
+ */
+function badge(text, options = {}) {
+ let { variant = false } = options;
+ let classNames = `.badge.badge-info.badge-flexible.badge-symbol-tag`;
+ if (variant) {
+ let variantType = getBadgeTextVariant(text);
+ classNames += `.symbols-view-badge-variant-${variantType}`;
+ }
+ return el(`span${classNames}`, text);
+}
+
+const MIGRATED_SETTINGS_MESSAGE = `The \`symbols-view\` package has migrated the setting \`symbols-view.useEditorGrammarAsCtagsLanguage\` to its new location inside the core package \`symbol-provider-ctags\`. If you have defined any scope-specific overrides for this setting, youβll need to change those overrides manually.`;
+
+// TODO: This function performs a one-time config migration. We can remove this
+// chore in the future when we feel like it's served its purpose.
+function migrateOldConfigIfNeeded({ force = false } = {}) {
+ if (!force) {
+ // Look up the schema for `symbols-view` and make sure that the deprecated
+ // setting isn't present.
+ let schema = atom.config.getSchema('symbols-view');
+ if (schema?.type === 'any') {
+ // We might be in a testing environment.
+ return;
+ }
+
+ if (!schema.properties || ('useEditorGrammarAsCtagsLanguage' in schema.properties)) {
+ // This means the setting is still expected as part of `symbols-view` and
+ // should not be migrated.
+ return;
+ }
+ }
+
+ // If we get this far, we know that any
+ // `symbols-view.useEditorGrammarAsCtagsLanguage` setting we find should be
+ // migrated to `symbol-provider-ctags.useEditorGrammarAsCtagsLanguage`.
+
+ // We're only interested in the value we get directly from the config file.
+ let settings = atom.config.get(
+ 'symbols-view',
+ { sources: [atom.config.getUserConfigPath()] }
+ ) || {};
+
+ if ('useEditorGrammarAsCtagsLanguage' in settings) {
+ atom.config.set(
+ 'symbol-provider-ctags.useEditorGrammarAsCtagsLanguage',
+ settings.useEditorGrammarAsCtagsLanguage
+ );
+ atom.config.unset('symbols-view.useEditorGrammarAsCtagsLanguage');
+ }
+
+ // 99% of users won't care that we've migrated this setting, but the other 1%
+ // would appreciate a heads-up. So let's try to detect any scope-specific
+ // overrides that have touched this setting. We won't attempt to migrate
+ // those automatically, but we'll at least let the user know that they might
+ // have to be changed.
+ //
+ // This reaches into the private implementation of `atom.config`, so let's
+ // exit gracefully if this fails.
+ let propertySets = atom.config?.scopedSettingsStore.propertySets;
+ if (!propertySets) return;
+
+ let sources = [];
+ for (let { properties, source } of propertySets) {
+ if (
+ properties['symbols-view'] &&
+ ('useEditorGrammarAsCtagsLanguage' in properties['symbols-view'])
+ ) {
+ sources.push(source);
+ }
+ }
+
+ if (sources.length > 0) {
+ sources = Array.from(new Set(sources));
+ let overrideSources = sources.map(s => `* \`${s}\``).join('\n');
+
+ atom.notifications.addInfo(
+ `Setting migrated`,
+ {
+ description: `${MIGRATED_SETTINGS_MESSAGE}
+
+Detected overrides in the following locations:
+
+${overrideSources}`,
+ dismissable: true
+ }
+ );
+ }
+}
+
+module.exports = {
+ badge,
+ isIterable,
+ migrateOldConfigIfNeeded,
+ timeout
+};
diff --git a/packages/symbols-view/package.json b/packages/symbols-view/package.json
new file mode 100644
index 000000000..8e1003300
--- /dev/null
+++ b/packages/symbols-view/package.json
@@ -0,0 +1,91 @@
+{
+ "name": "symbols-view",
+ "version": "1.0.0",
+ "main": "./lib/main",
+ "types": "./lib/main.d.ts",
+ "description": "Jump to a function/method in the current editor or in the project.",
+ "repository": "https://github.com/pulsar-edit/pulsar",
+ "engines": {
+ "atom": "*",
+ "node": ">=14"
+ },
+ "configSchema": {
+ "quickJumpToFileSymbol": {
+ "default": true,
+ "type": "boolean",
+ "order": 1,
+ "description": "Automatically visit selected file-symbols as you navigate the symbols list."
+ },
+ "showIconsInSymbolsView": {
+ "default": true,
+ "type": "boolean",
+ "order": 2,
+ "description": "When enabled, an icon will be shown alongside a symbol if the symbol provider specifies one."
+ },
+ "useBadgeColors": {
+ "default": false,
+ "type": "boolean",
+ "order": 3,
+ "description": "Whether to use an assortment of colors for symbol badges. If enabled, each badge will be one of sixteen colors based on its text. Badge colors are generated automatically as hue variants of your themeβs ordinary badge color."
+ },
+ "prefillSelectedText": {
+ "default": true,
+ "type": "boolean",
+ "order": 4,
+ "description": "When enabled, any text you may have selected in the active editor will be used as the search query in the symbols list."
+ },
+ "showProviderNamesInSymbolsView": {
+ "default": false,
+ "type": "boolean",
+ "order": 5,
+ "description": "When enabled, the name of the provider will be shown alongside each result."
+ },
+ "preferCertainProviders": {
+ "default": [],
+ "type": "array",
+ "order": 6,
+ "items": {
+ "type": "string"
+ },
+ "description": "A comma-separated list of preferred providers. Used to help break ties when more than one provider can contribute symbols. Anything on this list will be preferred over anything not on this list, and earlier items will be preferred over later items. (A provider can be identified by its official name or its package name; run the **Symbols View: Show Active Providers** command to see both values.)"
+ },
+ "providerTimeout": {
+ "default": 2000,
+ "type": "number",
+ "order": 7,
+ "description": "How long providers have to respond to symbol requests before this package gives up and shows the list. If a certain provider is particularly slow, you may have to increase this value. (Does not apply to project-wide symbol search **if** the list is already visible.)"
+ },
+ "enableDebugLogging": {
+ "default": false,
+ "type": "boolean",
+ "order": 10,
+ "description": "Whether to log certain diagnostic information to the console. (For example: which provider is chosen for a given task.)"
+ }
+ },
+ "consumedServices": {
+ "symbol.provider": {
+ "description": "Allows external sources to suggest symbols for a given file or project.",
+ "versions": {
+ "1.0.0": "consumeSymbolProvider"
+ }
+ }
+ },
+ "providedServices": {
+ "hyperclick": {
+ "versions": {
+ "0.1.0": "provideHyperclick"
+ }
+ }
+ },
+ "license": "MIT",
+ "dependencies": {
+ "atom-select-list": "^0.8.1",
+ "fs-plus": "^3.1.1",
+ "fuzzaldrin": "^2.1.0",
+ "murmurhash-js": "^1.0.0"
+ },
+ "devDependencies": {
+ "eslint": "^8.39.0",
+ "temp": "^0.9.4"
+ }
+}
diff --git a/packages/symbols-view/spec/.eslintrc.js b/packages/symbols-view/spec/.eslintrc.js
new file mode 100644
index 000000000..81e4f8437
--- /dev/null
+++ b/packages/symbols-view/spec/.eslintrc.js
@@ -0,0 +1,10 @@
+module.exports = {
+ env: { jasmine: true },
+ rules: {
+ "node/no-unpublished-require": "off",
+ "node/no-extraneous-require": "off",
+ "no-unused-vars": "off",
+ "no-empty": "off",
+ "no-constant-condition": "off"
+ }
+};
diff --git a/packages/symbols-view/spec/async-spec-helpers.js b/packages/symbols-view/spec/async-spec-helpers.js
new file mode 100644
index 000000000..7a2257747
--- /dev/null
+++ b/packages/symbols-view/spec/async-spec-helpers.js
@@ -0,0 +1,67 @@
+/** @babel */
+
+export function beforeEach (fn) {
+ global.beforeEach(function () {
+ const result = fn();
+ if (result instanceof Promise) {
+ waitsForPromise(() => result);
+ }
+ });
+}
+
+export function afterEach (fn) {
+ global.afterEach(function () {
+ const result = fn();
+ if (result instanceof Promise) {
+ waitsForPromise(() => result);
+ }
+ });
+}
+
+['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
+ module.exports[name] = function (description, fn) {
+ global[name](description, function () {
+ const result = fn();
+ if (result instanceof Promise) {
+ waitsForPromise(() => result);
+ }
+ });
+ };
+});
+
+export async function conditionPromise (condition) {
+ const startTime = Date.now();
+
+ while (true) {
+ await timeoutPromise(100);
+
+ let conditionResult = condition();
+ if (conditionResult instanceof Promise) {
+ conditionResult = await conditionResult;
+ }
+
+ if (conditionResult) {
+ return;
+ }
+
+ if (Date.now() - startTime > 5000) {
+ throw new Error('Timed out waiting on condition');
+ }
+ }
+}
+
+export function timeoutPromise (timeout) {
+ return new Promise(function (resolve) {
+ global.setTimeout(resolve, timeout);
+ });
+}
+
+function waitsForPromise (fn) {
+ const promise = fn();
+ global.waitsFor('spec promise to resolve', function (done) {
+ promise.then(done, function (error) {
+ jasmine.getEnv().currentSpec.fail(error);
+ done();
+ });
+ });
+}
diff --git a/packages/symbols-view/spec/fixtures/js/other-file.js b/packages/symbols-view/spec/fixtures/js/other-file.js
new file mode 100644
index 000000000..4e79b3a98
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/js/other-file.js
@@ -0,0 +1,29 @@
+// Another file for symbols to exist in. Used for project search.
+
+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));
+};
+
+var quicksort2 = 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/symbols-view/spec/fixtures/js/sample.js b/packages/symbols-view/spec/fixtures/js/sample.js
new file mode 100644
index 000000000..15ab7c831
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/js/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/symbols-view/spec/fixtures/js/tagged.js b/packages/symbols-view/spec/fixtures/js/tagged.js
new file mode 100644
index 000000000..932d7750c
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/js/tagged.js
@@ -0,0 +1,11 @@
+var thisIsCrazy = true;
+
+function callMeMaybe () {
+ return "here's my number";
+}
+
+var iJustMetYou = callMeMaybe();
+
+function duplicate () {
+ return true;
+}
diff --git a/packages/symbols-view/spec/fixtures/providers/async-provider.js b/packages/symbols-view/spec/fixtures/providers/async-provider.js
new file mode 100644
index 000000000..643cc5dfe
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/async-provider.js
@@ -0,0 +1,60 @@
+const { Point } = require('atom');
+// const path = require('path');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+async function wait (ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+const ICONS = [
+ 'icon-package',
+ 'icon-key',
+ 'icon-gear',
+ 'icon-tag',
+ null
+];
+
+module.exports = {
+ packageName: 'symbol-provider-dummy-async',
+ name: 'Dummy (Async)',
+ isExclusive: true,
+ canProvideSymbols (_meta) {
+ return true;
+ },
+ async getSymbols (meta, listController) {
+ let { editor, type } = meta;
+ let results = [];
+ if (type === 'file') {
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ icon: ICONS[(i / 3) % (ICONS.length + 1)]
+ });
+ }
+ } else if (type === 'project') {
+ let root = last(atom.project.getPaths());
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ directory: root,
+ file: 'other-file.js',
+ icon: ICONS[i % (ICONS.length + 1)]
+ });
+ }
+ }
+ await wait(100);
+ listController.set({ loadingMessage: 'Loadingβ¦' });
+ await wait(250);
+ listController.clear('loadingMessage');
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/cache-clearing-provider.js b/packages/symbols-view/spec/fixtures/providers/cache-clearing-provider.js
new file mode 100644
index 000000000..f73fd04c5
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/cache-clearing-provider.js
@@ -0,0 +1,45 @@
+const { Emitter, Point } = require('atom');
+// const path = require('path');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+module.exports = {
+ packageName: 'symbol-provider-cache-clearing',
+ name: 'Cache-clearing',
+ isExclusive: false,
+ canProvideSymbols (_meta) {
+ return true;
+ },
+ onShouldClearCache (callback) {
+ this.emitter ??= new Emitter;
+ return this.emitter.on('should-clear-cache', callback);
+ },
+ getSymbols (meta) {
+ let { editor, type } = meta;
+ let results = [];
+ setTimeout(() => {
+ this.emitter.emit('should-clear-cache', { editor });
+ }, 0);
+ if (type === 'file') {
+ results = [{
+ position: new Point(1, 0),
+ name: 'Transient symbol on row 2'
+ }];
+ } else if (type === 'project') {
+ let root = last(atom.project.getPaths());
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ directory: root,
+ file: 'other-file.js'
+ });
+ }
+ }
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/competing-exclusive-provider.js b/packages/symbols-view/spec/fixtures/providers/competing-exclusive-provider.js
new file mode 100644
index 000000000..d54e49293
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/competing-exclusive-provider.js
@@ -0,0 +1,41 @@
+const { Point } = require('atom');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+module.exports = {
+ packageName: 'symbol-provider-competing-exclusive',
+ name: 'Competing Exclusive',
+ isExclusive: true,
+ canProvideSymbols () {
+ return 0.9;
+ },
+ getSymbols (meta) {
+ let { editor, type } = meta;
+ let results = [];
+ if (type === 'file') {
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`
+ });
+ }
+ } else if (type === 'project') {
+ let root = last(atom.project.getPaths());
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ directory: root,
+ file: 'other-file.js'
+ });
+ }
+ }
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/dummy-provider.js b/packages/symbols-view/spec/fixtures/providers/dummy-provider.js
new file mode 100644
index 000000000..844fcb10a
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/dummy-provider.js
@@ -0,0 +1,51 @@
+const { Point } = require('atom');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+const ICONS = [
+ 'icon-package',
+ 'icon-key',
+ 'icon-gear',
+ 'icon-tag',
+ null
+];
+
+module.exports = {
+ packageName: 'symbol-provider-dummy',
+ name: 'Dummy',
+ isExclusive: true,
+ canProvideSymbols () {
+ return true;
+ },
+ getSymbols (meta) {
+ let { editor, type } = meta;
+ let results = [];
+ if (type === 'file') {
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ icon: ICONS[(i / 3) % (ICONS.length + 1)]
+ });
+ }
+ } else if (type === 'project') {
+ let root = last(atom.project.getPaths());
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ directory: root,
+ file: 'other-file.js',
+ icon: ICONS[i % (ICONS.length + 1)]
+ });
+ }
+ }
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/empty-provider.js b/packages/symbols-view/spec/fixtures/providers/empty-provider.js
new file mode 100644
index 000000000..042ab3507
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/empty-provider.js
@@ -0,0 +1,13 @@
+const { Point } = require('atom');
+
+module.exports = {
+ packageName: 'symbol-provider-empty',
+ name: 'Empty',
+ isExclusive: false,
+ canProvideSymbols (meta) {
+ return true;
+ },
+ getSymbols (meta) {
+ return [];
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/progressive-project-provider.js b/packages/symbols-view/spec/fixtures/providers/progressive-project-provider.js
new file mode 100644
index 000000000..4e4ed3cb3
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/progressive-project-provider.js
@@ -0,0 +1,57 @@
+const { Point } = require('atom');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+const SYMBOLS = [
+ {
+ name: 'Lorem ipsum',
+ position: new Point(0, 0),
+ file: 'other-file.js',
+ },
+ {
+ name: 'Loyalty',
+ position: new Point(1, 0),
+ file: 'other-file.js',
+ },
+ {
+ name: 'Lox',
+ position: new Point(2, 0),
+ file: 'other-file.js',
+ },
+ {
+ name: 'Lo mein',
+ position: new Point(3, 0),
+ file: 'other-file.js',
+ }
+];
+
+module.exports = {
+ packageName: 'symbol-provider-progressive',
+ name: 'Progressive Project Provider',
+ isExclusive: true,
+ canProvideSymbols (meta) {
+ return meta.type === 'project';
+ },
+ getSymbols (meta, controller) {
+ let root = last(atom.project.getPaths());
+ let { type, query = '' } = meta;
+ if (type !== 'project') return [];
+
+ // Simulate a provider that requires a minimum character count.
+ if (query.length < 3) {
+ controller.set({ emptyMessage: 'Query must be at least 3 characters long.' });
+ return [];
+ } else {
+ controller.clear('emptyMessage');
+ }
+
+ let results = SYMBOLS.filter(s => {
+ let term = s.name.toLowerCase();
+ return term.startsWith(query);
+ });
+
+ return results.map(r => ({ ...r, directory: root }));
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/quicksort-provider.js b/packages/symbols-view/spec/fixtures/providers/quicksort-provider.js
new file mode 100644
index 000000000..4dc2aad24
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/quicksort-provider.js
@@ -0,0 +1,44 @@
+const { Point } = require('atom');
+const path = require('path');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+module.exports = {
+ packageName: 'symbol-provider-quicksort',
+ name: 'Quicksort',
+ isExclusive: true,
+ canProvideSymbols (meta) {
+ return true;
+ },
+ getSymbols (meta) {
+ let { editor, type } = meta;
+ let results = [];
+ if (type === 'file') {
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ let name = `Symbol on Row ${i + 1}`;
+ if (i === 0) name = 'quicksort';
+ results.push({
+ position: new Point(i, 0),
+ name
+ });
+ }
+ } else if (type === 'project') {
+ let root = last(atom.project.getPaths());
+ let count = editor.getLineCount();
+ // Put a symbol on every third line.
+ for (let i = 0; i < count; i += 3) {
+ results.push({
+ position: new Point(i, 0),
+ name: `Symbol on Row ${i + 1}`,
+ directory: root,
+ file: 'other-file.js'
+ });
+ }
+ }
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/tagged-provider.js b/packages/symbols-view/spec/fixtures/providers/tagged-provider.js
new file mode 100644
index 000000000..74c2daa27
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/tagged-provider.js
@@ -0,0 +1,44 @@
+const { Point } = require('atom');
+const path = require('path');
+
+function last (arr) {
+ return arr[arr.length - 1];
+}
+
+const MOCK_FILE_NAME = 'tagged.js';
+const MOCK_RESULT_COUNT = 1;
+
+module.exports = {
+ // If you change these values, you MUST remember to call `reset` in an
+ // `afterEach` block!
+ mockResultCount: MOCK_RESULT_COUNT,
+ mockFileName: MOCK_FILE_NAME,
+
+ reset () {
+ this.mockFileName = MOCK_FILE_NAME;
+ this.mockResultCount = MOCK_RESULT_COUNT;
+ },
+
+ packageName: 'symbol-provider-tagged',
+ name: 'Tagged',
+ isExclusive: true,
+ canProvideSymbols (meta) {
+ if (!meta.type === 'project') return false;
+ return true;
+ },
+ getSymbols (meta) {
+ let root = last(atom.project.getPaths());
+ let { editor, type } = meta;
+ let results = [];
+ if (!type.includes('project')) return [];
+ for (let i = 0; i < this.mockResultCount; i++) {
+ results.push({
+ directory: root,
+ file: this.mockFileName,
+ position: new Point(2 + i, 10),
+ name: 'callMeMaybe'
+ });
+ }
+ return results;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/useless-provider.js b/packages/symbols-view/spec/fixtures/providers/useless-provider.js
new file mode 100644
index 000000000..4701c7a51
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/useless-provider.js
@@ -0,0 +1,13 @@
+const { Point } = require('atom');
+
+module.exports = {
+ packageName: 'symbol-provider-useless',
+ name: 'Useless',
+ isExclusive: false,
+ canProvideSymbols (meta) {
+ return false;
+ },
+ getSymbols (meta) {
+ return null;
+ }
+};
diff --git a/packages/symbols-view/spec/fixtures/providers/very-slow-provider.js b/packages/symbols-view/spec/fixtures/providers/very-slow-provider.js
new file mode 100644
index 000000000..076f076fd
--- /dev/null
+++ b/packages/symbols-view/spec/fixtures/providers/very-slow-provider.js
@@ -0,0 +1,29 @@
+const { Point } = require('atom');
+
+function wait (ms) {
+ return new Promise(resolve => {
+ setTimeout(resolve, ms);
+ });
+}
+
+module.exports = {
+ packageName: 'symbol-provider-very-slow',
+ name: 'Very Slow',
+ isExclusive: false,
+ canProvideSymbols () {
+ return true;
+ },
+ async getSymbols (meta) {
+ let { signal } = meta;
+ await wait(3000);
+ if (signal.aborted) {
+ return null;
+ }
+ return [
+ {
+ position: new Point(0, 0),
+ name: `Slow Symbol on Row 1`
+ }
+ ];
+ }
+};
diff --git a/packages/symbols-view/spec/symbols-view-spec.js b/packages/symbols-view/spec/symbols-view-spec.js
new file mode 100644
index 000000000..ef73f3b59
--- /dev/null
+++ b/packages/symbols-view/spec/symbols-view-spec.js
@@ -0,0 +1,976 @@
+const path = require('path');
+const etch = require('etch');
+const fs = require('fs-plus');
+const temp = require('temp');
+const SymbolsView = require('../lib/symbols-view');
+
+const { migrateOldConfigIfNeeded } = require('../lib/util');
+
+const DummyProvider = require('./fixtures/providers/dummy-provider');
+const AsyncDummyProvider = require('./fixtures/providers/async-provider');
+const ProgressiveProjectProvider = require('./fixtures/providers/progressive-project-provider.js');
+const QuicksortProvider = require('./fixtures/providers/quicksort-provider.js');
+const VerySlowProvider = require('./fixtures/providers/very-slow-provider');
+const UselessProvider = require('./fixtures/providers/useless-provider.js');
+const EmptyProvider = require('./fixtures/providers/empty-provider.js');
+const TaggedProvider = require('./fixtures/providers/tagged-provider.js');
+const CacheClearingProvider = require('./fixtures/providers/cache-clearing-provider.js');
+const CompetingExclusiveProvider = require('./fixtures/providers/competing-exclusive-provider.js');
+
+const { it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise } = require('./async-spec-helpers');
+
+async function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function getOrScheduleUpdatePromise() {
+ return new Promise((resolve) => etch.getScheduler().updateDocument(resolve));
+}
+
+function choiceCount(symbolsView) {
+ return symbolsView.element.querySelectorAll('li').length;
+}
+
+function getWorkspaceView() {
+ return atom.views.getView(atom.workspace);
+}
+
+function getEditor() {
+ return atom.workspace.getActiveTextEditor();
+}
+
+function getEditorView() {
+ return atom.views.getView(atom.workspace.getActiveTextEditor());
+}
+
+function getSymbolsView() {
+ return atom.workspace.getModalPanels()[0]?.item;
+}
+
+async function dispatchAndWaitForChoices(commandName) {
+ atom.commands.dispatch(getEditorView(), commandName);
+ let symbolsView = atom.workspace.getModalPanels()[0].item;
+ await conditionPromise(() => {
+ let count = symbolsView.element.querySelectorAll('li').length;
+ return count > 0;
+ });
+}
+
+function registerProvider(...args) {
+ let pkg = atom.packages.getActivePackage('symbols-view');
+ let main = pkg?.mainModule;
+ if (!main) {
+ let disposable = atom.packages.onDidActivatePackage(pack => {
+ if (pack.name !== 'symbols-view') return;
+ for (let provider of args) {
+ pack.mainModule.consumeSymbolProvider(provider);
+ }
+ disposable.dispose();
+ });
+ // If we let the package lazy-activate the first time a command is invoked,
+ // we lose an opportunity to add mock providers. So we should activate it
+ // manually.
+ atom.packages.getLoadedPackage('symbols-view').activateNow();
+ } else {
+ for (let provider of args) {
+ main.consumeSymbolProvider(provider);
+ }
+ }
+}
+
+describe('SymbolsView', () => {
+ let symbolsView, activationPromise, editor, directory, mainModule, languageMode;
+
+ beforeEach(async () => {
+ jasmine.unspy(Date, 'now');
+ jasmine.unspy(global, 'setTimeout');
+
+ atom.project.setPaths([
+ temp.mkdirSync('other-dir-'),
+ temp.mkdirSync('atom-symbols-view-')
+ ]);
+
+ directory = atom.project.getDirectories()[1];
+
+ fs.copySync(
+ path.join(__dirname, 'fixtures', 'js'),
+ atom.project.getPaths()[1]
+ );
+
+ atom.config.set('symbols-view.showProviderNamesInSymbolsView', false);
+ atom.config.set('symbols-view.showIconsInSymbolsView', false);
+
+ activationPromise = atom.packages.activatePackage('symbols-view');
+ await activationPromise.then(() => {
+ mainModule = atom.packages.getActivePackage('symbols-view').mainModule;
+ });
+ await atom.packages.activatePackage('language-javascript');
+ jasmine.attachToDOM(getWorkspaceView());
+ });
+
+ afterEach(async () => {
+ await atom.packages.deactivatePackage('symbols-view');
+ });
+
+ describe('when toggling file symbols', () => {
+ beforeEach(async () => {
+ atom.config.set('symbols-view.providerTimeout', 500);
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ languageMode = editor.getBuffer().getLanguageMode();
+ if (languageMode.ready) await languageMode.ready;
+ });
+
+ it('displays all symbols with line numbers', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText('Line 1');
+ expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText('Line 13');
+
+ // No icon-related classes should be added when `showIconsInSymbolsView`
+ // is false.
+ expect(symbolsView.element.querySelector('li:first-child .primary-line').classList.contains('icon')).toBe(false);
+ expect(symbolsView.element.querySelector('li:first-child .primary-line').classList.contains('no-icon')).toBe(false);
+
+ });
+
+ it('prefills the query field if `prefillSelectedText` is `true`', async () => {
+ atom.config.set('symbols-view.prefillSelectedText', true);
+ registerProvider(DummyProvider);
+ await activationPromise;
+ spyOn(editor, 'getSelectedText').andReturn('Symbol on Row 13');
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(1);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText('Line 13');
+
+ // We reach inside of the `SelectListView` instance to its `TextEditor`
+ // so that we can assert that the text in the query field is selected.
+ // This allows the user to start typing and replace the prefilled
+ // selection if they didn't mean to prefill the query.
+ expect(symbolsView.selectListView.refs.queryEditor.getSelectedText()).toBe('Symbol on Row 13');
+ });
+
+ it('does not prefill the query field if `prefillSelectedText` is `false`', async () => {
+ atom.config.set('symbols-view.prefillSelectedText', false);
+ registerProvider(DummyProvider);
+ await activationPromise;
+ spyOn(editor, 'getSelectedText').andReturn('Symbol on Row 13');
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText('Line 1');
+ expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText('Line 13');
+ });
+
+ it('does not wait for providers that take too long', async () => {
+ registerProvider(DummyProvider, VerySlowProvider);
+ await activationPromise;
+ expect(mainModule.broker.providers.length).toBe(2);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-file-symbols');
+
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+ await conditionPromise(async () => {
+ await getOrScheduleUpdatePromise();
+ let count = symbolsView.element.querySelectorAll('li').length;
+ return count > 0;
+ });
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText('Line 1');
+ expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText('Line 13');
+ });
+
+ it('allows the exclusive provider to control certain UI aspects', async () => {
+ registerProvider(AsyncDummyProvider);
+ await activationPromise;
+ expect(mainModule.broker.providers.length).toBe(1);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ spyOn(symbolsView.selectListView, 'update').andCallThrough();
+ await conditionPromise(async () => {
+ await getOrScheduleUpdatePromise();
+ let count = symbolsView.element.querySelectorAll('li').length;
+ return count > 0;
+ });
+
+ expect(symbolsView.selectListView.update).toHaveBeenCalledWith(
+ { loadingMessage: 'Loadingβ¦' }
+ );
+ });
+
+ it('caches tags until the editor changes', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+ await symbolsView.cancel();
+
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).not.toHaveBeenCalled();
+ await symbolsView.cancel();
+
+ await editor.save();
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).toHaveBeenCalled();
+ editor.destroy();
+ expect(symbolsView.cachedResults.get(editor)).toBeUndefined();
+ });
+
+ it("invalidates a single provider's tags if the provider asks it to", async () => {
+ registerProvider(DummyProvider, CacheClearingProvider);
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+ expect(choiceCount(symbolsView)).toBe(6);
+ await symbolsView.cancel();
+ await wait(100);
+
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+ spyOn(CacheClearingProvider, 'getSymbols').andCallThrough();
+
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ expect(choiceCount(symbolsView)).toBe(6);
+ expect(DummyProvider.getSymbols).not.toHaveBeenCalled();
+ expect(CacheClearingProvider.getSymbols).toHaveBeenCalled();
+ await symbolsView.cancel();
+ await editor.save();
+
+ expect(symbolsView.cachedResults.get(editor)).toBeUndefined();
+
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(choiceCount(symbolsView)).toBe(6);
+ expect(DummyProvider.getSymbols).toHaveBeenCalled();
+ expect(CacheClearingProvider.getSymbols).toHaveBeenCalled();
+ editor.destroy();
+ expect(symbolsView.cachedResults.get(editor)).toBeUndefined();
+ });
+
+ it('displays a message when no tags match text in mini-editor', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+
+ symbolsView = getSymbolsView();
+ symbolsView.selectListView.refs.queryEditor.setText('nothing will match this');
+
+ await conditionPromise(() => symbolsView.selectListView.refs.emptyMessage);
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(choiceCount(symbolsView)).toBe(0);
+
+ expect(symbolsView.selectListView.refs.emptyMessage.textContent.length).toBeGreaterThan(0);
+
+ symbolsView.selectListView.refs.queryEditor.setText('');
+ await conditionPromise(() => choiceCount(symbolsView) > 0);
+ expect( choiceCount(symbolsView) ).toBe(5);
+ expect(symbolsView.selectListView.refs.emptyMessage).toBeUndefined();
+ });
+
+ it('moves the cursor to the selected function', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ symbolsView.element.querySelectorAll('li')[1].click();
+ // It'll move to the first non-whitespace character on the line.
+ expect(editor.getCursorBufferPosition()).toEqual([3, 4]);
+ });
+
+ describe('when there are multiple exclusive providers', () => {
+ describe("and none have priority in the user's settings", () => {
+ it('prefers the one with the highest score', async () => {
+ registerProvider(DummyProvider, CompetingExclusiveProvider);
+ spyOn(CompetingExclusiveProvider, 'getSymbols').andCallThrough();
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).toHaveBeenCalled();
+ expect(CompetingExclusiveProvider.getSymbols).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('and one is listed in `preferCertainProviders`', () => {
+ beforeEach(() => {
+ atom.config.set('symbols-view.preferCertainProviders', ['symbol-provider-competing-exclusive']);
+ });
+
+ it('prefers the one with the highest score (providers listed beating those not listed)', async () => {
+ registerProvider(DummyProvider, CompetingExclusiveProvider);
+ spyOn(CompetingExclusiveProvider, 'getSymbols').andCallThrough();
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).not.toHaveBeenCalled();
+ expect(CompetingExclusiveProvider.getSymbols).toHaveBeenCalled();
+ });
+ });
+
+ describe('and more than one is listed in `preferCertainProviders`', () => {
+ beforeEach(() => {
+ // Last time we referred to this one by its package name; now we use
+ // its human-friendly name. They should be interchangeable.
+ atom.config.set(
+ 'symbols-view.preferCertainProviders',
+ ['Competing Exclusive', 'symbol-provider-dummy']
+ );
+ });
+
+ it('prefers the one with the highest score (providers listed earlier beating those listed later)', async () => {
+ registerProvider(DummyProvider, CompetingExclusiveProvider);
+ spyOn(CompetingExclusiveProvider, 'getSymbols').andCallThrough();
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).not.toHaveBeenCalled();
+ expect(CompetingExclusiveProvider.getSymbols).toHaveBeenCalled();
+ });
+ });
+
+ describe('and one has a scope-specific `preferCertainProviders` setting', () => {
+ beforeEach(() => {
+ // Last time we referred to this one by its package name; now we use
+ // its human-friendly name. They should be interchangeable.
+ atom.config.set(
+ 'symbols-view.preferCertainProviders',
+ ['Competing Exclusive', 'symbol-provider-dummy'],
+ { scopeSelector: '.source.js' }
+ );
+
+ atom.config.set(
+ 'symbols-view.preferCertainProviders',
+ ['symbol-provider-dummy']
+ );
+ });
+
+ it('prefers the one with the highest score (providers listed earlier beating those listed later)', async () => {
+ registerProvider(DummyProvider, CompetingExclusiveProvider);
+ spyOn(CompetingExclusiveProvider, 'getSymbols').andCallThrough();
+ spyOn(DummyProvider, 'getSymbols').andCallThrough();
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ expect(choiceCount(symbolsView)).toBe(5);
+ expect(DummyProvider.getSymbols).not.toHaveBeenCalled();
+ expect(CompetingExclusiveProvider.getSymbols).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when no symbols are found', () => {
+ it('shows the list view with an error message', async () => {
+ registerProvider(EmptyProvider);
+ await activationPromise;
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-file-symbols');
+ await conditionPromise(() => getSymbolsView()?.selectListView.refs.emptyMessage);
+ symbolsView = getSymbolsView();
+
+ expect(document.body.contains(symbolsView.element));
+ expect(choiceCount(symbolsView)).toBe(0);
+ let refs = symbolsView.selectListView.refs;
+ expect(refs.emptyMessage).toBeVisible();
+ expect(refs.emptyMessage.textContent.length).toBeGreaterThan(0);
+ expect(refs.loadingMessage).not.toBeVisible();
+ });
+ });
+
+ describe("when symbols can't be generated for a file", () => {
+ it('does not show the list view', async () => {
+ registerProvider(UselessProvider);
+ await activationPromise;
+ expect(mainModule.broker.providers.length).toBe(1);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-file-symbols');
+
+ await wait(1000);
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+
+ // List view should not be visible, nor should it have any options.
+ expect(
+ symbolsView.element.querySelectorAll('li').length
+ ).toBe(0);
+ expect(symbolsView.element).not.toBeVisible();
+ });
+ });
+
+ describe("when the user has enabled icons in the symbols list", () => {
+ beforeEach(() => {
+ atom.config.set('symbols-view.showIconsInSymbolsView', true);
+ });
+
+ it('shows icons in the symbols list', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line').classList.contains('icon-package')).toBe(true);
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line').classList.contains('no-icon')).toBe(true);
+
+ expect(symbolsView.element.querySelector('li:nth-child(2) .primary-line').classList.contains('icon-key')).toBe(true);
+ expect(symbolsView.element.querySelector('li:nth-child(3) .primary-line').classList.contains('icon-gear')).toBe(true);
+ expect(symbolsView.element.querySelector('li:nth-child(4) .primary-line').classList.contains('icon-tag')).toBe(true);
+
+ // Simulate lack of icon on a random element.
+ expect(symbolsView.element.querySelector('li:nth-child(5) .primary-line').classList.contains('no-icon')).toBe(true);
+ });
+ });
+ });
+
+ describe('when going to declaration', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ });
+
+ describe('when no declaration is found', () => {
+ beforeEach(async () => {
+ registerProvider(EmptyProvider);
+ editor = atom.workspace.getActiveTextEditor();
+ });
+
+ it("doesn't move the cursor", async () => {
+ await activationPromise;
+ editor.setCursorBufferPosition([0, 2]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-project-symbols');
+ await wait(100);
+
+ expect(editor.getCursorBufferPosition()).toEqual([0, 2]);
+ });
+ });
+
+ describe('when there is a single matching declaration', () => {
+ beforeEach(async () => {
+ registerProvider(TaggedProvider);
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ });
+
+ it('moves the cursor to the declaration', async () => {
+ editor.setCursorBufferPosition([6, 24]);
+ spyOn(SymbolsView.prototype, 'moveToPosition').andCallThrough();
+
+ atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration');
+
+ await conditionPromise(() => {
+ return SymbolsView.prototype.moveToPosition.callCount === 1;
+ });
+ expect(editor.getCursorBufferPosition()).toEqual([2, 0]);
+ });
+ });
+
+ describe('when there is more than one matching declaration', () => {
+ beforeEach(async () => {
+ registerProvider(TaggedProvider);
+ TaggedProvider.mockResultCount = 2;
+ TaggedProvider.mockFileName = 'other-file.js';
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ await activationPromise;
+ });
+
+ afterEach(() => {
+ TaggedProvider.reset();
+ });
+
+ it('displays matches and opens the selected match', async () => {
+ editor.setCursorBufferPosition([8, 14]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration');
+ symbolsView = getSymbolsView();
+
+ await conditionPromise(() => {
+ return symbolsView.element.querySelectorAll('li').length > 0;
+ });
+
+ expect(choiceCount(symbolsView)).toBe(2);
+ expect(symbolsView.element).toBeVisible();
+ spyOn(SymbolsView.prototype, 'moveToPosition').andCallThrough();
+ symbolsView.selectListView.confirmSelection();
+
+ await conditionPromise(() => {
+ return SymbolsView.prototype.moveToPosition.callCount === 1;
+ });
+
+ editor = atom.workspace.getActiveTextEditor();
+
+ expect(
+ atom.workspace.getActiveTextEditor().getPath()
+ ).toBe(directory.resolve('other-file.js'));
+
+ expect(
+ atom.workspace.getActiveTextEditor().getCursorBufferPosition()
+ ).toEqual([2, 0]);
+ });
+ });
+ });
+
+ describe('when returning from declaration', () => {
+ describe('in the same file', () => {
+ beforeEach(async () => {
+ registerProvider(TaggedProvider);
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ });
+
+ it("doesn't do anything when no go-tos have been triggered", async () => {
+ editor.setCursorBufferPosition([6, 0]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration');
+
+ expect(editor.getCursorBufferPosition()).toEqual([6, 0]);
+ });
+
+ it('returns to the previous row and column', async () => {
+ editor.setCursorBufferPosition([6, 24]);
+ editor = atom.workspace.getActiveTextEditor();
+ spyOn(SymbolsView.prototype, 'moveToPosition').andCallThrough();
+ atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration');
+
+ await conditionPromise(() => {
+ return SymbolsView.prototype.moveToPosition.callCount === 1;
+ });
+
+ expect(getEditor()).toBe(editor);
+
+ expect(getEditor().getCursorBufferPosition()).toEqual([2, 0]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration');
+
+ await conditionPromise(() => SymbolsView.prototype.moveToPosition.callCount === 2);
+ expect(getEditor().getCursorBufferPosition()).toEqual([6, 24]);
+ });
+ });
+
+ describe('in a different file', () => {
+ beforeEach(async () => {
+ registerProvider(TaggedProvider);
+ await atom.workspace.open(directory.resolve('sample.js'));
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ });
+
+ it("doesn't do anything when no go-tos have been triggered", async () => {
+ editor.setCursorBufferPosition([6, 0]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration');
+
+ expect(editor.getCursorBufferPosition()).toEqual([6, 0]);
+ });
+
+ it('returns to the previous row and column', async () => {
+ editor.setCursorBufferPosition([6, 24]);
+ editor = atom.workspace.getActiveTextEditor();
+ spyOn(SymbolsView.prototype, 'moveToPosition').andCallThrough();
+ atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration');
+
+ await conditionPromise(() => {
+ return SymbolsView.prototype.moveToPosition.callCount === 1;
+ });
+
+ expect(getEditor()).not.toBe(editor);
+
+ expect(getEditor().getCursorBufferPosition()).toEqual([2, 0]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration');
+
+ await conditionPromise(() => SymbolsView.prototype.moveToPosition.callCount === 2);
+
+ expect(getEditor()).toBe(editor);
+ expect(getEditor().getCursorBufferPosition()).toEqual([6, 24]);
+ });
+
+ it('returns to a different file when the file was already open', async () => {
+ editor.setCursorBufferPosition([6, 24]);
+ editor = atom.workspace.getActiveTextEditor();
+ spyOn(SymbolsView.prototype, 'moveToPosition').andCallThrough();
+ atom.commands.dispatch(getEditorView(), 'symbols-view:go-to-declaration');
+
+ await conditionPromise(() => {
+ return SymbolsView.prototype.moveToPosition.callCount === 1;
+ });
+
+ expect(getEditor()).not.toBe(editor);
+ let editorPath = editor.getPath();
+ let editorId = editor.id;
+ atom.workspace.getActivePane().destroyItem(editor);
+
+ expect(getEditor().getCursorBufferPosition()).toEqual([2, 0]);
+ atom.commands.dispatch(getEditorView(), 'symbols-view:return-from-declaration');
+
+ await conditionPromise(() => SymbolsView.prototype.moveToPosition.callCount === 2);
+
+ // Make sure this is a different instance of TextEditor for the same
+ // path.
+ expect(getEditor().getPath()).toBe(editorPath);
+ expect(getEditor().id).not.toBe(editorId);
+ expect(getEditor().getCursorBufferPosition()).toEqual([6, 24]);
+ });
+ });
+ });
+
+ describe('when toggling project symbols', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ });
+
+ it('displays all symbols', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols');
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ let root = atom.project.getPaths()[1];
+ let resolved = directory.resolve('other-file.js');
+ let relative = `${path.basename(root)}${resolved.replace(root, '')}`;
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText(`${relative}:1`);
+ expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText(`${relative}:13`);
+ });
+
+ it('prefills the query field if `prefillSelectedText` is `true`', async () => {
+ atom.config.set('symbols-view.prefillSelectedText', true);
+ registerProvider(DummyProvider);
+ await activationPromise;
+ spyOn(editor, 'getSelectedText').andReturn('Symbol on Row 13');
+ await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(1);
+
+ let root = atom.project.getPaths()[1];
+ let resolved = directory.resolve('other-file.js');
+ let relative = `${path.basename(root)}${resolved.replace(root, '')}`;
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText(`${relative}:13`);
+ });
+
+ it('does not prefill the query field if `prefillSelectedText` is `false`', async () => {
+ atom.config.set('symbols-view.prefillSelectedText', false);
+ registerProvider(DummyProvider);
+ await activationPromise;
+ spyOn(editor, 'getSelectedText').andReturn('Symbol on Row 13');
+ await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols');
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+
+ expect(symbolsView.selectListView.refs.loadingMessage).toBeUndefined();
+ expect(document.body.contains(symbolsView.element)).toBe(true);
+ expect(symbolsView.element.querySelectorAll('li').length).toBe(5);
+
+ let root = atom.project.getPaths()[1];
+ let resolved = directory.resolve('other-file.js');
+ let relative = `${path.basename(root)}${resolved.replace(root, '')}`;
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Symbol on Row 1');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText(`${relative}:1`);
+ expect(symbolsView.element.querySelector('li:last-child .primary-line')).toHaveText('Symbol on Row 13');
+ expect(symbolsView.element.querySelector('li:last-child .secondary-line')).toHaveText(`${relative}:13`);
+ });
+
+ it('asks for new symbols when the user starts typing', async () => {
+ registerProvider(ProgressiveProjectProvider);
+ spyOn(ProgressiveProjectProvider, 'getSymbols').andCallThrough();
+ await activationPromise;
+ atom.commands.dispatch(getEditorView(), 'symbols-view:toggle-project-symbols');
+ symbolsView = atom.workspace.getModalPanels()[0].item;
+ await wait(2000);
+
+ expect(symbolsView.element.querySelectorAll('li .primary-line').length).toBe(0);
+ expect(ProgressiveProjectProvider.getSymbols.callCount).toBe(1);
+
+ expect(symbolsView.selectListView.props.emptyMessage).toBe('Query must be at least 3 characters long.');
+
+ await symbolsView.updateView({ query: 'lor' });
+ await wait(2000);
+
+ expect(symbolsView.selectListView.props.emptyMessage).toBeNull();
+
+ expect(symbolsView.element.querySelectorAll('li .primary-line').length).toBe(1);
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('Lorem ipsum');
+ expect(ProgressiveProjectProvider.getSymbols.callCount).toBe(2);
+ });
+
+ describe('when there is only one project', () => {
+ beforeEach(() => {
+ atom.project.setPaths([directory.getPath()]);
+ });
+
+ it("does not include the root directory's name when displaying the symbol's filename", async () => {
+ registerProvider(TaggedProvider);
+ await atom.workspace.open(directory.resolve('tagged.js'));
+ await activationPromise;
+ expect(getWorkspaceView().querySelector('.symbols-view')).toBeNull();
+ await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols');
+ symbolsView = getSymbolsView();
+
+ expect(choiceCount(symbolsView)).toBe(1);
+
+ expect(symbolsView.element.querySelector('li:first-child .primary-line')).toHaveText('callMeMaybe');
+ expect(symbolsView.element.querySelector('li:first-child .secondary-line')).toHaveText('tagged.js:3');
+ });
+ });
+
+ describe('when selecting a tag', () => {
+ describe("when the file doesn't exist", () => {
+ beforeEach(async () => fs.removeSync(directory.resolve('tagged.js')));
+
+ it("doesn't open the editor", async () => {
+ registerProvider(TaggedProvider);
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-project-symbols');
+ symbolsView = getSymbolsView();
+
+ spyOn(atom.workspace, 'open').andCallThrough();
+
+ symbolsView.element.querySelector('li:first-child').click();
+
+ await conditionPromise(() => symbolsView.selectListView.refs.errorMessage);
+
+ expect(atom.workspace.open).not.toHaveBeenCalled();
+ expect(
+ symbolsView.selectListView.refs.errorMessage.textContent.length
+ ).toBeGreaterThan(0);
+ });
+ });
+ });
+
+ describe('match highlighting', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ registerProvider(QuicksortProvider);
+ });
+
+ it('highlights an exact match', async () => {
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+
+ symbolsView = getSymbolsView();
+ symbolsView.selectListView.refs.queryEditor.setText('quicksort');
+ await getOrScheduleUpdatePromise();
+ let resultView = symbolsView.element.querySelector('.selected');
+ let matches = resultView.querySelectorAll('.character-match');
+ expect(matches.length).toBe(1);
+ expect(matches[0].textContent).toBe('quicksort');
+ });
+
+ it('highlights a partial match', async () => {
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ symbolsView.selectListView.refs.queryEditor.setText('quick');
+ await getOrScheduleUpdatePromise();
+
+ let resultView = symbolsView.element.querySelector('.selected');
+ let matches = resultView.querySelectorAll('.character-match');
+ expect(matches.length).toBe(1);
+ expect(matches[0].textContent).toBe('quick');
+ });
+
+ it('highlights multiple matches in the symbol name', async () => {
+ await activationPromise;
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ symbolsView.selectListView.refs.queryEditor.setText('quicort');
+ await getOrScheduleUpdatePromise();
+
+ let resultView = symbolsView.element.querySelector('.selected');
+ let matches = resultView.querySelectorAll('.character-match');
+ expect(matches.length).toBe(2);
+ expect(matches[0].textContent).toBe('quic');
+ expect(matches[1].textContent).toBe('ort');
+ });
+ });
+
+ describe('when quickJumpToSymbol is true', () => {
+ beforeEach(async () => {
+ await atom.workspace.open(directory.resolve('sample.js'));
+ editor = atom.workspace.getActiveTextEditor();
+ languageMode = editor.getBuffer().getLanguageMode();
+ if (languageMode.ready) await languageMode.ready;
+ });
+
+ it('jumps to the selected function', async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ symbolsView.selectListView.selectNext();
+
+ expect(editor.getCursorBufferPosition()).toEqual([3, 4]);
+ });
+
+ // NOTE: If this test fails, could it have been because you opened the
+ // dev tools console? That seems to break it on a reliable basis. Not
+ // sure why yet.
+ it('restores previous editor state on cancel', async () => {
+ atom.config.set('symbols-view.prefillSelectedText', false);
+ registerProvider(DummyProvider);
+ await activationPromise;
+ const bufferRanges = [{start: {row: 0, column: 0}, end: {row: 0, column: 3}}];
+ editor = atom.workspace.getActiveTextEditor();
+ editor.setSelectedBufferRanges(bufferRanges);
+
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+
+ symbolsView.selectListView.selectNext();
+ expect(editor.getCursorBufferPosition()).toEqual([3, 4]);
+
+ await symbolsView.cancel();
+ expect(editor.getSelectedBufferRanges()).toEqual(bufferRanges);
+ });
+ });
+
+ describe('when quickJumpToSymbol is false', () => {
+ beforeEach(async () => {
+ atom.config.set('symbols-view.quickJumpToFileSymbol', false);
+ await atom.workspace.open(directory.resolve('sample.js'));
+ });
+
+ it("won't jump to the selected function", async () => {
+ registerProvider(DummyProvider);
+ await activationPromise;
+ editor = atom.workspace.getActiveTextEditor();
+ expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
+
+ await dispatchAndWaitForChoices('symbols-view:toggle-file-symbols');
+ symbolsView = getSymbolsView();
+ symbolsView.selectListView.selectNext();
+ expect(editor.getCursorBufferPosition()).toEqual([0, 0]);
+ });
+ });
+ });
+
+ describe('when migrating legacy setting', () => {
+ beforeEach(async () => {
+ atom.config.set(
+ 'symbols-view.useEditorGrammarAsCtagsLanguage',
+ false,
+ { source: atom.config.getUserConfigPath() }
+ );
+ spyOn(atom.notifications, 'addInfo');
+ });
+
+ afterEach(async () => {
+ atom.config.unset(
+ 'symbols-view.useEditorGrammarAsCtagsLanguage',
+ { source: atom.config.getUserConfigPath() }
+ );
+ });
+
+ it('migrates the setting as expected', () => {
+ expect(
+ atom.config.get('symbols-view.useEditorGrammarAsCtagsLanguage')
+ ).toBe(false);
+
+ migrateOldConfigIfNeeded({ force: true });
+
+ expect(
+ atom.config.get(
+ 'symbols-view.useEditorGrammarAsCtagsLanguage',
+ { source: atom.config.getUserConfigPath() }
+ )
+ ).toBeUndefined();
+
+ expect(
+ atom.config.get(
+ 'symbol-provider-ctags.useEditorGrammarAsCtagsLanguage',
+ { source: atom.config.getUserConfigPath() }
+ )
+ ).toBe(false);
+
+ expect(atom.notifications.addInfo).not.toHaveBeenCalled();
+ });
+
+ describe('and a scope-specific override has been made for that setting', () => {
+ beforeEach(() => {
+ atom.config.set(
+ 'symbols-view.useEditorGrammarAsCtagsLanguage',
+ true,
+ {
+ source: 'baz',
+ scopeSelector: ['.source.ts']
+ }
+ );
+ });
+
+ afterEach(() => {
+ atom.config.unset(
+ 'symbols-view.useEditorGrammarAsCtagsLanguage',
+ {
+ source: 'baz',
+ scopeSelector: ['.source.ts']
+ }
+ );
+ });
+
+ it('alerts the user', () => {
+ migrateOldConfigIfNeeded({ force: true });
+
+ expect(atom.notifications.addInfo).toHaveBeenCalled();
+
+ expect(
+ atom.notifications.addInfo.mostRecentCall?.args[1].description
+ ).toBe("The `symbols-view` package has migrated the setting `symbols-view.useEditorGrammarAsCtagsLanguage` to its new location inside the core package `symbol-provider-ctags`. If you have defined any scope-specific overrides for this setting, youβll need to change those overrides manually.\n\nDetected overrides in the following locations:\n\n* `baz`");
+ });
+ });
+
+ });
+});
diff --git a/packages/symbols-view/styles/symbols-view.less b/packages/symbols-view/styles/symbols-view.less
new file mode 100644
index 000000000..da33cbeea
--- /dev/null
+++ b/packages/symbols-view/styles/symbols-view.less
@@ -0,0 +1,94 @@
+@import "ui-variables";
+
+@wedge: (360 / 16);
+
+.badge-variant(@char, @num) {
+ .symbols-view-badge-variant-@{char} {
+ background-color: spin(@background-color-info, @wedge * @num) !important;
+ }
+}
+
+// Generate 16 different badge colors.
+//
+// We need colors that vary from one another, but they need to harmonize with
+// the user's chosen UI theme. So we'll just use `spin` to move around the
+// color wheel, starting with their UI theme's own badge background color, and
+// varying the hue while keeping the saturation and brightness constant.
+.badge-variant(e('0'), 0);
+.badge-variant(e('1'), 1);
+.badge-variant(e('2'), 2);
+.badge-variant(e('3'), 3);
+.badge-variant(e('4'), 4);
+.badge-variant(e('5'), 5);
+.badge-variant(e('6'), 6);
+.badge-variant(e('7'), 7);
+.badge-variant(e('8'), 8);
+.badge-variant(e('9'), 9);
+.badge-variant(e('a'), 10);
+.badge-variant(e('b'), 11);
+.badge-variant(e('c'), 12);
+.badge-variant(e('d'), 13);
+.badge-variant(e('e'), 14);
+.badge-variant(e('f'), 15);
+
+
+// Highlight matched text
+.symbols-view .list-group .character-match {
+ color: @text-color-highlight;
+ font-weight: bold;
+}
+
+
+atom-panel.modal .symbols-view {
+ .primary-line {
+ display: flex !important;
+ flex-direction: row;
+ align-items: center;
+
+ &.icon::before {
+ margin-left: 0;
+ }
+
+ .name {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: pre;
+ margin-right: auto;
+ }
+
+ .badge-container {
+ display: block;
+ flex-grow: 0;
+ display: flex;
+ flex-direction: row;
+
+ .badge:not(:last-child) {
+ margin-right: 5px;
+ }
+ }
+
+ .badge-symbol-tag {
+ text-transform: capitalize;
+ }
+ }
+
+ .secondary-line {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ align-items: baseline;
+ max-width: 100%;
+
+ .location {
+ display: block;
+ }
+
+ .context {
+ display: block;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ flex-grow: 0;
+ }
+ }
+
+}
diff --git a/packages/welcome/lib/changelog-view.js b/packages/welcome/lib/changelog-view.js
index e6983c403..391335f0f 100644
--- a/packages/welcome/lib/changelog-view.js
+++ b/packages/welcome/lib/changelog-view.js
@@ -50,30 +50,23 @@ export default class ChangeLogView {
Feel free to read our Full Change Log .
- Made the modification of `editor.preferredLineLength` configurable within `wrap-guide` when changing `wrap-guide.columns`
+ Fixed some folds in Ruby like unless
, some blocks, multiline comments, function calls, and different array syntaxes for strings and keywords.
- Fixed Snippets from `language-php` that would lose the `$` character
+ Improved the accuracy of indentation hinting in modern Tree-sitter grammars, especially in multi-cursor scenarios.
- Fixed a condition where an invalid config may crash Pulsar before fully starting up, but not registering that it's crashed
+ Improved the ability of the user to opt into a specific kind of grammar for a specific language.
- Reduced error notifications that may appear from `autocomplete-html` when handling EJS files
+ Changed the behavior of the grammar-selector
package so that it will show the user's preferred grammar for a specific language.
- Fixed macOS binary signing after moving over to GitHub Actions for CI
+ Updated to version 0.20.9
of web-tree-sitter
.
- Updated PPM to a newer `node-gyp`, allowing newer versions of C/C++ compiler toolchains and Python to be used (also dropped support for Python 2.x!)
+ Improved syntax highlighting, indentation, and code folding in various languages, including TypeScript, shell scripts, Ruby, and C/C++.
-
- Fully decaffed the entire PPM codebase
-
-
- Added a new autocomplete API that does not uses prefixes and instead declares the range it'll replace (better LSP support)
-
-
diff --git a/packages/wrap-guide/lib/main.js b/packages/wrap-guide/lib/main.js
index e9380d4eb..7cd7e9150 100644
--- a/packages/wrap-guide/lib/main.js
+++ b/packages/wrap-guide/lib/main.js
@@ -6,20 +6,25 @@ module.exports = {
this.subscriptions = new CompositeDisposable();
this.wrapGuides = new Map();
- this.subscriptions.add(atom.workspace.observeTextEditors(editor => {
- if (this.wrapGuides.has(editor)) { return; }
+ this.when = atom.config.get("wrap-guide.showWrapGuide");
+ this.subscriptions.add(atom.config.onDidChange('wrap-guide.showWrapGuide', (args) => {
+ this.when = args.newValue;
+ atom.workspace.getTextEditors().forEach(async (editor) => {
+ await this.wrapGuides.get(editor).setWhen(this.when);
+ });
+ }));
+ this.subscriptions.add(atom.workspace.observeTextEditors((editor) => {
+ if (this.wrapGuides.has(editor)) return;
const editorElement = atom.views.getView(editor);
- const wrapGuideElement = new WrapGuideElement(editor, editorElement);
+ const wrapGuideElement = new WrapGuideElement(editor, editorElement, this.when);
this.wrapGuides.set(editor, wrapGuideElement);
this.subscriptions.add(editor.onDidDestroy(() => {
this.wrapGuides.get(editor).destroy();
this.wrapGuides.delete(editor);
- })
- );
- })
- );
+ }));
+ }));
},
deactivate() {
diff --git a/packages/wrap-guide/lib/wrap-guide-element.js b/packages/wrap-guide/lib/wrap-guide-element.js
index ab4b6e3e0..a18f8a742 100644
--- a/packages/wrap-guide/lib/wrap-guide-element.js
+++ b/packages/wrap-guide/lib/wrap-guide-element.js
@@ -1,65 +1,103 @@
const {CompositeDisposable} = require('atom');
module.exports = class WrapGuideElement {
- constructor(editor, editorElement) {
+ #_when = null;
+ #_shouldShow = null;
+ constructor(editor, editorElement, when) {
this.editor = editor;
this.editorElement = editorElement;
this.subscriptions = new CompositeDisposable();
this.configSubscriptions = new CompositeDisposable();
+ this.softWrapAPLLsubscriptions = null;
this.element = document.createElement('div');
this.element.setAttribute('is', 'wrap-guide');
this.element.classList.add('wrap-guide-container');
this.attachToLines();
this.handleEvents();
- this.updateGuide();
+ this.setWhen(when);
- this.element.updateGuide = this.updateGuide.bind(this);
+ this.element.updateGuide = (async () => await this.updateGuide()).bind(this);
this.element.getDefaultColumn = this.getDefaultColumn.bind(this);
}
+ get shouldShow() { return this.#_shouldShow; }
+ get when() { return this.#_when; }
+
+ setWhen(when) {
+ if (when == this.when) return;
+ this.#_when = when;
+ this.updateWhen();
+ }
+
+ async updateWhen() {
+ switch (this.when) {
+ case "atPreferredLineLength":
+ this.#_shouldShow = this.editor.isSoftWrapped() && atom.config.get('editor.softWrapAtPreferredLineLength', {scope: this.editor.getRootScopeDescriptor()});
+ break;
+ case "wrapping":
+ this.#_shouldShow = this.editor.isSoftWrapped();
+ break;
+ default: // "always"
+ this.#_shouldShow = true;
+ break;
+ }
+ await this.updateGuide();
+ }
+
attachToLines() {
const scrollView = this.editorElement.querySelector('.scroll-view');
return (scrollView != null ? scrollView.appendChild(this.element) : undefined);
}
handleEvents() {
- const updateGuideCallback = () => this.updateGuide();
+ const updateGuideCallback = async () => await this.updateGuide();
this.handleConfigEvents();
- this.subscriptions.add(atom.config.onDidChange('editor.fontSize', () => {
+ this.subscriptions.add(this.editor.onDidChangeSoftWrapped(async (wrapped) => {
+ if (this.when === null) return;
+ await this.updateWhen();
+ }));
+
+ this.subscriptions.add(atom.config.onDidChange('editor.fontSize', async () => {
// Wait for editor to finish updating before updating wrap guide
- // TODO: Use async/await once this file is converted to JS
- this.editorElement.getComponent().getNextUpdatePromise().then(() => updateGuideCallback());
- })
- );
+ await this.editorElement.getComponent().getNextUpdatePromise();
+ updateGuideCallback();
+ }));
this.subscriptions.add(this.editorElement.onDidChangeScrollLeft(updateGuideCallback));
this.subscriptions.add(this.editor.onDidChangePath(updateGuideCallback));
- this.subscriptions.add(this.editor.onDidChangeGrammar(() => {
+ this.subscriptions.add(this.editor.onDidChangeGrammar(async () => {
this.configSubscriptions.dispose();
this.handleConfigEvents();
- updateGuideCallback();
- })
- );
+ await this.updateWhen();
+ }));
this.subscriptions.add(this.editor.onDidDestroy(() => {
this.subscriptions.dispose();
+ if (this.softWrapAPLLsubscriptions) this.softWrapAPLLsubscriptions.dispose();
this.configSubscriptions.dispose();
- })
- );
+ }));
- this.subscriptions.add(this.editorElement.onDidAttach(() => {
+ this.subscriptions.add(this.editorElement.onDidAttach(async () => {
this.attachToLines();
- updateGuideCallback();
- })
- );
+ await updateGuideCallback();
+ }));
}
handleConfigEvents() {
const {uniqueAscending} = require('./main');
- const updatePreferredLineLengthCallback = args => {
+ if (this.softWrapAPLLsubscriptions) this.softWrapAPLLsubscriptions.dispose();
+ this.softWrapAPLLsubscriptions = new CompositeDisposable();
+
+ this.softWrapAPLLsubscriptions.add(atom.config.onDidChange('editor.softWrapAtPreferredLineLength',
+ {scope: this.editor.getRootScopeDescriptor()}, async ({newValue}) => {
+ if (this.when === null) return;
+ await this.updateWhen();
+ }));
+
+ const updatePreferredLineLengthCallback = async (args) => {
// ensure that the right-most wrap guide is the preferredLineLength
let columns = atom.config.get('wrap-guide.columns', {scope: this.editor.getRootScopeDescriptor()});
if (columns.length > 0) {
@@ -68,24 +106,22 @@ module.exports = class WrapGuideElement {
atom.config.set('wrap-guide.columns', columns,
{scopeSelector: `.${this.editor.getGrammar().scopeName}`});
}
- return this.updateGuide();
+ return await this.updateGuide();
};
this.configSubscriptions.add(atom.config.onDidChange(
'editor.preferredLineLength',
{scope: this.editor.getRootScopeDescriptor()},
updatePreferredLineLengthCallback
- )
- );
+ ));
- const updateGuideCallback = () => this.updateGuide();
+ const updateGuideCallback = async () => await this.updateGuide();
this.configSubscriptions.add(atom.config.onDidChange(
'wrap-guide.enabled',
{scope: this.editor.getRootScopeDescriptor()},
updateGuideCallback
- )
- );
+ ));
- const updateGuidesCallback = args => {
+ const updateGuidesCallback = async (args) => {
// ensure that multiple guides stay sorted in ascending order
const columns = uniqueAscending(args.newValue);
if (columns != null ? columns.length : undefined) {
@@ -94,15 +130,14 @@ module.exports = class WrapGuideElement {
atom.config.set('editor.preferredLineLength', columns[columns.length - 1],
{scopeSelector: `.${this.editor.getGrammar().scopeName}`});
}
- return this.updateGuide();
+ return await this.updateGuide();
}
};
return this.configSubscriptions.add(atom.config.onDidChange(
'wrap-guide.columns',
{scope: this.editor.getRootScopeDescriptor()},
updateGuidesCallback
- )
- );
+ ));
}
getDefaultColumn() {
@@ -130,15 +165,14 @@ module.exports = class WrapGuideElement {
}
updateGuide() {
- if (this.isEnabled()) {
+ if (this.isEnabled())
return this.updateGuides();
- } else {
- return this.hide();
- }
+ else return this.hide();
}
updateGuides() {
this.removeGuides();
+ if (!this.shouldShow) return this.hide();
this.appendGuides();
if (this.element.children.length) {
return this.show();
@@ -150,6 +184,7 @@ module.exports = class WrapGuideElement {
destroy() {
this.element.remove();
this.subscriptions.dispose();
+ if (this.softWrapAPLLsubscriptions) this.softWrapAPLLsubscriptions.dispose();
return this.configSubscriptions.dispose();
}
diff --git a/packages/wrap-guide/package.json b/packages/wrap-guide/package.json
index 5deece076..421df76d5 100644
--- a/packages/wrap-guide/package.json
+++ b/packages/wrap-guide/package.json
@@ -1,6 +1,6 @@
{
"name": "wrap-guide",
- "version": "0.41.0",
+ "version": "0.41.1",
"main": "./lib/main",
"description": "Displays a vertical line at the 80th character in the editor.\nThis packages uses the config value of `editor.preferredLineLength` when set.",
"license": "MIT",
@@ -25,6 +25,25 @@
"enabled": {
"default": true,
"type": "boolean"
+ },
+ "showWrapGuide": {
+ "type": "string",
+ "description": "Choose when to show the wrap guide.",
+ "enum": [
+ {
+ "value": "always",
+ "description": "Always"
+ },
+ {
+ "value": "wrapping",
+ "description": "When soft wrap is enabled"
+ },
+ {
+ "value": "atPreferredLineLength",
+ "description": "When soft wrap at preferred line length is enabled"
+ }
+ ],
+ "default": "always"
}
}
}
diff --git a/packages/wrap-guide/spec/wrap-guide-element-spec.js b/packages/wrap-guide/spec/wrap-guide-element-spec.js
index ce8d342a3..85b877ac5 100644
--- a/packages/wrap-guide/spec/wrap-guide-element-spec.js
+++ b/packages/wrap-guide/spec/wrap-guide-element-spec.js
@@ -10,145 +10,128 @@ const {uniqueAscending} = require('../lib/main');
describe("WrapGuideElement", function() {
let [editor, editorElement, wrapGuide, workspaceElement] = [];
- beforeEach(function() {
- workspaceElement = atom.views.getView(atom.workspace);
- workspaceElement.style.height = "200px";
- workspaceElement.style.width = "1500px";
+ describe("When always shown", function() {
+ beforeEach(function() {
+ atom.config.set('wrap-guide.showWrapGuide', 'always');
+ workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.style.height = "200px";
+ workspaceElement.style.width = "1500px";
- jasmine.attachToDOM(workspaceElement);
+ jasmine.attachToDOM(workspaceElement);
- waitsForPromise(() => atom.packages.activatePackage('wrap-guide'));
+ waitsForPromise(() => atom.packages.activatePackage('wrap-guide'));
- waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
+ waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
- waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'));
+ waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'));
- waitsForPromise(() => atom.workspace.open('sample.js'));
-
- runs(function() {
- editor = atom.workspace.getActiveTextEditor();
- editorElement = editor.getElement();
- wrapGuide = editorElement.querySelector(".wrap-guide-container");
- });
- });
-
- describe(".activate", function() {
- const getWrapGuides = function() {
- const wrapGuides = [];
- atom.workspace.getTextEditors().forEach(function(editor) {
- const guides = editor.getElement().querySelectorAll(".wrap-guide");
- if (guides) { return wrapGuides.push(guides); }
- });
- return wrapGuides;
- };
-
- it("appends a wrap guide to all existing and new editors", function() {
- expect(atom.workspace.getTextEditors().length).toBe(1);
-
- expect(getWrapGuides().length).toBe(1);
- expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0);
-
- atom.workspace.getActivePane().splitRight({copyActiveItem: true});
- expect(atom.workspace.getTextEditors().length).toBe(2);
- expect(getWrapGuides().length).toBe(2);
- expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0);
- expect(getLeftPosition(getWrapGuides()[1][0])).toBeGreaterThan(0);
- });
-
- it("positions the guide at the configured column", function() {
- const width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn();
- expect(width).toBeGreaterThan(0);
- expect(Math.abs(getLeftPosition(wrapGuide.firstChild) - width)).toBeLessThan(1);
- expect(wrapGuide).toBeVisible();
- });
-
- it("appends multiple wrap guides to all existing and new editors", function() {
- const columns = [10, 20, 30];
- atom.config.set("wrap-guide.columns", columns);
-
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ waitsForPromise(() => atom.workspace.open('sample.js'));
runs(function() {
+ editor = atom.workspace.getActiveTextEditor();
+ editorElement = editor.getElement();
+ wrapGuide = editorElement.querySelector(".wrap-guide-container");
+ });
+ });
+
+ describe(".activate", function() {
+ const getWrapGuides = function() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach(function(editor) {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ };
+
+ it("appends a wrap guide to all existing and new editors", function() {
expect(atom.workspace.getTextEditors().length).toBe(1);
+
expect(getWrapGuides().length).toBe(1);
- const positions = getLeftPositions(getWrapGuides()[0]);
- expect(positions.length).toBe(columns.length);
- expect(positions[0]).toBeGreaterThan(0);
- expect(positions[1]).toBeGreaterThan(positions[0]);
- expect(positions[2]).toBeGreaterThan(positions[1]);
+ expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0);
atom.workspace.getActivePane().splitRight({copyActiveItem: true});
expect(atom.workspace.getTextEditors().length).toBe(2);
expect(getWrapGuides().length).toBe(2);
- const pane1_positions = getLeftPositions(getWrapGuides()[0]);
- expect(pane1_positions.length).toBe(columns.length);
- expect(pane1_positions[0]).toBeGreaterThan(0);
- expect(pane1_positions[1]).toBeGreaterThan(pane1_positions[0]);
- expect(pane1_positions[2]).toBeGreaterThan(pane1_positions[1]);
- const pane2_positions = getLeftPositions(getWrapGuides()[1]);
- expect(pane2_positions.length).toBe(pane1_positions.length);
- expect(pane2_positions[0]).toBe(pane1_positions[0]);
- expect(pane2_positions[1]).toBe(pane1_positions[1]);
- expect(pane2_positions[2]).toBe(pane1_positions[2]);
+ expect(getLeftPosition(getWrapGuides()[0][0])).toBeGreaterThan(0);
+ expect(getLeftPosition(getWrapGuides()[1][0])).toBeGreaterThan(0);
});
- });
- it("positions multiple guides at the configured columns", function() {
- // Previously used CoffeeScript below:
- /**
+ it("positions the guide at the configured column", function() {
+ const width = editor.getDefaultCharWidth() * wrapGuide.getDefaultColumn();
+ expect(width).toBeGreaterThan(0);
+ expect(Math.abs(getLeftPosition(wrapGuide.firstChild) - width)).toBeLessThan(1);
+ expect(wrapGuide).toBeVisible();
+ });
+
+ it("appends multiple wrap guides to all existing and new editors", function() {
+ const columns = [10, 20, 30];
+ atom.config.set("wrap-guide.columns", columns);
+
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(function() {
+ expect(atom.workspace.getTextEditors().length).toBe(1);
+ expect(getWrapGuides().length).toBe(1);
+ const positions = getLeftPositions(getWrapGuides()[0]);
+ expect(positions.length).toBe(columns.length);
+ expect(positions[0]).toBeGreaterThan(0);
+ expect(positions[1]).toBeGreaterThan(positions[0]);
+ expect(positions[2]).toBeGreaterThan(positions[1]);
+
+ atom.workspace.getActivePane().splitRight({copyActiveItem: true});
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ expect(getWrapGuides().length).toBe(2);
+ const pane1_positions = getLeftPositions(getWrapGuides()[0]);
+ expect(pane1_positions.length).toBe(columns.length);
+ expect(pane1_positions[0]).toBeGreaterThan(0);
+ expect(pane1_positions[1]).toBeGreaterThan(pane1_positions[0]);
+ expect(pane1_positions[2]).toBeGreaterThan(pane1_positions[1]);
+ const pane2_positions = getLeftPositions(getWrapGuides()[1]);
+ expect(pane2_positions.length).toBe(pane1_positions.length);
+ expect(pane2_positions[0]).toBe(pane1_positions[0]);
+ expect(pane2_positions[1]).toBe(pane1_positions[1]);
+ expect(pane2_positions[2]).toBe(pane1_positions[2]);
+ });
+ });
+
+ it("positions multiple guides at the configured columns", function() {
+ // Previously used CoffeeScript below:
+ /**
* columnCount = 5
* columns = (c * 10 for c in [1..columnCount])
*/
- const columnCount = 5;
- let columns = [];
+ const columnCount = 5;
+ let columns = [];
- for (let i = 0; i < columnCount; i++) {
- columns.push(i * 10);
- }
-
- atom.config.set("wrap-guide.columns", columns);
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
-
- runs(function() {
- const positions = getLeftPositions(getWrapGuides()[0]);
- expect(positions.length).toBe(columnCount);
- expect(wrapGuide.children.length).toBe(columnCount);
-
- for (let i of Array.from(columnCount - 1)) {
- const width = editor.getDefaultCharWidth() * columns[i];
- expect(width).toBeGreaterThan(0);
- expect(Math.abs(getLeftPosition(wrapGuide.children[i]) - width)).toBeLessThan(1);
+ for (let i = 0; i < columnCount; i++) {
+ columns.push(i * 10);
}
- expect(wrapGuide).toBeVisible();
- });
- });
- });
- describe("when the font size changes", function() {
- it("updates the wrap guide position", function() {
- const initial = getLeftPosition(wrapGuide.firstChild);
- expect(initial).toBeGreaterThan(0);
- const fontSize = atom.config.get("editor.fontSize");
- atom.config.set("editor.fontSize", fontSize + 10);
+ atom.config.set("wrap-guide.columns", columns);
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ runs(function() {
+ const positions = getLeftPositions(getWrapGuides()[0]);
+ expect(positions.length).toBe(columnCount);
+ expect(wrapGuide.children.length).toBe(columnCount);
- runs(function() {
- expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
- expect(wrapGuide.firstChild).toBeVisible();
+ for (let i of Array.from(columnCount - 1)) {
+ const width = editor.getDefaultCharWidth() * columns[i];
+ expect(width).toBeGreaterThan(0);
+ expect(Math.abs(getLeftPosition(wrapGuide.children[i]) - width)).toBeLessThan(1);
+ }
+ expect(wrapGuide).toBeVisible();
+ });
});
});
- it("updates the wrap guide position for hidden editors when they become visible", function() {
- const initial = getLeftPosition(wrapGuide.firstChild);
- expect(initial).toBeGreaterThan(0);
-
- waitsForPromise(() => atom.workspace.open());
-
- runs(function() {
+ describe("when the font size changes", function() {
+ it("updates the wrap guide position", function() {
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ expect(initial).toBeGreaterThan(0);
const fontSize = atom.config.get("editor.fontSize");
atom.config.set("editor.fontSize", fontSize + 10);
- atom.workspace.getActivePane().activatePreviousItem();
waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
@@ -157,171 +140,479 @@ describe("WrapGuideElement", function() {
expect(wrapGuide.firstChild).toBeVisible();
});
});
+
+ it("updates the wrap guide position for hidden editors when they become visible", function() {
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ expect(initial).toBeGreaterThan(0);
+
+ waitsForPromise(() => atom.workspace.open());
+
+ runs(function() {
+ const fontSize = atom.config.get("editor.fontSize");
+ atom.config.set("editor.fontSize", fontSize + 10);
+ atom.workspace.getActivePane().activatePreviousItem();
+
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(function() {
+ expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
+ expect(wrapGuide.firstChild).toBeVisible();
+ });
+ });
+ });
});
- });
- describe("when the column config changes", () => it("updates the wrap guide position", function() {
- const initial = getLeftPosition(wrapGuide.firstChild);
- expect(initial).toBeGreaterThan(0);
- const column = atom.config.get("editor.preferredLineLength");
- atom.config.set("editor.preferredLineLength", column + 10);
- expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
- expect(wrapGuide).toBeVisible();
- }));
+ describe("when the column config changes", () => it("updates the wrap guide position", function() {
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ expect(initial).toBeGreaterThan(0);
+ const column = atom.config.get("editor.preferredLineLength");
+ atom.config.set("editor.preferredLineLength", column + 10);
+ expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
+ expect(wrapGuide).toBeVisible();
+ }));
- describe("when the preferredLineLength changes", () => it("updates the wrap guide positions", function() {
- const initial = [10, 15, 20, 30];
- atom.config.set('wrap-guide.columns', initial,
+ describe("when the preferredLineLength changes", () => it("updates the wrap guide positions", function() {
+ const initial = [10, 15, 20, 30];
+ atom.config.set('wrap-guide.columns', initial,
{scopeSelector: `.${editor.getGrammar().scopeName}`});
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
- runs(function() {
- atom.config.set('editor.preferredLineLength', 15,
+ runs(function() {
+ atom.config.set('editor.preferredLineLength', 15,
{scopeSelector: `.${editor.getGrammar().scopeName}`});
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
- runs(function() {
- const columns = atom.config.get('wrap-guide.columns', {scope: editor.getRootScopeDescriptor()});
- expect(columns.length).toBe(2);
- expect(columns[0]).toBe(10);
- expect(columns[1]).toBe(15);
+ runs(function() {
+ const columns = atom.config.get('wrap-guide.columns', {scope: editor.getRootScopeDescriptor()});
+ expect(columns.length).toBe(2);
+ expect(columns[0]).toBe(10);
+ expect(columns[1]).toBe(15);
+ });
+ });
+ }));
+
+ describe("when the columns config changes", function() {
+ it("updates the wrap guide positions", function() {
+ const initial = getLeftPositions(wrapGuide.children);
+ expect(initial.length).toBe(1);
+ expect(initial[0]).toBeGreaterThan(0);
+
+ const columns = [10, 20, 30];
+ atom.config.set("wrap-guide.columns", columns);
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(function() {
+ const positions = getLeftPositions(wrapGuide.children);
+ expect(positions.length).toBe(columns.length);
+ expect(positions[0]).toBeGreaterThan(0);
+ expect(positions[1]).toBeGreaterThan(positions[0]);
+ expect(positions[2]).toBeGreaterThan(positions[1]);
+ expect(wrapGuide).toBeVisible();
+ });
+ });
+
+ it("updates the preferredLineLength", function() {
+ const initial = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
+ atom.config.set("wrap-guide.columns", [initial, initial + 10]);
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(function() {
+ const length = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
+ expect(length).toBe(initial + 10);
+ });
+ });
+
+ it("keeps guide positions unique and in ascending order", function() {
+ const initial = getLeftPositions(wrapGuide.children);
+ expect(initial.length).toBe(1);
+ expect(initial[0]).toBeGreaterThan(0);
+
+ const reverseColumns = [30, 20, 10];
+ const columns = [reverseColumns[reverseColumns.length - 1], ...reverseColumns, reverseColumns[0]];
+ const uniqueColumns = uniqueAscending(columns);
+ expect(uniqueColumns.length).toBe(3);
+ expect(uniqueColumns[0]).toBeGreaterThan(0);
+ expect(uniqueColumns[1]).toBeGreaterThan(uniqueColumns[0]);
+ expect(uniqueColumns[2]).toBeGreaterThan(uniqueColumns[1]);
+
+ atom.config.set("wrap-guide.columns", columns);
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(function() {
+ const positions = getLeftPositions(wrapGuide.children);
+ expect(positions.length).toBe(uniqueColumns.length);
+ expect(positions[0]).toBeGreaterThan(0);
+ expect(positions[1]).toBeGreaterThan(positions[0]);
+ expect(positions[2]).toBeGreaterThan(positions[1]);
+ expect(wrapGuide).toBeVisible();
+ });
+ });
+
+ it("leaves alone preferredLineLength if modifyPreferredLineLength is false", () => {
+ const initial = atom.config.get("editor.preferredLineLength", { scope: editor.getRootScopeDescriptor() });
+ atom.config.set("wrap-guide.modifyPreferredLineLength", false);
+
+ atom.config.set("wrap-guide.columns", [ initial, initial + 10]);
+ waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+
+ runs(() => {
+ const length = atom.config.get("editor.preferredLineLength", { scope: editor.getRootScopeDescriptor() });
+ expect(length).toBe(initial);
+ });
});
});
- }));
- describe("when the columns config changes", function() {
- it("updates the wrap guide positions", function() {
- const initial = getLeftPositions(wrapGuide.children);
- expect(initial.length).toBe(1);
- expect(initial[0]).toBeGreaterThan(0);
+ describe("when the editor's scroll left changes", () => it("updates the wrap guide position to a relative position on screen", function() {
+ editor.setText("a long line which causes the editor to scroll");
+ editorElement.style.width = "100px";
- const columns = [10, 20, 30];
- atom.config.set("wrap-guide.columns", columns);
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ waitsFor(() => editorElement.component.getMaxScrollLeft() > 10);
runs(function() {
- const positions = getLeftPositions(wrapGuide.children);
- expect(positions.length).toBe(columns.length);
- expect(positions[0]).toBeGreaterThan(0);
- expect(positions[1]).toBeGreaterThan(positions[0]);
- expect(positions[2]).toBeGreaterThan(positions[1]);
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ expect(initial).toBeGreaterThan(0);
+ editorElement.setScrollLeft(10);
+ expect(getLeftPosition(wrapGuide.firstChild)).toBe(initial - 10);
+ expect(wrapGuide.firstChild).toBeVisible();
+ });
+ }));
+
+ describe("when the editor's grammar changes", function() {
+ it("updates the wrap guide position", function() {
+ atom.config.set('editor.preferredLineLength', 20, {scopeSelector: '.source.js'});
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ expect(initial).toBeGreaterThan(0);
+ expect(wrapGuide).toBeVisible();
+
+ editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'));
+ expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
expect(wrapGuide).toBeVisible();
});
- });
- it("updates the preferredLineLength", function() {
- const initial = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
- atom.config.set("wrap-guide.columns", [initial, initial + 10]);
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
-
- runs(function() {
- const length = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
- expect(length).toBe(initial + 10);
+ it('listens for preferredLineLength updates for the new grammar', function() {
+ editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'));
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ atom.config.set('editor.preferredLineLength', 20, {scopeSelector: '.source.coffee'});
+ expect(getLeftPosition(wrapGuide.firstChild)).toBeLessThan(initial);
});
- });
- it("keeps guide positions unique and in ascending order", function() {
- const initial = getLeftPositions(wrapGuide.children);
- expect(initial.length).toBe(1);
- expect(initial[0]).toBeGreaterThan(0);
-
- const reverseColumns = [30, 20, 10];
- const columns = [reverseColumns[reverseColumns.length - 1], ...reverseColumns, reverseColumns[0]];
- const uniqueColumns = uniqueAscending(columns);
- expect(uniqueColumns.length).toBe(3);
- expect(uniqueColumns[0]).toBeGreaterThan(0);
- expect(uniqueColumns[1]).toBeGreaterThan(uniqueColumns[0]);
- expect(uniqueColumns[2]).toBeGreaterThan(uniqueColumns[1]);
-
- atom.config.set("wrap-guide.columns", columns);
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
-
- runs(function() {
- const positions = getLeftPositions(wrapGuide.children);
- expect(positions.length).toBe(uniqueColumns.length);
- expect(positions[0]).toBeGreaterThan(0);
- expect(positions[1]).toBeGreaterThan(positions[0]);
- expect(positions[2]).toBeGreaterThan(positions[1]);
+ it('listens for wrap-guide.enabled updates for the new grammar', function() {
+ editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'));
expect(wrapGuide).toBeVisible();
+ atom.config.set('wrap-guide.enabled', false, {scopeSelector: '.source.coffee'});
+ expect(wrapGuide).not.toBeVisible();
});
});
- it("leaves alone preferredLineLength if modifyPreferredLineLength is false", () => {
- const initial = atom.config.get("editor.preferredLineLength", { scope: editor.getRootScopeDescriptor() });
- atom.config.set("wrap-guide.modifyPreferredLineLength", false);
+ describe('scoped config', function() {
+ it('::getDefaultColumn returns the scope-specific column value', function() {
+ atom.config.set('editor.preferredLineLength', 132, {scopeSelector: '.source.js'});
- atom.config.set("wrap-guide.columns", [ initial, initial + 10]);
- waitsForPromise(() => editorElement.getComponent().getNextUpdatePromise());
+ expect(wrapGuide.getDefaultColumn()).toBe(132);
+ });
- runs(() => {
- const length = atom.config.get("editor.preferredLineLength", { scope: editor.getRootScopeDescriptor() });
- expect(length).toBe(initial);
+ it('updates the guide when the scope-specific column changes', function() {
+ const initial = getLeftPosition(wrapGuide.firstChild);
+ const column = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
+ atom.config.set('editor.preferredLineLength', column + 10, {scope: '.source.js'});
+ expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
+ });
+
+ it('updates the guide when wrap-guide.enabled is set to false', function() {
+ expect(wrapGuide).toBeVisible();
+
+ atom.config.set('wrap-guide.enabled', false, {scopeSelector: '.source.js'});
+
+ expect(wrapGuide).not.toBeVisible();
});
});
});
- describe("when the editor's scroll left changes", () => it("updates the wrap guide position to a relative position on screen", function() {
- editor.setText("a long line which causes the editor to scroll");
- editorElement.style.width = "100px";
+ describe("When only shown if wrapping at preferred line length", () => {
+ beforeEach(() => {
+ atom.config.set('wrap-guide.showWrapGuide', 'atPreferredLineLength');
- waitsFor(() => editorElement.component.getMaxScrollLeft() > 10);
+ waitsForPromise(() => atom.packages.activatePackage('wrap-guide'));
- runs(function() {
- const initial = getLeftPosition(wrapGuide.firstChild);
- expect(initial).toBeGreaterThan(0);
- editorElement.setScrollLeft(10);
- expect(getLeftPosition(wrapGuide.firstChild)).toBe(initial - 10);
- expect(wrapGuide.firstChild).toBeVisible();
- });
- }));
+ waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
- describe("when the editor's grammar changes", function() {
- it("updates the wrap guide position", function() {
- atom.config.set('editor.preferredLineLength', 20, {scopeSelector: '.source.js'});
- const initial = getLeftPosition(wrapGuide.firstChild);
- expect(initial).toBeGreaterThan(0);
- expect(wrapGuide).toBeVisible();
+ waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'));
- editor.setGrammar(atom.grammars.grammarForScopeName('text.plain.null-grammar'));
- expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
- expect(wrapGuide).toBeVisible();
+ waitsForPromise(() => atom.workspace.open('sample.txt'));
+ waitsForPromise(() => atom.workspace.open('sample.js'));
});
- it('listens for preferredLineLength updates for the new grammar', function() {
- editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'));
- const initial = getLeftPosition(wrapGuide.firstChild);
- atom.config.set('editor.preferredLineLength', 20, {scopeSelector: '.source.coffee'});
- expect(getLeftPosition(wrapGuide.firstChild)).toBeLessThan(initial);
+ describe("while the wrapping at preferred line length is active", () => {
+ beforeEach(() => {
+ atom.config.set('editor.softWrap', true);
+ atom.config.set('editor.softWrapAtPreferredLineLength', true);
+ workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.style.height = "200px";
+ workspaceElement.style.width = "1500px";
+
+ jasmine.attachToDOM(workspaceElement);
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor();
+ editorElement = editor.getElement();
+ wrapGuide = editorElement.querySelector(".wrap-guide-container");
+ });
+ });
+
+ it("should generate wrap-guides as usual until either wrappings are deactivated", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+ const scopeDescriptor = editor.getRootScopeDescriptor();
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrapAtPreferredLineLength')]).toEqual([true, true]);
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrap', false, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrap', {scope: scopeDescriptor})]).toEqual([true, false]);
+
+ expect(getWrapGuides().length).toBe(1);
+
+ atom.config.set('editor.softWrap', true, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect(atom.config.get('editor.softWrap', {scope: scopeDescriptor})).toBe(true);
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrapAtPreferredLineLength', false, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrapAtPreferredLineLength'),
+ atom.config.get('editor.softWrapAtPreferredLineLength', {scope: scopeDescriptor})]).
+ toEqual([true, false]);
+
+ expect(getWrapGuides().length).toBe(1);
+
+ atom.config.unset('editor.softWrapAtPreferredLineLength', {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect(atom.config.get('editor.softWrapAtPreferredLineLength', {scope: scopeDescriptor})).toBe(true);
+
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrapAtPreferredLineLength', false);
+
+ expect([atom.config.get('editor.softWrapAtPreferredLineLength'),
+ atom.config.get('editor.softWrapAtPreferredLineLength', {scope: scopeDescriptor})]).
+ toEqual([false, false]);
+
+ expect(getWrapGuides().length).toBe(0);
+ });
+
+ it("should adapt when changing the grammar", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+ const scopeDescriptor = editor.getRootScopeDescriptor();
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrapAtPreferredLineLength')]).toEqual([true, true]);
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrapAtPreferredLineLength', false, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrapAtPreferredLineLength'),
+ atom.config.get('editor.softWrapAtPreferredLineLength', {scope: scopeDescriptor})]).toEqual([true, false]);
+
+ expect(getWrapGuides().length).toBe(1);
+
+ editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'));
+ const new_scopeDescriptor = editor.getRootScopeDescriptor();
+
+ expect([scopeDescriptor != new_scopeDescriptor,
+ atom.config.get('editor.softWrapAtPreferredLineLength', {scope: new_scopeDescriptor})]).
+ toEqual([true, true]);
+
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrapAtPreferredLineLength', false, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect(getWrapGuides().length).toBe(1);
+ });
});
- it('listens for wrap-guide.enabled updates for the new grammar', function() {
- editor.setGrammar(atom.grammars.grammarForScopeName('source.coffee'));
- expect(wrapGuide).toBeVisible();
- atom.config.set('wrap-guide.enabled', false, {scopeSelector: '.source.coffee'});
- expect(wrapGuide).not.toBeVisible();
+ describe("while the wrapping is inactive", () => {
+ beforeEach(() => {
+ atom.config.set('editor.softWrap', false);
+ atom.config.set('editor.softWrapAtPreferredLineLength', false);
+ workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.style.height = "200px";
+ workspaceElement.style.width = "1500px";
+
+ jasmine.attachToDOM(workspaceElement);
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor();
+ editorElement = editor.getElement();
+ wrapGuide = editorElement.querySelector(".wrap-guide-container");
+ });
+ });
+
+ it("should not generate wrap-guides until wrapping at preferred line length is reactivated", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+ const scopeDescriptor = editor.getRootScopeDescriptor();
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrapAtPreferredLineLength')]).toEqual([false, false]);
+ expect(getWrapGuides().length).toBe(0);
+
+ atom.config.set('editor.softWrap', true, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrap', {scope: scopeDescriptor})]).toEqual([false, true]);
+
+ expect(getWrapGuides().length).toBe(0);
+
+ atom.config.set('editor.softWrapAtPreferredLineLength', true, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrapAtPreferredLineLength'),
+ atom.config.get('editor.softWrapAtPreferredLineLength', {scope: scopeDescriptor})]).
+ toEqual([false, true]);
+
+ expect(getWrapGuides().length).toBe(1);
+ });
+
+ it("should adapt when changing to another mod", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrapAtPreferredLineLength')]).toEqual([false, false]);
+ expect(getWrapGuides().length).toBe(0);
+
+ atom.config.set('editor.softWrap', true, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect(getWrapGuides().length).toBe(0);
+
+ atom.config.set('wrap-guide.showWrapGuide', 'wrapping');
+
+ expect(getWrapGuides().length).toBe(1);
+ });
});
});
- describe('scoped config', function() {
- it('::getDefaultColumn returns the scope-specific column value', function() {
- atom.config.set('editor.preferredLineLength', 132, {scopeSelector: '.source.js'});
+ describe("When only shown if wrapping", () => {
+ beforeEach(() => {
+ atom.config.set('wrap-guide.showWrapGuide', 'wrapping');
- expect(wrapGuide.getDefaultColumn()).toBe(132);
+ waitsForPromise(() => atom.packages.activatePackage('wrap-guide'));
+
+ waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
+
+ waitsForPromise(() => atom.packages.activatePackage('language-coffee-script'));
+
+ waitsForPromise(() => atom.workspace.open('sample.txt'));
+ waitsForPromise(() => atom.workspace.open('sample.js'));
});
- it('updates the guide when the scope-specific column changes', function() {
- const initial = getLeftPosition(wrapGuide.firstChild);
- const column = atom.config.get('editor.preferredLineLength', {scope: editor.getRootScopeDescriptor()});
- atom.config.set('editor.preferredLineLength', column + 10, {scope: '.source.js'});
- expect(getLeftPosition(wrapGuide.firstChild)).toBeGreaterThan(initial);
+ describe("while the wrapping is active", () => {
+ beforeEach(() => {
+ atom.config.set('editor.softWrap', true);
+ workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.style.height = "200px";
+ workspaceElement.style.width = "1500px";
+
+ jasmine.attachToDOM(workspaceElement);
+
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor();
+ editorElement = editor.getElement();
+ wrapGuide = editorElement.querySelector(".wrap-guide-container");
+ });
+ });
+
+ it("should generate wrap-guides as usual until wrapping is deactivated", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+
+ expect(atom.config.get('editor.softWrap')).toBe(true);
+ expect(getWrapGuides().length).toBe(2);
+
+ atom.config.set('editor.softWrap', false, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrap', {scope: editor.getRootScopeDescriptor()})]).toEqual([true, false]);
+
+ expect(getWrapGuides().length).toBe(1);
+ });
});
- it('updates the guide when wrap-guide.enabled is set to false', function() {
- expect(wrapGuide).toBeVisible();
+ describe("while the wrapping is inactive", () => {
+ beforeEach(() => {
+ atom.config.set('editor.softWrap', false);
+ workspaceElement = atom.views.getView(atom.workspace);
+ workspaceElement.style.height = "200px";
+ workspaceElement.style.width = "1500px";
- atom.config.set('wrap-guide.enabled', false, {scopeSelector: '.source.js'});
+ jasmine.attachToDOM(workspaceElement);
- expect(wrapGuide).not.toBeVisible();
+ runs(() => {
+ editor = atom.workspace.getActiveTextEditor();
+ editorElement = editor.getElement();
+ wrapGuide = editorElement.querySelector(".wrap-guide-container");
+ });
+ });
+
+ it("should not generate wrap-guides until wrapping is reactivated", () => {
+ expect(atom.workspace.getTextEditors().length).toBe(2);
+ function getWrapGuides() {
+ const wrapGuides = [];
+ atom.workspace.getTextEditors().forEach((editor) => {
+ const guides = editor.getElement().querySelectorAll(".wrap-guide");
+ if (guides && guides.length > 0) { return wrapGuides.push(guides); }
+ });
+ return wrapGuides;
+ }
+
+ expect(atom.config.get('editor.softWrap')).toBe(false);
+ expect(getWrapGuides().length).toBe(0);
+
+ atom.config.set('editor.softWrap', true, {scopeSelector: `.${editor.getGrammar().scopeName}`});
+
+ expect([atom.config.get('editor.softWrap'),
+ atom.config.get('editor.softWrap', {scope: editor.getRootScopeDescriptor()})]).toEqual([false, true]);
+
+ expect(getWrapGuides().length).toBe(1);
+ });
});
});
});
diff --git a/packages/wrap-guide/spec/wrap-guide-spec.js b/packages/wrap-guide/spec/wrap-guide-spec.js
index 92d6f7c6b..0a40cd5bb 100644
--- a/packages/wrap-guide/spec/wrap-guide-spec.js
+++ b/packages/wrap-guide/spec/wrap-guide-spec.js
@@ -7,6 +7,7 @@ describe('Wrap Guide', () => {
beforeEach(async () => {
await atom.packages.activatePackage('wrap-guide')
+ atom.config.set('wrap-guide.showWrapGuide', 'always')
editor = await atom.workspace.open('sample.js')
editorElement = editor.getElement()
diff --git a/ppm b/ppm
index a2ade745b..241d794f3 160000
--- a/ppm
+++ b/ppm
@@ -1 +1 @@
-Subproject commit a2ade745bfbc5f1ff181a7975b58fd6400f19abc
+Subproject commit 241d794f326b63b5abdb9769584a36bb34001be1
diff --git a/pulsar.sh b/pulsar.sh
index 6fd4615ce..e3dab18cd 100755
--- a/pulsar.sh
+++ b/pulsar.sh
@@ -10,7 +10,10 @@ else
fi
# Only set the ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT env var if it hasn't been set.
-: ${ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT:=true}
+if [ -z "$ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT" ]
+then
+ export ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=true
+fi
ATOM_ADD=false
ATOM_NEW_WINDOW=false
diff --git a/resources/win/modifyWindowsPath.ps1 b/resources/win/modifyWindowsPath.ps1
index f02be5f1e..2ae10b5dd 100644
--- a/resources/win/modifyWindowsPath.ps1
+++ b/resources/win/modifyWindowsPath.ps1
@@ -3,97 +3,94 @@
# Example Usage:
# Pulsar User Installation:
-# .\_.ps1 -installMode User -installdir "$INSTDIR" -remove 0
+# .\_.ps1 -installdir "$INSTDIR" -remove FALSE
# Pulsar Machine Installation:
-# .\_.ps1 -installMode Machine -installdir "$INSTDIR" -remove 0
+# .\_.ps1 -installdir "$INSTDIR" -remove FALSE
# Pulsar User Uninstallation:
-# .\_.ps1 -installMode User -installdir "$INSTDIR" -remove 1
+# .\_.ps1 -installdir "$INSTDIR" -remove TRUE
# Pulsar Machine Uninstallation:
-# .\_.ps1 -installMode Machine -installdir "$INSTDIR" -remove 1
+# .\_.ps1 -installdir "$INSTDIR" -remove TRUE
-param ($installMode,$installdir,$remove)
+# For safe interaction with environment variables taken from:
+# https://github.com/chocolatey/choco/blob/HEAD/src/chocolatey.resources/helpers/functions/Set-EnvironmentVariable.ps1
+# https://github.com/chocolatey/choco/blob/HEAD/src/chocolatey.resources/helpers/functions/Get-EnvironmentVariable.ps1
+# https://github.com/chocolatey/choco/blob/HEAD/src/chocolatey.resources/helpers/functions/Install-ChocolateyPath.ps1
+
+param ($installdir,$remove)
# When self-elevating, we can't pass a raw boolean. Meaning we accept anything then convert
$remove = [System.Convert]::ToBoolean($remove)
-# Only when modifying the Machine PATH, it takes much longer than expected. So here's a loading bar
-$prog = 1
-Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
-
-if ($installMode -eq "Machine") {
- # PowerShell needs to be running as Admin to modify the Machine Variables
- # So lets attempt to self-elevate
- if (-Not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
- if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
-
- $processOptions = @{
- FilePath = "PowerShell.exe"
- Wait = $true
- PassThru = $true
- Verb = "RunAs"
- ArgumentList = "-File `"" + $MyInvocation.MyCommand.Path + "`" -installMode $installMode -installdir `"" + $installdir + "`" -remove $remove"
- }
-
- Start-Process @processOptions
-
- Exit
- }
- }
-}
-
if (-not $remove) {
- if ($installMode -eq "User" -or $installMode -eq "Machine") {
+ # We want to add Pulsar path values
- $prog = 25
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ # Lets first save a copy of the users current path
+ $env:Path > prior2addition.txt;
- [Environment]::SetEnvironmentVariable("Path", $env:Path + ";$installdir\resources;$installdir\resources\app\ppm\bin", $installMode)
+ $originalPathToInstall = $installdir
- $prog = 50
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ $pulsarPath = $installdir + "\resources";
+ $ppmPath = $installdir + "\resources\app\ppm\bin";
- # While this originally attempting to use the string '%USERPROFILE%' to avoid taking
- # space on the PATH, whatever reads this path at startup in Pulsar, can't handle
- # the variable, and instead creates the directory of the same name
- # within the current folder. But only when opened via the context menu, terminal
- # is fine.
- $exitCode = [Environment]::SetEnvironmentVariable("ATOM_HOME", "$env:UserProfile\.pulsar", $installMode)
+ # Get the current PATH variable
+ $envPath = $env:PATH;
+ if (!$envPath.toLower().Contains($installdir.ToLower())) {
+ # we don't already have the correct environment variable
+ $actualPath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User);
- $prog = 100
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ $statementTerminator = ";";
- Exit $exitCode
+ $pathToInstall = $pulsarPath + $statementTerminator + $ppmPath + $statementTerminator;
+
+ # Does the path end in ';'?
+ $hasStatementTerminator = $actualPath -ne $null -and $actualPath.EndsWith($statementTerminator);
+ # If the last digit is not ';', then add it
+ if (!$hasStatementTerminator -and $actualPath -ne $null) {
+ $pathToInstall = $statementTerminator + $pathToInstall;
+ }
+
+ $actualPath = $actualPath + $pathToInstall;
+
+ # Now to actually set the path to the system
+ $registryType = [Microsoft.Win32.RegistryValueKind]::ExpandString;
+ $keyHive = "HKEY_CURRENT_USER";
+ $registryKey = "Environment";
+
+ $exitCode = [Microsoft.Win32.Registry]::SetValue($keyHive + "\" + $registryKey, "Path", $actualPath, [System.EnvironmentVariableTarget]::User);
+
+ Exit $exitCode;
+ } else {
+ Write-Host "Pulsar is already present on the User PATH.";
}
+
} else {
- if ($installMode -eq "User" -or $installMode -eq "Machine") {
+ # We want to remove Pulsar from the user path
- $prog = 25
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ # Lets first save a copy of the users current path
+ $env:Path > prior2removal.txt;
- $path = [Environment]::GetEnvironmentVariable("Path", $installMode)
+ $pulsarPath = $installdir + "\resources";
+ $ppmPath = $installdir + "\resources\app\ppm\bin";
- $prog = 50
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ # Get the current PATH variable
+ $envPath = $env:PATH;
+ if ($envPath.toLower().Contains($installdir.ToLower())) {
+ # the install dir is in fact on the path
+ $actualPath = [Environment]::GetEnvironmentVariable("Path", [System.EnvironmentVariableTarget]::User);
- # Remove unwanted element from path
- $path = ($path.Split(";") | Where-Object { $_ -ne "$installdir\resources" }) -join ";"
- $path = ($path.Split(";") | Where-Object { $_ -ne "$installdir\resources\app\ppm\bin" }) -join ";"
+ $actualPath = ($actualPath.Split(";") | Where-Object { $_ -ne $ppmPath }) -join ";";
+ # Order is important, as Pulsar's path INCLUDES ppm's path
+ $actualPath = ($actualPath.Split(";") | Where-Object { $_ -ne $pulsarPath }) -join ";";
- $prog = 75
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
+ # Now to actually set the path to the system
+ $registryType = [Microsoft.Win32.RegistryValueKind]::ExpandString;
+ $keyHive = "HKEY_CURRENT_USER";
+ $registryKey = "Environment";
- # Set our new path
- [Environment]::SetEnvironmentVariable("Path", $path, $installMode)
+ $exitCode = [Microsoft.Win32.Registry]::SetValue($keyHive + "\" + $registryKey, "Path", $actualPath, [System.EnvironmentVariableTarget]::User);
- $prog = 90
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
-
- # Set ATOM_HOME path
- $exitCode = [Environment]::SetEnvironmentVariable("ATOM_HOME", $null, $installMode)
-
- $prog = 100
- Write-Progress -Activity "Modifying Pulsar ($installdir) on the PATH..." -Status "$prog% Complete:" -PercentComplete $prog
-
- Exit $exitCode
- } # Else we have been given bad params, and will silently exit
+ Exit $exitCode;
+ } else {
+ Write-Host "Pulsar is not present on the User PATH.";
+ }
}
diff --git a/script/rolling-release-scripts/rolling-release-binary-upload.js b/script/rolling-release-scripts/rolling-release-binary-upload.js
index f5039c57b..9cf035ea1 100644
--- a/script/rolling-release-scripts/rolling-release-binary-upload.js
+++ b/script/rolling-release-scripts/rolling-release-binary-upload.js
@@ -9,8 +9,9 @@ const packageJson = require("../../package.json");
// Since cirrus always triggers this script, we must check if the version is a rolling
// release version
const verSegments = packageJson.version.split(".");
+const lastVerSegment = verSegments[verSegments.length - 1];
-if (verSegments[verSegments.length - 1].length < 4) {
+if (lastVerSegment.length < 4 || lastVerSegment.includes('-dev')) {
console.log(`According to our version: ${packageJson.version} this is not a rolling release...`);
console.log("Exiting without changes...");
process.exit(0);
diff --git a/script/validate-wasm-grammar-prs.js b/script/validate-wasm-grammar-prs.js
index 503547530..ff208903b 100644
--- a/script/validate-wasm-grammar-prs.js
+++ b/script/validate-wasm-grammar-prs.js
@@ -62,8 +62,16 @@ if (wasmFilesChanged.length === 0) {
// are also accompanied by a change in the `parserSource` key
for (const wasmFile of wasmFilesChanged) {
+ // Ignore files that have been deleted or moved.
+ if (!fs.existsSync(wasmFile)) {
+ console.log(`Skipping file that no longer exists: ${wasmFile}`);
+ continue;
+ }
const wasmPath = path.dirname(wasmFile);
+ // Don't check the base `tree-sitter.wasm` file.
+ if (wasmFile.includes('vendor/web-tree-sitter')) continue;
+
const files = fs.readdirSync(path.join(wasmPath, ".."));
console.log(`Detected changes to: ${wasmFile}`);
@@ -76,6 +84,9 @@ for (const wasmFile of wasmFilesChanged) {
}
for (const file of files) {
+ // Only check `cson` files.
+ if (!file.endsWith('.cson')) continue;
+
const filePath = path.join(wasmPath, "..", file);
console.log(`Checking: ${filePath}`);
diff --git a/spec/.eslintrc.js b/spec/.eslintrc.js
index 5226d6921..77888e061 100644
--- a/spec/.eslintrc.js
+++ b/spec/.eslintrc.js
@@ -1,5 +1,9 @@
module.exports = {
env: { jasmine: true },
+ globals: {
+ waitsForPromise: true,
+ advanceClock: true
+ },
rules: {
"node/no-unpublished-require": "off",
"node/no-extraneous-require": "off",
diff --git a/spec/config-spec.js b/spec/config-spec.js
index b6b63c65c..f292542f9 100644
--- a/spec/config-spec.js
+++ b/spec/config-spec.js
@@ -150,6 +150,41 @@ describe('Config', () => {
).toBe(4);
});
+ it("merges project-specific settings with other settings when the keypath is an object", () => {
+ atom.config.set('x.y', 1);
+ atom.config.set('x.z', "fibrinolysis");
+
+ atom.project.replace({
+ originPath: 'TEST',
+ paths: atom.project.getPaths(),
+ config: {
+ "*": {
+ "x": {
+ "y": 4
+ }
+ }
+ }
+ });
+
+ // Project-specific settings work fine, as the spec below shows, when
+ // the value being retrieved is a primitive. But until recently, Pulsar
+ // didn't know what to do if the value being retrieved was an object.
+ //
+ // Imagine asking for _all_ config settings. The non-project-specific
+ // lookup returns everything. The project-specific lookup returns only
+ // a few overrides. Pulsar needs to _blend_ these two objects, but was
+ // previously choosing the project-specific lookup just because it
+ // wasn't undefined.
+
+ // Here we demonstrate that it now retrieves an object for the given
+ // key path at the normal location and applies a project-specific
+ // βpatchβ¦β
+ expect(atom.config.get('x')).toEqual({ y: 4, z: "fibrinolysis" })
+
+ // β¦without any general settings leaking into the project config.
+ expect(atom.config.projectSettings.x.z).toBeUndefined();
+ });
+
it("ignores the project-specific source when 'excludeSources' tells it to", () => {
atom.config.set('x.y', 1);
diff --git a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson
index 935132388..daff47abe 100644
--- a/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson
+++ b/spec/fixtures/packages/package-with-rb-filetype/grammars/rb.cson
@@ -1,5 +1,5 @@
'name': 'Test Ruby'
-'type': 'tree-sitter'
+'type': 'modern-tree-sitter'
'scopeName': 'test.rb'
'parser': 'tree-sitter-ruby'
@@ -8,3 +8,7 @@
'fileTypes': [
'rb'
]
+
+treeSitter:
+ highlightsQuery: './tree-sitter-empty.scm'
+ grammar: './tree-sitter-empty.wasm'
diff --git a/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.scm b/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.scm
new file mode 100644
index 000000000..a24aa458a
--- /dev/null
+++ b/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.scm
@@ -0,0 +1 @@
+; (empty)
diff --git a/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.wasm b/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.wasm
new file mode 100755
index 000000000..c8cafa361
Binary files /dev/null and b/spec/fixtures/packages/package-with-rb-filetype/grammars/tree-sitter-empty.wasm differ
diff --git a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
index f6b32e532..d0351bd54 100644
--- a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
+++ b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/some-language.cson
@@ -2,7 +2,7 @@ name: 'Some Language'
scopeName: 'some-language'
-type: 'tree-sitter'
+type: 'modern-tree-sitter'
parser: './fake-parser'
@@ -12,3 +12,7 @@ fileTypes: [
scopes:
'class > identifier': 'entity.name.type.class'
+
+treeSitter:
+ grammar: './tree-sitter-empty.wasm'
+ highlightsQuery: './tree-sitter-empty.scm'
diff --git a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.scm b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.scm
new file mode 100644
index 000000000..a24aa458a
--- /dev/null
+++ b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.scm
@@ -0,0 +1 @@
+; (empty)
diff --git a/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.wasm b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.wasm
new file mode 100755
index 000000000..c8cafa361
Binary files /dev/null and b/spec/fixtures/packages/package-with-tree-sitter-grammar/grammars/tree-sitter-empty.wasm differ
diff --git a/spec/grammar-registry-spec.js b/spec/grammar-registry-spec.js
index 66b3a78cb..cae311ec8 100644
--- a/spec/grammar-registry-spec.js
+++ b/spec/grammar-registry-spec.js
@@ -11,9 +11,9 @@ const { OnigScanner } = SecondMate;
// Expects one of `textmate`, `node-tree-sitter`, or `wasm-tree-sitter`.
function setConfigForLanguageMode(mode, options = {}) {
let useTreeSitterParsers = mode !== 'textmate';
- let useExperimentalModernTreeSitter = mode === 'wasm-tree-sitter';
+ let useLegacyTreeSitter = mode === 'node-tree-sitter';
atom.config.set('core.useTreeSitterParsers', useTreeSitterParsers, options);
- atom.config.set('core.useExperimentalModernTreeSitter', useExperimentalModernTreeSitter, options);
+ atom.config.set('core.useLegacyTreeSitter', useLegacyTreeSitter, options);
}
describe('GrammarRegistry', () => {
@@ -258,7 +258,7 @@ describe('GrammarRegistry', () => {
// TODO: Why doesn't this path resolution work like the one above?
const modernTreeSitterGrammar = grammarRegistry.loadGrammarSync(
require.resolve(
- '../packages/language-javascript/grammars/tree-sitter-2-javascript.cson'
+ '../packages/language-javascript/grammars/modern-tree-sitter-javascript.cson'
)
);
expect(buffer.getLanguageMode().grammar).toBe(modernTreeSitterGrammar);
@@ -285,7 +285,7 @@ describe('GrammarRegistry', () => {
// TODO: Why doesn't this path resolution work like the one above?
const modernTreeSitterGrammar = grammarRegistry.loadGrammarSync(
require.resolve(
- '../packages/language-javascript/grammars/tree-sitter-2-javascript.cson'
+ '../packages/language-javascript/grammars/modern-tree-sitter-javascript.cson'
)
);
@@ -478,6 +478,14 @@ describe('GrammarRegistry', () => {
);
});
+ it(`returns a legacy Tree-sitter grammar if the user opted into it via a scope-specific setting`, async () => {
+ await atom.packages.activatePackage('language-javascript');
+ setConfigForLanguageMode('node-tree-sitter', { scopeSelector: '.source.js' })
+ let grammar = atom.grammars.selectGrammar('file.js');
+ expect(grammar.name).toBe('JavaScript');
+ expect(grammar.constructor.name).toBe('TreeSitterGrammar');
+ })
+
it("uses the filePath's shebang line if the grammar cannot be determined by the extension or basename", async () => {
await atom.packages.activatePackage('language-javascript');
await atom.packages.activatePackage('language-ruby');
diff --git a/spec/package-manager-spec.js b/spec/package-manager-spec.js
index 6948e10eb..8b7da3ad3 100644
--- a/spec/package-manager-spec.js
+++ b/spec/package-manager-spec.js
@@ -1440,7 +1440,8 @@ describe('PackageManager', () => {
await atom.packages.activatePackage('package-with-tree-sitter-grammar');
const grammar = atom.grammars.selectGrammar('test.somelang');
expect(grammar.name).toBe('Some Language');
- expect(grammar.languageModule.isFakeTreeSitterParser).toBe(true);
+ await grammar.getQuery('highlightsQuery');
+ expect(grammar.highlightsQuery.includes('(empty)')).toBe(true);
});
});
diff --git a/spec/scope-resolver-spec.js b/spec/scope-resolver-spec.js
index ccd2af6b2..0aee61c60 100644
--- a/spec/scope-resolver-spec.js
+++ b/spec/scope-resolver-spec.js
@@ -23,22 +23,18 @@ function resolve(modulePath) {
}
const jsGrammarPath = resolve(
- 'language-javascript/grammars/tree-sitter-2-javascript.cson'
+ 'language-javascript/grammars/modern-tree-sitter-javascript.cson'
);
let jsConfig = CSON.readFileSync(jsGrammarPath);
const jsRegexGrammarPath = resolve(
- 'language-javascript/grammars/tree-sitter-2-regex.cson'
+ 'language-javascript/grammars/modern-tree-sitter-regex.cson'
);
let jsRegexConfig = CSON.readFileSync(jsRegexGrammarPath);
-async function getAllCaptures(grammar, languageMode, layer = null) {
+async function getAllCapturesWithScopeResolver(grammar, languageMode, scopeResolver, layer = null) {
let query = await grammar.getQuery('highlightsQuery');
layer = layer ?? languageMode.rootLanguageLayer;
- let scopeResolver = new ScopeResolver(
- layer,
- (name) => languageMode.idForScope(name),
- );
let { start, end } = languageMode.buffer.getRange();
let { tree } = layer;
return {
@@ -47,6 +43,32 @@ async function getAllCaptures(grammar, languageMode, layer = null) {
};
}
+function makeScopeResolver(languageMode, layer) {
+ layer = layer ?? languageMode.rootLanguageLayer;
+ return new ScopeResolver(
+ layer,
+ (name) => languageMode.idForScope(name),
+ );
+}
+
+async function getAllCaptures(grammar, languageMode, layer = null) {
+ layer = layer ?? languageMode.rootLanguageLayer;
+ let scopeResolver = makeScopeResolver(languageMode, layer);
+ return getAllCapturesWithScopeResolver(grammar, languageMode, scopeResolver, layer);
+}
+
+async function getAllMatchesWithScopeResolver(...args) {
+ let { captures, scopeResolver } = await getAllCapturesWithScopeResolver(...args);
+ let matches = [];
+ for (let capture of captures) {
+ let range = scopeResolver.store(capture);
+ if (range) {
+ matches.push(capture);
+ }
+ }
+ return matches;
+}
+
async function getAllMatches(...args) {
let { captures, scopeResolver } = await getAllCaptures(...args);
let matches = [];
@@ -71,7 +93,7 @@ function rangeFromDescriptor(rawRange) {
}
describe('ScopeResolver', () => {
- let editor, buffer, grammar, scopeResolver;
+ let editor, buffer, grammar;
beforeEach(async () => {
grammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
@@ -79,7 +101,10 @@ describe('ScopeResolver', () => {
buffer = editor.getBuffer();
atom.grammars.addGrammar(grammar);
atom.config.set('core.useTreeSitterParsers', true);
- atom.config.set('core.useExperimentalModernTreeSitter', true);
+ });
+
+ afterEach(() => {
+ ScopeResolver.clearConfigCache();
});
it('resolves all scopes in absence of any tests or adjustments', async () => {
@@ -100,13 +125,45 @@ describe('ScopeResolver', () => {
let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode);
for (let capture of captures) {
- let { node, name } = capture;
+ let { node } = capture;
let range = scopeResolver.store(capture);
expect(stringForNodeRange(range))
.toBe(stringForNodeRange(node));
}
});
+ it('provides the grammar with the text of leaf nodes only', async () => {
+ await grammar.setQueryForTest('highlightsQuery', `
+ (expression_statement) @not_leaf_node
+ (call_expression) @also_not_leaf_node
+ (identifier) @leaf_node
+ (property_identifier) @also_leaf_node
+ `);
+
+ let tokens = [];
+ const original = grammar.idForScope.bind(grammar);
+ grammar.idForScope = function(scope, text) {
+ if (text) {
+ tokens.push(text);
+ }
+ return original(scope, text);
+ };
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ buffer.setText('aa.bb(cc.dd());');
+ await languageMode.ready;
+
+ // If non-leaf nodes are included, this list would included things like
+ // 'aa.bb()' and `cc.dd()`
+ expect(tokens).toEqual([
+ 'aa',
+ 'bb',
+ 'cc',
+ 'dd',
+ ]);
+ });
+
it('interpolates magic tokens in scope names', async () => {
await grammar.setQueryForTest('highlightsQuery', `
(lexical_declaration kind: _ @declaration._TYPE_)
@@ -134,6 +191,67 @@ describe('ScopeResolver', () => {
]);
});
+ it('does not apply any scopes when @_IGNORE_ is used', async () => {
+ await grammar.setQueryForTest('highlightsQuery', `
+ (lexical_declaration kind: _ @_IGNORE_
+ (#match? @_IGNORE_ "const"))
+ (lexical_declaration kind: _ @let
+ (#match? @let "let"))
+ `);
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ buffer.setText(dedent`
+ // this is a comment
+ const foo = "ahaha";
+ let bar = 'troz'
+ `);
+ await languageMode.ready;
+
+ let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode);
+
+ for (let capture of captures) {
+ let { node, name } = capture;
+ let result = scopeResolver.store(capture);
+ if (name === '_IGNORE_') {
+ expect(!!result).toBe(false);
+ } else {
+ expect(!!result).toBe(true);
+ }
+ }
+ });
+
+ it('does not apply any scopes when multiple @_IGNORE_s are used', async () => {
+ await grammar.setQueryForTest('highlightsQuery', `
+ (variable_declarator
+ (identifier) @_IGNORE_.identifier
+ (string) @_IGNORE_.string
+ )
+ `);
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ buffer.setText(dedent`
+ // this is a comment
+ const foo = "ahaha";
+ let bar = false
+ `);
+ await languageMode.ready;
+
+ let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode);
+
+ for (let capture of captures) {
+ let { node, name } = capture;
+ let result = scopeResolver.store(capture);
+ if (name.startsWith('_IGNORE_')) {
+ expect(!!result).toBe(false);
+ } else {
+ expect(!!result).toBe(true);
+ }
+ }
+ });
+
+
describe('adjustments', () => {
it('adjusts ranges with (#set! adjust.startAt)', async () => {
await grammar.setQueryForTest('highlightsQuery', `
@@ -276,12 +394,12 @@ describe('ScopeResolver', () => {
describe('tests', () => {
- it('rejects scopes for ranges that have already been claimed by another capture with (#set! capture.final true)', async () => {
+ it('rejects scopes for ranges that have already been claimed by another capture with (#set! capture.final)', async () => {
await grammar.setQueryForTest('highlightsQuery', `
(comment) @comment
(string) @string0
((string) @string1
- (#set! capture.final true))
+ (#set! capture.final))
(string) @string2
"=" @operator
@@ -298,7 +416,7 @@ describe('ScopeResolver', () => {
let { scopeResolver, captures } = await getAllCaptures(grammar, languageMode);
for (let capture of captures) {
- let { node, name } = capture;
+ let { name } = capture;
let result = scopeResolver.store(capture);
if (name === 'string0') {
expect(!!result).toBe(true);
@@ -348,12 +466,12 @@ describe('ScopeResolver', () => {
}
});
- it('rejects scopes for ranges that have already been claimed by another capture with (#set! capture.final true)', async () => {
+ it('rejects scopes for ranges that have already been claimed by another capture with (#set! capture.final)', async () => {
await grammar.setQueryForTest('highlightsQuery', `
(comment) @comment
(string) @string0
((string) @string1
- (#set! capture.final true))
+ (#set! capture.final))
(string) @string2
"=" @operator
@@ -1136,12 +1254,33 @@ describe('ScopeResolver', () => {
`);
await languageMode.ready;
- let matched = await getAllMatches(grammar, languageMode);
+ let scopeResolver = makeScopeResolver(languageMode);
+
+ let matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
expect(matched.length).toBe(4);
atom.config.set('core.careAboutBooleans', "something-else");
- matched = await getAllMatches(grammar, languageMode);
+ matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
+ expect(matched.length).toBe(0);
+
+ atom.config.set(
+ 'core.careAboutBooleans',
+ 'something',
+ { scope: [grammar.scopeName] }
+ );
+
+ matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
+ expect(matched.length).toBe(4);
+
+ atom.config.set('core.careAboutBooleans', "something");
+
+ atom.config.set(
+ 'core.careAboutBooleans',
+ 'something-else',
+ { scope: [grammar.scopeName] }
+ );
+ matched = await getAllMatchesWithScopeResolver(grammar, languageMode, scopeResolver);
expect(matched.length).toBe(0);
});
diff --git a/spec/text-editor-registry-spec.js b/spec/text-editor-registry-spec.js
index bc1850f80..71b921e9d 100644
--- a/spec/text-editor-registry-spec.js
+++ b/spec/text-editor-registry-spec.js
@@ -5,6 +5,13 @@ const { Point, Range } = TextBuffer;
const dedent = require('dedent');
const NullGrammar = require('../src/null-grammar');
+function setupLanguageMode (editor) {
+ let languageMode = editor.getBuffer().getLanguageMode();
+ languageMode.useAsyncParsing = false;
+ languageMode.useAsyncIndent = false;
+ return languageMode;
+}
+
describe('TextEditorRegistry', function() {
let registry, editor, initialPackageActivation;
@@ -298,8 +305,10 @@ describe('TextEditorRegistry', function() {
await initialPackageActivation;
await atom.packages.activatePackage('language-javascript');
atom.grammars.assignLanguageMode(editor, 'source.js');
+ let languageMode = setupLanguageMode(editor);
atom.config.set('editor.tabType', 'auto');
await initialPackageActivation;
+ await languageMode.ready;
editor.setText(dedent`
{
diff --git a/spec/text-editor-spec.js b/spec/text-editor-spec.js
index bc9c73d77..832c482a5 100644
--- a/spec/text-editor-spec.js
+++ b/spec/text-editor-spec.js
@@ -7,10 +7,17 @@ const os = require('os');
const TextEditor = require('../src/text-editor');
const TextBuffer = require('text-buffer');
const TextMateLanguageMode = require('../src/text-mate-language-mode');
-const TreeSitterLanguageMode = require('../src/tree-sitter-language-mode');
+const WASMTreeSitterLanguageMode = require('../src/wasm-tree-sitter-language-mode');
+
+async function languageModeReady(editor) {
+ let languageMode = editor.getBuffer().getLanguageMode();
+ if (languageMode.ready) {
+ await languageMode.ready;
+ }
+}
describe('TextEditor', () => {
- let buffer, editor, lineLengths;
+ let buffer, editor, lineLengths, languageMode;
beforeEach(async () => {
editor = await atom.workspace.open('sample.js');
@@ -18,8 +25,22 @@ describe('TextEditor', () => {
editor.update({ autoIndent: false });
lineLengths = buffer.getLines().map(line => line.length);
await atom.packages.activatePackage('language-javascript');
+ languageMode = buffer.getLanguageMode();
+ if ('useAsyncParsing' in languageMode) {
+ languageMode.useAsyncParsing = false;
+ languageMode.useAsyncIndent = false;
+ }
+ if (languageMode.ready) {
+ await languageMode.ready;
+ }
});
+ afterEach(async () => {
+ if (languageMode?.atTransactionEnd) {
+ await languageMode.atTransactionEnd();
+ }
+ })
+
it('generates unique ids for each editor', async () => {
// Deserialized editors are initialized with the serialized id. We can
// initialize an editor with what we expect to be the next id:
@@ -1565,7 +1586,7 @@ describe('TextEditor', () => {
it("doesn't get stuck in a infinite loop when called from ::onDidAddCursor after the last selection has been destroyed (regression)", () => {
let callCount = 0;
editor.getLastSelection().destroy();
- editor.onDidAddCursor(function(cursor) {
+ editor.onDidAddCursor(function (cursor) {
callCount++;
editor.getLastSelection();
});
@@ -2223,13 +2244,13 @@ describe('TextEditor', () => {
expect(scopeDescriptors[0].getScopesArray()).toEqual(['source.js']);
expect(scopeDescriptors[1].getScopesArray()).toEqual([
'source.js',
- 'string.quoted'
+ 'string.quoted.single.js'
]);
spyOn(
editor.getBuffer().getLanguageMode(),
'getNonWordCharacters'
- ).andCallFake(function(position) {
+ ).andCallFake(function (position) {
const result = '/()"\':,.;<>~!@#$%^&*|+=[]{}`?';
const scopes = this.scopeDescriptorForPosition(
position
@@ -4160,9 +4181,10 @@ describe('TextEditor', () => {
});
});
- it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", () => {
+ it("inserts a newline below the cursor's current line, autoindents it, and moves the cursor to the end of the line", async () => {
editor.update({ autoIndent: true });
editor.insertNewlineBelow();
+ await languageMode.atTransactionEnd();
expect(buffer.lineForRow(0)).toBe('var quicksort = function () {');
expect(buffer.lineForRow(1)).toBe(' ');
expect(editor.getCursorBufferPosition()).toEqual([1, 2]);
@@ -5291,7 +5313,7 @@ describe('TextEditor', () => {
it('notifies ::onWillInsertText observers', () => {
const insertedStrings = [];
- editor.onWillInsertText(function({ text, cancel }) {
+ editor.onWillInsertText(function ({ text, cancel }) {
insertedStrings.push(text);
cancel();
});
@@ -5355,12 +5377,13 @@ describe('TextEditor', () => {
});
describe('when pasting line(s) above a line that matches the decreaseIndentPattern', () =>
- it('auto-indents based on the pasted line(s) only', () => {
+ it('auto-indents based on the pasted line(s) only', async () => {
atom.clipboard.write('a(x);\n b(x);\n c(x);\n', {
indentBasis: 0
});
editor.setCursorBufferPosition([7, 0]);
editor.pasteText();
+ await languageMode.atTransactionEnd();
expect(editor.lineTextForBufferRow(7)).toBe(' a(x);');
expect(editor.lineTextForBufferRow(8)).toBe(' b(x);');
@@ -6823,7 +6846,7 @@ describe('TextEditor', () => {
describe("when the buffer's language mode changes", () => {
beforeEach(() => {
atom.config.set('core.useTreeSitterParsers', false);
-;
+
});
it('notifies onDidTokenize observers when retokenization is finished', async () => {
@@ -6908,7 +6931,7 @@ describe('TextEditor', () => {
});
describe('when the line preceding the newline is a comment', () => {
- it('maintains the indent of the commented line', () => {
+ it('maintains the indent of the commented line', async () => {
editor.setCursorBufferPosition([0, 0]);
editor.insertText(' //');
editor.setCursorBufferPosition([0, Infinity]);
@@ -7381,7 +7404,7 @@ describe('TextEditor', () => {
describe("when the editor's grammar has an injection selector", () => {
beforeEach(async () => {
atom.config.set('core.useTreeSitterParsers', false);
-;
+
await atom.packages.activatePackage('language-text');
await atom.packages.activatePackage('language-javascript');
});
@@ -8071,45 +8094,52 @@ describe('TextEditor', () => {
describe('indent guides', () => {
it('shows indent guides when `editor.showIndentGuide` is set to true and the editor is not mini', () => {
editor.update({ showIndentGuide: false });
+
expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([
{
text: ' ',
- scopes: ['syntax--source syntax--js', 'leading-whitespace']
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'leading-whitespace']
},
{
text: 'var',
- scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type']
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']
},
- { text: ' sort ', scopes: ['syntax--source syntax--js'] }
+ {
+ text: ' ',
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js']
+ }
]);
editor.update({ showIndentGuide: true });
expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([
{
text: ' ',
- scopes: [
- 'syntax--source syntax--js',
- 'leading-whitespace indent-guide'
- ]
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'leading-whitespace indent-guide']
},
{
text: 'var',
- scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type']
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']
},
- { text: ' sort ', scopes: ['syntax--source syntax--js'] }
+ {
+ text: ' ',
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js']
+ }
]);
editor.setMini(true);
expect(editor.tokensForScreenRow(1).slice(0, 3)).toEqual([
{
text: ' ',
- scopes: ['syntax--source syntax--js', 'leading-whitespace']
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'leading-whitespace']
},
{
text: 'var',
- scopes: ['syntax--source syntax--js', 'syntax--storage syntax--type']
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js', 'syntax--storage syntax--type syntax--var syntax--js']
},
- { text: ' sort ', scopes: ['syntax--source syntax--js'] }
+ {
+ text: ' ',
+ scopes: ['syntax--source syntax--js', 'syntax--meta syntax--block syntax--function syntax--js']
+ }
]);
});
});
@@ -8181,10 +8211,68 @@ describe('TextEditor', () => {
});
});
+ describe('.getCommentDelimitersForBufferPosition', () => {
+ it('returns comment delimiters on a TextMate grammar', async () => {
+ atom.config.set('core.useTreeSitterParsers', false);
+
+ editor = await atom.workspace.open('sample.js', { autoIndent: false });
+ await atom.packages.activatePackage('language-javascript');
+
+ let buffer = editor.getBuffer();
+
+ let languageMode = new TextMateLanguageMode({
+ buffer,
+ grammar: atom.grammars.grammarForScopeName('source.js')
+ });
+
+ buffer.setLanguageMode(languageMode);
+
+ languageMode.startTokenizing();
+ while (languageMode.firstInvalidRow() != null) {
+ advanceClock();
+ }
+
+ let delimiters = editor.getCommentDelimitersForBufferPosition([8, 0]);
+ expect(delimiters).toEqual({
+ line: '//',
+ block: ['/*', '*/']
+ })
+ })
+
+ it('returns comment delimiters on a modern Tree-sitter grammar', async () => {
+ jasmine.useRealClock();
+ atom.config.set('core.useTreeSitterParsers', true);
+
+ editor = await atom.workspace.open('sample.js', { autoIndent: false });
+ await atom.packages.activatePackage('language-javascript');
+
+ let buffer = editor.getBuffer();
+
+ let languageMode = new WASMTreeSitterLanguageMode({
+ buffer,
+ grammar: atom.grammars.grammarForScopeName('source.js'),
+ grammars: atom.grammars
+ });
+
+ languageMode.useAsyncParsing = false;
+ languageMode.useAsyncIndent = false;
+
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+
+ let delimiters = editor.getCommentDelimitersForBufferPosition([8, 0]);
+ expect(delimiters).toEqual({
+ line: '//',
+ block: ['/*', '*/']
+ })
+ })
+ })
+
describe('.syntaxTreeScopeDescriptorForBufferPosition(position)', () => {
it('returns the result of scopeDescriptorForBufferPosition() when textmate language mode is used', async () => {
atom.config.set('core.useTreeSitterParsers', false);
-;
+
editor = await atom.workspace.open('sample.js', { autoIndent: false });
await atom.packages.activatePackage('language-javascript');
@@ -8212,17 +8300,23 @@ describe('TextEditor', () => {
});
it('returns the result of syntaxTreeScopeDescriptorForBufferPosition() when tree-sitter language mode is used', async () => {
+ jasmine.useRealClock();
editor = await atom.workspace.open('sample.js', { autoIndent: false });
await atom.packages.activatePackage('language-javascript');
let buffer = editor.getBuffer();
- buffer.setLanguageMode(
- new TreeSitterLanguageMode({
- buffer,
- grammar: atom.grammars.grammarForScopeName('source.js')
- })
- );
+ let languageMode = new WASMTreeSitterLanguageMode({
+ buffer,
+ grammar: atom.grammars.grammarForScopeName('source.js'),
+ grammars: atom.grammars
+ });
+
+ languageMode.useAsyncParsing = false;
+ languageMode.useAsyncIndent = false;
+
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
const syntaxTreeeScopeDescriptor = editor.syntaxTreeScopeDescriptorForBufferPosition(
[4, 17]
@@ -8748,6 +8842,7 @@ describe('TextEditor', () => {
describe('.unfoldAll()', () => {
it('unfolds every folded line', async () => {
editor = await atom.workspace.open('sample.js', { autoIndent: false });
+ await languageModeReady(editor);
const initialScreenLineCount = editor.getScreenLineCount();
editor.foldBufferRow(0);
@@ -8763,6 +8858,7 @@ describe('TextEditor', () => {
editor = await atom.workspace.open('sample-with-comments.js', {
autoIndent: false
});
+ await languageModeReady(editor);
const initialScreenLineCount = editor.getScreenLineCount();
editor.foldBufferRow(0);
@@ -8778,6 +8874,7 @@ describe('TextEditor', () => {
describe('.foldAll()', () => {
it('folds every foldable line', async () => {
editor = await atom.workspace.open('sample.js', { autoIndent: false });
+ await languageModeReady(editor);
editor.foldAll();
const [fold1, fold2, fold3] = editor.unfoldAll();
@@ -8790,6 +8887,7 @@ describe('TextEditor', () => {
describe('.foldBufferRow(bufferRow)', () => {
beforeEach(async () => {
editor = await atom.workspace.open('sample.js');
+ await languageModeReady(editor);
});
describe('when bufferRow can be folded', () => {
@@ -8852,6 +8950,7 @@ describe('TextEditor', () => {
describe('.foldAllAtIndentLevel(indentLevel)', () => {
it('folds blocks of text at the given indentation level', async () => {
editor = await atom.workspace.open('sample.js', { autoIndent: false });
+ await languageModeReady(editor);
editor.foldAllAtIndentLevel(0);
expect(editor.lineTextForScreenRow(0)).toBe(
@@ -8885,6 +8984,7 @@ describe('TextEditor', () => {
editor = await atom.workspace.open('sample-with-comments.js', {
autoIndent: false
});
+ await languageModeReady(editor);
editor.foldAllAtIndentLevel(0);
const folds = editor.unfoldAll();
diff --git a/spec/tree-sitter-language-mode-spec.js b/spec/tree-sitter-language-mode-spec.js
index 32a1e2c12..3fc7451d7 100644
--- a/spec/tree-sitter-language-mode-spec.js
+++ b/spec/tree-sitter-language-mode-spec.js
@@ -42,7 +42,7 @@ describe('TreeSitterLanguageMode', () => {
buffer = editor.getBuffer();
editor.displayLayer.reset({ foldCharacter: 'β¦' });
atom.config.set('core.useTreeSitterParsers', true);
- atom.config.set('core.useExperimentalModernTreeSitter', false);
+ atom.config.set('core.useLegacyTreeSitter', true);
});
describe('highlighting', () => {
@@ -87,7 +87,7 @@ describe('TreeSitterLanguageMode', () => {
});
const original = grammar.idForScope.bind(grammar);
let tokens = [];
- grammar.idForScope = function(scope, text) {
+ grammar.idForScope = function (scope, text) {
if (text && tokens[tokens.length - 1] !== text) {
tokens.push(text);
}
@@ -2342,13 +2342,30 @@ describe('TreeSitterLanguageMode', () => {
const htmlCommentStrings = {
commentStartString: ''
+ commentEndString: '-->',
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
};
const jsCommentStrings = {
commentStartString: '//',
- commentEndString: undefined
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: '//',
+ block: undefined
+ }
};
+ const hybridCommentStrings = {
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ }
+
expect(languageMode.commentStringsForPosition(new Point(0, 0))).toEqual(
htmlCommentStrings
);
@@ -2356,16 +2373,16 @@ describe('TreeSitterLanguageMode', () => {
htmlCommentStrings
);
expect(languageMode.commentStringsForPosition(new Point(2, 0))).toEqual(
- jsCommentStrings
+ hybridCommentStrings
);
expect(languageMode.commentStringsForPosition(new Point(3, 0))).toEqual(
- jsCommentStrings
+ hybridCommentStrings
);
expect(languageMode.commentStringsForPosition(new Point(4, 0))).toEqual(
htmlCommentStrings
);
expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual(
- jsCommentStrings
+ hybridCommentStrings
);
expect(languageMode.commentStringsForPosition(new Point(6, 0))).toEqual(
htmlCommentStrings
diff --git a/spec/ui-spec.js b/spec/ui-spec.js
new file mode 100644
index 000000000..8ad5e0e66
--- /dev/null
+++ b/spec/ui-spec.js
@@ -0,0 +1,37 @@
+
+describe("Renders Markdown", () => {
+ describe("properly when given no opts", () => {
+ it("handles bold", () => {
+ expect(atom.ui.markdown.render("**Hello World**"))
+ .toBe("Hello World
\n");
+ });
+ });
+
+ describe("transforms links correctly", () => {
+ it("makes no changes to a fqdn link", () => {
+ expect(atom.ui.markdown.render("[Hello World](https://github.com)"))
+ .toBe('Hello World
\n');
+ });
+ it("resolves package links to pulsar", () => {
+ expect(atom.ui.markdown.render("[Hello](https://atom.io/packages/hey-pane)"))
+ .toBe('Hello
\n');
+ });
+ it("resolves atom links to web archive", () => {
+ expect(atom.ui.markdown.render("[Hello](https://flight-manual.atom.io/some-docs)"))
+ .toBe('Hello
\n');
+ });
+ it("resolves incomplete local links", () => {
+ expect(atom.ui.markdown.render(
+ "[Hello](./readme.md)",
+ { rootDomain: "https://github.com/pulsar-edit/pulsar" }
+ )).toBe('Hello
\n');
+ });
+ it("resolves incomplete root links", () => {
+ expect(atom.ui.markdown.render(
+ "[Hello](/readme.md)",
+ { rootDomain: "https://github.com/pulsar-edit/pulsar" }
+ )).toBe('Hello
\n');
+ });
+ });
+
+});
diff --git a/spec/wasm-tree-sitter-language-mode-spec.js b/spec/wasm-tree-sitter-language-mode-spec.js
index 9937880c8..0d4836f8b 100644
--- a/spec/wasm-tree-sitter-language-mode-spec.js
+++ b/spec/wasm-tree-sitter-language-mode-spec.js
@@ -17,20 +17,25 @@ function resolve(modulePath) {
return require.resolve(`${PATH}/${modulePath}`)
}
+// Just for syntax highlighting.
+function scm(strings) {
+ return strings.join('');
+}
+
const cGrammarPath = resolve('language-c/grammars/modern-tree-sitter-c.cson');
const pythonGrammarPath = resolve(
'language-python/grammars/modern-tree-sitter-python.cson'
);
const jsGrammarPath = resolve(
- 'language-javascript/grammars/tree-sitter-2-javascript.cson'
+ 'language-javascript/grammars/modern-tree-sitter-javascript.cson'
);
const jsRegexGrammarPath = resolve(
- 'language-javascript/grammars/tree-sitter-2-regex.cson'
+ 'language-javascript/grammars/modern-tree-sitter-regex.cson'
);
const jsdocGrammarPath = resolve(
- 'language-javascript/grammars/tree-sitter-2-jsdoc.cson'
+ 'language-javascript/grammars/modern-tree-sitter-jsdoc.cson'
);
const htmlGrammarPath = resolve(
'language-html/grammars/modern-tree-sitter-html.cson'
@@ -39,7 +44,7 @@ const ejsGrammarPath = resolve(
'language-html/grammars/modern-tree-sitter-ejs.cson'
);
const rubyGrammarPath = resolve(
- 'language-ruby/grammars/tree-sitter-2-ruby.cson'
+ 'language-ruby/grammars/modern-tree-sitter-ruby.cson'
);
const rustGrammarPath = resolve(
'language-rust-bundled/grammars/modern-tree-sitter-rust.cson'
@@ -52,9 +57,7 @@ let rubyConfig = CSON.readFileSync(rubyGrammarPath);
let htmlConfig = CSON.readFileSync(htmlGrammarPath);
function wait(ms) {
- return new Promise((resolve) => {
- setTimeout(resolve, ms);
- });
+ return new Promise((resolve) => setTimeout(resolve, ms));
}
describe('WASMTreeSitterLanguageMode', () => {
@@ -66,7 +69,6 @@ describe('WASMTreeSitterLanguageMode', () => {
buffer = editor.getBuffer();
editor.displayLayer.reset({ foldCharacter: 'β¦' });
atom.config.set('core.useTreeSitterParsers', true);
- atom.config.set('core.useExperimentalModernTreeSitter', true);
});
afterEach(() => {
@@ -590,9 +592,7 @@ describe('WASMTreeSitterLanguageMode', () => {
]
]);
- console.log('adding: ()');
buffer.setTextInRange([[0, 3], [0, 3]], '()');
- console.log('done: ()');
expectTokensToEqual(editor, [
[
@@ -601,9 +601,7 @@ describe('WASMTreeSitterLanguageMode', () => {
]
]);
- console.log('adding: new');
buffer.setTextInRange([[0, 0], [0, 0]], 'new ');
- console.log('done: new');
expectTokensToEqual(editor, [
[
@@ -616,7 +614,6 @@ describe('WASMTreeSitterLanguageMode', () => {
await nextHighlightingUpdate(languageMode);
// await wait(0);
// await languageMode.atTransactionEnd();
- console.log('proceeding!');
expectTokensToEqual(editor, [
[
@@ -1034,6 +1031,32 @@ describe('WASMTreeSitterLanguageMode', () => {
]);
});
+ it('handles injections with no highlights query', async () => {
+ jasmine.useRealClock();
+ atom.grammars.addGrammar(jsGrammar);
+ atom.grammars.addGrammar(htmlGrammar);
+ htmlGrammar.highlightsQuery = false;
+ // Pretend this grammar doesn't have a highlights query.
+ spyOn(htmlGrammar, 'getQuery').andReturn(Promise.resolve(null));
+ const languageMode = new WASMTreeSitterLanguageMode({
+ grammar: jsGrammar,
+ buffer,
+ config: atom.config,
+ grammars: atom.grammars
+ });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ buffer.setText('text = html`
`');
+ await languageMode.atTransactionEnd();
+
+ // An injection should still be able to add its root scope even when
+ // its grammar has no `highlightsQuery`.
+ let descriptor = editor.scopeDescriptorForBufferPosition([0, 15]);
+
+ expect(descriptor.getScopesArray()).toContain('text.html.basic');
+ });
+
it('terminates comment token at the end of an injection, so that the next injection is NOT a continuation of the comment', async () => {
jasmine.useRealClock();
const ejsGrammar = new WASMTreeSitterGrammar(
@@ -1410,6 +1433,73 @@ describe('WASMTreeSitterLanguageMode', () => {
).toBe(true);
});
+ it('allows multiple base scopes on the injected layer when `languageScope` is a function', async () => {
+
+ let customJsConfig = { ...jsConfig };
+ let customJsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, customJsConfig);
+
+ await jsGrammar.setQueryForTest('highlightsQuery', `
+ (comment) @comment
+ (property_identifier) @property
+ (call_expression (identifier) @function)
+ (template_string) @string
+ (template_substitution
+ ["\${" "}"] @interpolation)
+ `);
+
+ let customHtmlConfig = { ...htmlConfig };
+ let customHtmlGrammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, customHtmlConfig);
+
+ await htmlGrammar.setQueryForTest('highlightsQuery', `
+ (fragment) @html
+ (tag_name) @tag
+ (attribute_name) @attr
+ `);
+
+ customHtmlGrammar.addInjectionPoint({
+ ...SCRIPT_TAG_INJECTION_POINT,
+ languageScope: (grammar, _buffer, range) => {
+ return [grammar.scopeName, `meta.line${range.start.row}`];
+ }
+ });
+
+ jasmine.useRealClock();
+ atom.grammars.addGrammar(customJsGrammar);
+ atom.grammars.addGrammar(customHtmlGrammar);
+ buffer.setText('\n\n
\n');
+
+ const languageMode = new WASMTreeSitterLanguageMode({
+ grammar: customHtmlGrammar,
+ buffer,
+ config: atom.config,
+ grammars: atom.grammars
+ });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ let descriptor = languageMode.scopeDescriptorForPosition([1, 1]);
+ expect(
+ descriptor.getScopesArray().includes('source.js')
+ ).toBe(true);
+ expect(
+ descriptor.getScopesArray().includes(`meta.line0`)
+ ).toBe(true);
+ expect(
+ descriptor.getScopesArray().includes(`meta.line5`)
+ ).toBe(false);
+
+ descriptor = languageMode.scopeDescriptorForPosition([6, 1]);
+ expect(
+ descriptor.getScopesArray().includes('source.js')
+ ).toBe(true);
+ expect(
+ descriptor.getScopesArray().includes(`meta.line5`)
+ ).toBe(true);
+ expect(
+ descriptor.getScopesArray().includes(`meta.line0`)
+ ).toBe(false);
+ });
+
it('notifies onDidTokenize listeners the first time all syntax highlighting is done', async () => {
const promise = new Promise(resolve => {
editor.onDidTokenize(event => {
@@ -1610,7 +1700,12 @@ describe('WASMTreeSitterLanguageMode', () => {
});
describe('.suggestedIndentForBufferRows', () => {
+ beforeEach(async () => {
+ await atom.packages.activatePackage('language-javascript');
+ })
+
it('works correctly when straddling an injection boundary', async () => {
+
const jsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
jsGrammar.addInjectionPoint(HTML_TEMPLATE_LITERAL_INJECTION_POINT);
@@ -1628,9 +1723,16 @@ describe('WASMTreeSitterLanguageMode', () => {
// `suggestedIndentForBufferRows` should use the HTML grammar to
// determine the indent level of `let foo` rather than the JS grammar.
+ //
+ // And on line 5, it should use the JavaScript grammar to determine
+ // ``'s _initial_ indentation level, but the HTML grammar to
+ // determine whether to dedent relative to that initial level.
buffer.setText(dedent`
`);
@@ -1644,9 +1746,9 @@ describe('WASMTreeSitterLanguageMode', () => {
buffer.setLanguageMode(languageMode);
await languageMode.ready;
- let map = languageMode.suggestedIndentForBufferRows(1, 1, editor.getTabLength());
+ let map = languageMode.suggestedIndentForBufferRows(0, 5, editor.getTabLength());
- expect(map.get(1)).toBe(1);
+ expect(Array.from(map.values())).toEqual([0, 1, 1, 2, 1, 0]);
});
});
@@ -1664,20 +1766,6 @@ describe('WASMTreeSitterLanguageMode', () => {
] @fold
`);
- // {
- // parser: 'tree-sitter-javascript',
- // folds: [
- // {
- // start: { type: '{', index: 0 },
- // end: { type: '}', index: -1 }
- // },
- // {
- // start: { type: '(', index: 0 },
- // end: { type: ')', index: -1 }
- // }
- // ]
- // }
-
buffer.setText(dedent`
module.exports =
class A {
@@ -1795,7 +1883,7 @@ describe('WASMTreeSitterLanguageMode', () => {
(#set! fold.endAt lastChild.previousSibling.endPosition))
((jsx_self_closing_element) @fold
- (#set! fold.endAt lastChild.previousSibling.startPosition))
+ (#set! fold.endAt lastChild.startPosition))
`);
buffer.setText(dedent`
@@ -1839,6 +1927,118 @@ describe('WASMTreeSitterLanguageMode', () => {
`);
});
+ it('updates its fold cache properly when `fold.invalidateOnChange` is specified', async () => {
+ const grammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, htmlConfig);
+
+ await grammar.setQueryForTest('foldsQuery', scm`
+ (element
+ (start_tag
+ (tag_name) @_IGNORE_
+ ">" @fold)
+ (#set! fold.endAt parent.parent.lastNamedChild.startPosition)
+ (#set! fold.adjustToEndOfPreviousRow true)
+ )
+
+ (element
+ (start_tag
+ (tag_name) @_IGNORE_) @fold
+ (#set! fold.invalidateOnChange true)
+ (#set! fold.endAt lastChild.startPosition)
+ (#set! fold.adjustToEndOfPreviousRow true))
+ `);
+
+ // This is almost the exact scenario that created the need for this
+ // predicate. Since we use `adjustToEndOfPreviousRow`, this fold won't be
+ // valid in the below scenario because it'd start and end on row 0.
+ buffer.setText(dedent`
+
+ hello
+ world
+
+ `);
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ expect(editor.isFoldableAtBufferRow(0)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(1)).toBe(true);
+ expect(editor.isFoldableAtBufferRow(2)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(3)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(4)).toBe(false);
+
+ editor.setCursorBufferPosition([1, 11]);
+ editor.insertText('\n');
+ await languageMode.atTransactionEnd();
+
+ // It's only after we make this edit β and `start_tag` ends on row 2
+ // instead of row 1 β that the fold becomes valid, since now the fold
+ // range will start at row 0 and end at row 1. But without
+ // `fold.invalidateOnChange`, we wouldn't know that the change on line 1
+ // could have any effect on whether row 0 was foldable.
+ expect(editor.getText()).toBe(dedent`
+
+ hello
+ world
+
+ `)
+
+ expect(editor.isFoldableAtBufferRow(0)).toBe(true);
+ expect(editor.isFoldableAtBufferRow(1)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(2)).toBe(true);
+ expect(editor.isFoldableAtBufferRow(3)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(4)).toBe(false);
+ });
+
+ it('understands custom predicates', async () => {
+ const grammar = new WASMTreeSitterGrammar(atom.grammars, htmlGrammarPath, htmlConfig);
+
+ await grammar.setQueryForTest('foldsQuery', scm`
+ ((element
+ (start_tag
+ (tag_name) @_IGNORE_.tag)) @_IGNORE_.element
+ (#eq? @_IGNORE_.tag "div")
+ (#set! isDiv true))
+
+ ; Make self-closing elements foldable only when they're ancestors of
+ ; DIVs. This is a very silly thing to do.
+ ((element
+ (start_tag
+ (tag_name) @_IGNORE_) @fold)
+ (#match? @_IGNORE_ "^(area|base|br|col|embed|hr|img|input|keygen|link|meta|param|source|track|wbr)$")
+ (#set! test.descendantOfNodeWithData "isDiv")
+ (#set! capture.final)
+ )
+
+ `);
+
+ buffer.setText(dedent`
+
+
+
+
+
+ `);
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ expect(editor.isFoldableAtBufferRow(0)).toBe(false);
+ expect(editor.isFoldableAtBufferRow(7)).toBe(true);
+ });
+
it('can fold entire nodes when no start or end parameters are specified', async () => {
const grammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
@@ -3038,6 +3238,15 @@ describe('WASMTreeSitterLanguageMode', () => {
});
describe('.commentStringsForPosition(position)', () => {
+ beforeEach(() => {
+ atom.config.unset('editor.commentDelimiters', { scopeSelector: '.source.js' });
+ atom.config.unset('editor.commentStart', { scopeSelector: '.source.js' });
+ atom.config.unset('editor.commentEnd', { scopeSelector: '.source.js' });
+ atom.config.unset('editor.commentDelimiters', { scopeSelector: '.text.html.basic' });
+ atom.config.unset('editor.commentStart', { scopeSelector: '.text.html.basic' });
+ atom.config.unset('editor.commentEnd', { scopeSelector: '.text.html.basic' });
+ });
+
it('returns the correct comment strings for nested languages', async () => {
jasmine.useRealClock();
const jsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
@@ -3055,6 +3264,132 @@ describe('WASMTreeSitterLanguageMode', () => {
atom.grammars.addGrammar(jsGrammar);
atom.grammars.addGrammar(htmlGrammar);
+ atom.config.set(
+ 'editor.commentDelimiters',
+ {
+ line: '//',
+ block: ['/*', '*/']
+ },
+ { scopeSelector: '.source.js' }
+ );
+
+ atom.config.set(
+ 'editor.commentStart',
+ '//',
+ { scopeSelector: '.source.js' }
+ );
+
+
+ atom.config.set(
+ 'editor.commentDelimiters',
+ {
+ block: ['']
+ },
+ { scopeSelector: '.text.html.basic' }
+ );
+
+ atom.config.set(
+ 'editor.commentStart',
+ '',
+ { scopeSelector: '.text.html.basic' }
+ );
+
+ const languageMode = new WASMTreeSitterLanguageMode({
+ grammar: htmlGrammar,
+ buffer,
+ config: atom.config,
+ grammars: atom.grammars
+ });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ buffer.setText(
+ `
+hi
+
+ `.trim()
+ );
+
+ const htmlCommentStrings = {
+ commentStartString: '',
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ };
+ const jsCommentStrings = {
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: '//',
+ block: ['/*', '*/']
+ }
+ };
+
+ // Needs a short delay to allow injection grammars to be loaded.
+ await languageMode.nextTransaction;
+
+ expect(languageMode.commentStringsForPosition(new Point(0, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(1, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(2, 0))).toEqual(
+ jsCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(3, 0))).toEqual(
+ jsCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(4, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual({
+ // This is the curveball. Original position is inside the HTML, so
+ // that's what the delimiters will be. But `commentStartString` will be
+ // `// ` because it looks up the scope of the first non-whitespace
+ // content on the row.
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ });
+ expect(languageMode.commentStringsForPosition(new Point(6, 0))).toEqual(
+ htmlCommentStrings
+ );
+ });
+
+ it('uses grammar comment settings when config data is missing', async () => {
+ jasmine.useRealClock();
+ const jsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
+
+ jsGrammar.addInjectionPoint(HTML_TEMPLATE_LITERAL_INJECTION_POINT);
+
+ const htmlGrammar = new WASMTreeSitterGrammar(
+ atom.grammars,
+ htmlGrammarPath,
+ htmlConfig
+ );
+
+ htmlGrammar.addInjectionPoint(SCRIPT_TAG_INJECTION_POINT);
+
+ atom.grammars.addGrammar(jsGrammar);
+ atom.grammars.addGrammar(htmlGrammar);
+
const languageMode = new WASMTreeSitterLanguageMode({
grammar: htmlGrammar,
buffer,
@@ -3077,12 +3412,20 @@ describe('WASMTreeSitterLanguageMode', () => {
);
const htmlCommentStrings = {
- commentStartString: ''
+ commentStartString: '',
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
};
const jsCommentStrings = {
- commentStartString: '// ',
- commentEndString: undefined
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: '//',
+ block: ['/*', '*/']
+ }
};
// Needs a short delay to allow injection grammars to be loaded.
@@ -3103,13 +3446,133 @@ describe('WASMTreeSitterLanguageMode', () => {
expect(languageMode.commentStringsForPosition(new Point(4, 0))).toEqual(
htmlCommentStrings
);
- expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual(
- jsCommentStrings
- );
+ expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual({
+ // This is the curveball. Original position is inside the HTML, so
+ // that's what the delimiters will be. But `commentStartString` will be
+ // `// ` because it looks up the scope of the first non-whitespace
+ // content on the row.
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ });
expect(languageMode.commentStringsForPosition(new Point(6, 0))).toEqual(
htmlCommentStrings
);
});
+
+ it('constructs the right comment settings when grammar data is missing', async () => {
+ jasmine.useRealClock();
+ const jsGrammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
+
+ jsGrammar.addInjectionPoint(HTML_TEMPLATE_LITERAL_INJECTION_POINT);
+
+ const htmlGrammar = new WASMTreeSitterGrammar(
+ atom.grammars,
+ htmlGrammarPath,
+ htmlConfig
+ );
+
+ spyOn(jsGrammar, 'getCommentDelimiters').andReturn({ line: undefined, block: undefined });
+ spyOn(htmlGrammar, 'getCommentDelimiters').andReturn({ line: undefined, block: undefined });
+
+ atom.config.set(
+ 'editor.commentDelimiters',
+ {
+ line: '//',
+ block: ['/*', '*/']
+ },
+ { scopeSelector: '.source.js' }
+ );
+
+ atom.config.set(
+ 'editor.commentDelimiters',
+ {
+ block: ['']
+ },
+ { scopeSelector: '.text.html.basic' }
+ );
+
+ htmlGrammar.addInjectionPoint(SCRIPT_TAG_INJECTION_POINT);
+
+ atom.grammars.addGrammar(jsGrammar);
+ atom.grammars.addGrammar(htmlGrammar);
+
+ const languageMode = new WASMTreeSitterLanguageMode({
+ grammar: htmlGrammar,
+ buffer,
+ config: atom.config,
+ grammars: atom.grammars
+ });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ buffer.setText(
+ `
+hi
+
+ `.trim()
+ );
+
+ const htmlCommentStrings = {
+ commentStartString: '',
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ };
+ const jsCommentStrings = {
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: '//',
+ block: ['/*', '*/']
+ }
+ };
+
+ // Needs a short delay to allow injection grammars to be loaded.
+ await languageMode.nextTransaction;
+
+ expect(languageMode.commentStringsForPosition(new Point(0, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(1, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(2, 0))).toEqual(
+ jsCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(3, 0))).toEqual(
+ jsCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(4, 0))).toEqual(
+ htmlCommentStrings
+ );
+ expect(languageMode.commentStringsForPosition(new Point(5, 0))).toEqual({
+ // This is the curveball. Original position is inside the HTML, so
+ // that's what the delimiters will be. But `commentStartString` will be
+ // `// ` because it looks up the scope of the first non-whitespace
+ // content on the row.
+ commentStartString: '//',
+ commentEndString: undefined,
+ commentDelimiters: {
+ line: undefined,
+ block: ['']
+ }
+ });
+ expect(languageMode.commentStringsForPosition(new Point(6, 0))).toEqual(
+ htmlCommentStrings
+ );
+ });
+
});
describe('TextEditor.selectLargerSyntaxNode and .selectSmallerSyntaxNode', () => {
@@ -3120,11 +3583,6 @@ describe('WASMTreeSitterLanguageMode', () => {
(program) @source
`);
- // {
- // parser: 'tree-sitter-javascript',
- // scopes: { program: 'source' }
- // });
-
buffer.setText(dedent`
function a (b, c, d) {
eee.f()
@@ -3680,7 +4138,6 @@ describe('WASMTreeSitterLanguageMode', () => {
expect(editor.getText()).toEqual(emptyClassText);
});
- // This test is known to fail (and expected to fail) without async-indent enabled.
it('auto-indents correctly if any change in a transaction wants auto-indentation', async () => {
jasmine.useRealClock();
const grammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
@@ -3704,20 +4161,47 @@ describe('WASMTreeSitterLanguageMode', () => {
buffer.setText(emptyClassText);
const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ // Force this test to use async indent in all cases.
+ languageMode.transactionReparseBudgetMs = 0;
+ languageMode.currentTransactionReparseBudgetMs = 0;
buffer.setLanguageMode(languageMode);
await languageMode.ready;
await wait(0);
- editor.setCursorBufferPosition([1, 0]);
+ spyOn(languageMode, 'suggestedIndentForBufferRows').andCallThrough();
+
+ editor.setCursorBufferPosition([0, 15]);
editor.transact(() => {
- editor.insertText('// this is a comment', { autoIndent: true });
+ // This is a transaction in which each indentation decision is
+ // contingent on the previous indentation decisions that have been
+ // made. But in async indent mode, we cannot make any indentation
+ // suggestions until the end of the transaction.
+ //
+ // Still, in some scenarios, this will be OK. Either we can make each
+ // of these indentation decisions in order once the transaction is
+ // done, or we can give up and auto-indent the whole range covered by
+ // the transaction when we're done.
+ //
+ // This is an imperfect heuristic and won't produce good results in
+ // many cases, which is why we flip to async indent reluctantly and
+ // only in certain scenarios. But it's better than committing to N
+ // re-parses (where N equals the number of indentation suggestions
+ // we're asked to make during a given transaction) no matter how high N
+ // may be. And it's also better than performing no indentation at all
+ // in these cases.
editor.insertNewline();
- editor.insertText('// and this is another', { autoIndent: true });
+ editor.insertText('// this is a comment', { autoIndent: true, autoDecreaseIndent: true });
+ editor.insertNewline();
+ editor.insertText('// and this is another', { autoIndent: true, autoDecreaseIndent: true });
editor.insertNewline();
});
await wait(0);
+ expect(
+ languageMode.suggestedIndentForBufferRows
+ ).toHaveBeenCalled();
+
expect(editor.lineTextForBufferRow(1)).toEqual(
` // this is a comment`
);
@@ -3803,10 +4287,12 @@ describe('WASMTreeSitterLanguageMode', () => {
buffer.setLanguageMode(languageMode);
await languageMode.ready;
await wait(0);
-
- editor.setCursorBufferPosition([2, 4]);
+ editor.setCursorBufferPosition([2, 0]);
+ editor.insertText(' ');
+ await wait(0);
editor.insertText('}', { autoIndent: true });
+ await languageMode.atTransactionEnd();
await wait(0);
expect(editor.lineTextForBufferRow(2)).toEqual(` }`);
@@ -3873,7 +4359,6 @@ describe('WASMTreeSitterLanguageMode', () => {
["}"] @dedent
`);
- // let textToPaste = `// this is a comment\n // and this is another`;
let textToPaste = `a comment`;
buffer.setText(textToPaste);
@@ -3918,43 +4403,101 @@ describe('WASMTreeSitterLanguageMode', () => {
await grammar.setQueryForTest('indentsQuery', `
["{"] @indent
["}"] @dedent
- `);
+ `);
- let emptyClassText = dedent`
- class Example {
+ let emptyClassText = dedent`
+ class Example {
- }
- `;
+ }
+ `;
- buffer.setText(emptyClassText);
+ buffer.setText(emptyClassText);
- const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
- buffer.setLanguageMode(languageMode);
- await languageMode.ready;
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
- editor.setCursorBufferPosition([1, 0]);
- editor.indent();
- await languageMode.atTransactionEnd();
- editor.insertText('// this is a comment', { autoIndent: true });
- await languageMode.atTransactionEnd();
- expect(editor.lineTextForBufferRow(1)).toEqual(' // this is a comment');
+ editor.setCursorBufferPosition([1, 0]);
+ editor.indent();
+ await languageMode.atTransactionEnd();
+ editor.insertText('// this is a comment', { autoIndent: true });
+ await languageMode.atTransactionEnd();
+ expect(editor.lineTextForBufferRow(1)).toEqual(' // this is a comment');
- editor.insertNewline();
- await languageMode.atTransactionEnd();
- await wait(0);
- expect(editor.lineTextForBufferRow(2)).toEqual(' ');
+ editor.insertNewline();
+ await languageMode.atTransactionEnd();
+ await wait(0);
+ expect(editor.lineTextForBufferRow(2)).toEqual(' ');
- editor.insertNewline();
- await languageMode.atTransactionEnd();
- await wait(0);
- expect(editor.lineTextForBufferRow(3)).toEqual(' ');
+ editor.insertNewline();
+ await languageMode.atTransactionEnd();
+ await wait(0);
+ expect(editor.lineTextForBufferRow(3)).toEqual(' ');
- editor.insertNewline();
- await languageMode.atTransactionEnd();
- await wait(0);
- expect(editor.lineTextForBufferRow(4)).toEqual(' ');
+ editor.insertNewline();
+ await languageMode.atTransactionEnd();
+ await wait(0);
+ expect(editor.lineTextForBufferRow(4)).toEqual(' ');
});
+ it(`can indent properly in a multi-cursor environment without auto-indenting large ranges of the buffer`, async () => {
+ jasmine.useRealClock();
+ const grammar = new WASMTreeSitterGrammar(atom.grammars, jsGrammarPath, jsConfig);
+
+ expect(editor.getUndoGroupingInterval()).toBe(300);
+
+ await grammar.setQueryForTest('indentsQuery', `
+ ["{"] @indent
+ ["}"] @dedent
+ `);
+
+ const languageMode = new WASMTreeSitterLanguageMode({ grammar, buffer });
+ // Force this test to use async indent in all cases.
+ languageMode.transactionReparseBudgetMs = 0;
+ languageMode.currentTransactionReparseBudgetMs = 0;
+ spyOn(languageMode, 'suggestedIndentForBufferRows').andCallThrough();
+ buffer.setLanguageMode(languageMode);
+ await languageMode.ready;
+
+ // No spaces after the `{`s in these examples so that we can more easily
+ // compare expected output to actual output.
+ buffer.setText(dedent`
+ function test () {return }
+
+ function test () {return }
+
+ function test () {return }
+ `);
+
+ expect(
+ languageMode.suggestedIndentForBufferRows
+ ).not.toHaveBeenCalled();
+
+ editor.setCursorBufferPosition([0, 18])
+ editor.addCursorAtBufferPosition([2, 18])
+ editor.addCursorAtBufferPosition([4, 18])
+
+ editor.insertNewline({
+ autoIndent: true,
+ autoIndentNewline: true,
+ autoDecreaseIndent: true
+ })
+
+ await wait(0);
+
+ expect(buffer.getText()).toBe(dedent`
+ function test () {
+ return }
+
+ function test () {
+ return }
+
+ function test () {
+ return }
+ `)
+
+ })
+
});
});
diff --git a/spec/workspace-spec.js b/spec/workspace-spec.js
index 6cb0fce50..c8e0c451e 100644
--- a/spec/workspace-spec.js
+++ b/spec/workspace-spec.js
@@ -1983,63 +1983,65 @@ describe('Workspace', () => {
});
});
- it('stores the active grammars used by all the open editors', () => {
- waitsForPromise(() => atom.packages.activatePackage('language-javascript'));
+ it('stores the active grammars used by all the open editors', async () => {
+ await Promise.all([
+ atom.packages.activatePackage('language-javascript'),
+ atom.packages.activatePackage('language-coffee-script'),
+ atom.packages.activatePackage('language-todo'),
+ ]);
- waitsForPromise(() =>
- atom.packages.activatePackage('language-coffee-script')
+ await atom.workspace.open('sample.coffee');
+
+ atom.workspace.getActiveTextEditor().setText(dedent`
+ i = /test/; #FIXME\
+ `);
+
+ const atom2 = new AtomEnvironment({
+ applicationDelegate: atom.applicationDelegate
+ });
+ atom2.initialize({
+ window: document.createElement('div'),
+ document: Object.assign(document.createElement('div'), {
+ body: document.createElement('div'),
+ head: document.createElement('div')
+ })
+ });
+
+ atom2.packages.loadPackage('language-javascript');
+ atom2.packages.loadPackage('language-coffee-script');
+ atom2.packages.loadPackage('language-todo');
+ atom2.project.deserialize(atom.project.serialize());
+ atom2.workspace.deserialize(
+ atom.workspace.serialize(),
+ atom2.deserializers
);
- waitsForPromise(() => atom.packages.activatePackage('language-todo'));
+ let grammars = atom2.grammars
+ .getGrammars({ includeTreeSitter: true });
- waitsForPromise(() => atom.workspace.open('sample.coffee'));
+ let grammarScopes = grammars.map(grammar => grammar.scopeName).sort();
- runs(() => {
- atom.workspace.getActiveTextEditor().setText(dedent`
- i = /test/; #FIXME\
- `);
+ expect(
+ grammarScopes
+ ).toEqual([
+ 'source.coffee',
+ 'source.js', // Tree-sitter grammars also load
+ 'source.js',
+ 'source.js',
+ 'source.js.regexp',
+ 'source.js.regexp',
+ 'source.js.regexp',
+ 'source.js.regexp.replacement',
+ 'source.jsdoc',
+ 'source.jsdoc',
+ 'source.jsdoc',
+ 'source.litcoffee',
+ 'text.plain.null-grammar',
+ 'text.todo',
+ 'text.todo'
+ ]);
- const atom2 = new AtomEnvironment({
- applicationDelegate: atom.applicationDelegate
- });
- atom2.initialize({
- window: document.createElement('div'),
- document: Object.assign(document.createElement('div'), {
- body: document.createElement('div'),
- head: document.createElement('div')
- })
- });
-
- atom2.packages.loadPackage('language-javascript');
- atom2.packages.loadPackage('language-coffee-script');
- atom2.packages.loadPackage('language-todo');
- atom2.project.deserialize(atom.project.serialize());
- atom2.workspace.deserialize(
- atom.workspace.serialize(),
- atom2.deserializers
- );
-
- expect(
- atom2.grammars
- .getGrammars({ includeTreeSitter: true })
- .map(grammar => grammar.scopeName)
- .sort()
- ).toEqual([
- 'source.coffee',
- 'source.js', // Tree-sitter grammars also load
- 'source.js',
- 'source.js.regexp',
- 'source.js.regexp',
- 'source.js.regexp.replacement',
- 'source.jsdoc',
- 'source.jsdoc',
- 'source.litcoffee',
- 'text.plain.null-grammar',
- 'text.todo'
- ]);
-
- atom2.destroy();
- });
+ atom2.destroy();
});
describe('document.title', () => {
@@ -2278,11 +2280,11 @@ describe('Workspace', () => {
Ctor.prototype = HTMLElement.prototype;
TestItemElement.prototype = new Ctor();
TestItemElement.__super__ = HTMLElement.prototype;
- TestItemElement.prototype.initialize = function(model) {
+ TestItemElement.prototype.initialize = function (model) {
this.model = model;
return this;
};
- TestItemElement.prototype.getModel = function() {
+ TestItemElement.prototype.getModel = function () {
return this.model;
};
diff --git a/src/atom-environment.js b/src/atom-environment.js
index cf20b2c87..510970615 100644
--- a/src/atom-environment.js
+++ b/src/atom-environment.js
@@ -46,6 +46,7 @@ const TextBuffer = require('text-buffer');
const TextEditorRegistry = require('./text-editor-registry');
const StartupTime = require('./startup-time');
const { getReleaseChannel } = require('./get-app-details.js');
+const UI = require('./ui.js');
const packagejson = require("../package.json");
const stat = util.promisify(fs.stat);
@@ -226,6 +227,8 @@ class AtomEnvironment {
urlCoreRepo: packagejson.repository.url
};
+ this.ui = UI;
+
// Keep instances of HistoryManager in sync
this.disposables.add(
this.history.onDidChangeProjects(event => {
diff --git a/src/comment-utils.js b/src/comment-utils.js
new file mode 100644
index 000000000..bf6a01682
--- /dev/null
+++ b/src/comment-utils.js
@@ -0,0 +1,85 @@
+
+// Accept a comment metadata block and make it more consistent and predictable.
+//
+// The `comments` key in a grammar definition file has historically contained
+// some comment metadata under the `start` and `end` properties, but only as
+// much as was needed for the βToggle Line Commentβ command. Newer convention
+// is to define `line` (`string`) and `block` (`[string, string]`) properties
+// to reflect all comment metadata for the language.
+//
+// Some grammars also specify block comments as `{ start: string, end: string
+// }`, so we'll normalize that to the `[string, string]` variant.
+function normalizeDelimiters(meta = {}) {
+ // Adapt the style used in `TreeSitterGrammar`.
+ if (
+ ('commentStartString' in meta && 'commentEndString' in meta) && !('line' in meta || 'block' in meta)
+ ) {
+ let { commentStartString: start, commentEndString: end } = meta;
+ meta = { start, end };
+ }
+ let { line, block } = meta;
+ // Normalize the `{ start: string, end: string }` version to `[string,
+ // string].`
+ if (block && (!Array.isArray(block))) {
+ let { start, end } = block;
+ block = [start, end];
+ }
+
+ // Our preferred properties are `line` and `block`, but if `start` and/or
+ // `end` are present, we can extract some value out of them.
+
+ // If `start` and `end` both exist, they must identify block delimiters.
+ if (!block && meta.start && meta.end) {
+ block = [meta.start.trim(), meta.end.trim()];
+ }
+ // If `start` exists but `end` does not, `start` must be a line delimiter.
+ if (!line && meta.start && !meta.end) {
+ line = meta.start.trim();
+ }
+
+ return { line, block };
+}
+
+// Convert comment delimiter metadata to the format expected by
+// `LanguageMode::getCommentStringsForPosition`. We can act as a provider of
+// this data if the traditional sources are empty.
+function commentStringsFromDelimiters(meta) {
+ let { line, block } = normalizeDelimiters(meta);
+ let commentStartString;
+ let commentEndString;
+ let commentDelimiters = { line, block };
+ let blockIsValid = block != null && Array.isArray(block);
+ let lineIsValid = typeof line === 'string';
+ if (lineIsValid || blockIsValid) {
+ commentDelimiters = { line, block };
+ if (lineIsValid) {
+ // The βToggle Line Commentβ command obviously prefers a line comment if
+ // one is present.
+ commentStartString = line;
+ } else if (blockIsValid) {
+ [commentStartString, commentEndString] = block;
+ }
+ }
+ let result = { commentStartString, commentEndString, commentDelimiters };
+ return result;
+}
+
+
+
+// Given a scope, return a single object of `editor.commentDelimiters` data.
+// Needed because an ordinary config lookup will βblendβ objects from cascading
+// scopes β which is usually the behavior we want! Just not this time.
+function getDelimitersForScope(scope) {
+ let reversed = [...scope.scopes].reverse();
+ let mapped = reversed.map(scope => {
+ return atom.config.get('editor.commentDelimiters', { scope: [scope] })
+ })
+ let result = mapped.find(setting => !!setting)
+ return result ? normalizeDelimiters(result) : result
+}
+
+module.exports = {
+ normalizeDelimiters,
+ commentStringsFromDelimiters,
+ getDelimitersForScope
+};
diff --git a/src/config-schema.js b/src/config-schema.js
index 29108d6a9..fd038634f 100644
--- a/src/config-schema.js
+++ b/src/config-schema.js
@@ -360,13 +360,14 @@ const configSchema = {
useTreeSitterParsers: {
type: 'boolean',
default: true,
+ title: 'Use Tree-sitter Parsers',
description: 'Use Tree-sitter parsers for supported languages.'
},
- useExperimentalModernTreeSitter: {
+ useLegacyTreeSitter: {
type: 'boolean',
default: false,
- title: 'Use Modern Tree-Sitter Implementation',
- description: 'Experimental: Use the new query-file-based Tree-sitter system instead of the legacy system from Atom. (This system will eventually replace the legacy system.) Has no effect unless "Use Tree Sitter Parsers" is also checked.'
+ title: 'Use Legacy Tree-sitter Implementation',
+ description: 'Opt into the legacy Atom Tree-sitter system instead of the modern system added by Pulsar. (We plan to remove this legacy system soon.) Has no effect unless βUse Tree-sitter Parsersβ is also checked.'
},
colorProfile: {
description:
diff --git a/src/config.js b/src/config.js
index 2f808abf1..fa0d407fd 100644
--- a/src/config.js
+++ b/src/config.js
@@ -1098,8 +1098,23 @@ class Config {
!sources || sources.includes(this.projectFile)
)
) {
- const projectValue = getValueAtKeyPath(this.projectSettings, keyPath);
- value = projectValue === undefined ? value : projectValue;
+ let projectValue = getValueAtKeyPath(this.projectSettings, keyPath);
+ if (projectValue === undefined) {
+ // There is no project-specific override for this key path. `value`
+ // stays as `value` and we pretend this never happened.
+ } else if (isPlainObject(value) && isPlainObject(projectValue)) {
+ // This key path returned an object, so we need to merge the contents
+ // of the two objects into a third composite object. First we clone
+ // the project object so as not to modify itβ¦
+ projectValue = this.deepClone(projectValue);
+ // β¦then we copy over the regular value's properties, preferring the
+ // project-specific value wherever there is overlap.
+ this.deepDefaults(projectValue, value);
+ value = projectValue;
+ } else {
+ // This is a single value, so we prefer the project version.
+ value = projectValue;
+ }
}
}
diff --git a/src/grammar-registry.js b/src/grammar-registry.js
index f4852d70e..85d556f2a 100644
--- a/src/grammar-registry.js
+++ b/src/grammar-registry.js
@@ -46,7 +46,7 @@ module.exports = class GrammarRegistry {
this.textmateRegistry.onDidUpdateGrammar(grammarAddedOrUpdated);
let onLanguageModeChange = () => {
- this.grammarScoresByBuffer.forEach((score, buffer) => {
+ this.grammarScoresByBuffer.forEach((_score, buffer) => {
if (!this.languageOverridesByBufferId.has(buffer.id)) {
this.autoAssignLanguageMode(buffer);
}
@@ -55,7 +55,7 @@ module.exports = class GrammarRegistry {
this.subscriptions.add(
this.config.onDidChange('core.useTreeSitterParsers', onLanguageModeChange),
- this.config.onDidChange('core.useExperimentalModernTreeSitter', onLanguageModeChange)
+ this.config.onDidChange('core.useLegacyTreeSitter', onLanguageModeChange)
);
}
@@ -253,10 +253,10 @@ module.exports = class GrammarRegistry {
scope = new ScopeDescriptor({ scopes: [scope] })
}
let useTreeSitterParsers = this.config.get('core.useTreeSitterParsers', { scope });
- let useExperimentalModernTreeSitter = this.config.get('core.useExperimentalModernTreeSitter', { scope });
+ let useLegacyTreeSitter = this.config.get('core.useLegacyTreeSitter', { scope });
if (!useTreeSitterParsers) return 'textmate';
- return useExperimentalModernTreeSitter ? 'wasm-tree-sitter' : 'node-tree-sitter';
+ return useLegacyTreeSitter ? 'node-tree-sitter' : 'wasm-tree-sitter';
}
// Extended: Returns a {Number} representing how well the grammar matches the
@@ -288,20 +288,20 @@ module.exports = class GrammarRegistry {
if (isNewTreeSitter) {
if (parserConfig === 'wasm-tree-sitter') {
score += 0.1;
- } else {
- // Never automatically switch to a new Tree-sitter grammar unless the
- // user has opted into the experimental setting.
- score = -Infinity;
+ } else if (parserConfig === 'textmate') {
+ score = -1;
}
} else if (isOldTreeSitter) {
if (parserConfig === 'node-tree-sitter') {
score += 0.1;
} else if (parserConfig === 'wasm-tree-sitter') {
- // In experimental new-tree-sitter mode, we still would rather fall
- // back to an old-tree-sitter grammar than a TM grammar. Bump the
+ // If `useLegacyTreeSitter` isn't checked, we probably still prefer a
+ // legacy Tree-sitter grammar over a TextMate-style grammar. Bump the
// score, but just a bit less than we'd bump it if this were a
- // new-tree-sitter grammar.
+ // modern Tree-sitter grammar.
score += 0.09;
+ } else if (parserConfig === 'textmate') {
+ score = -1;
}
}
@@ -770,18 +770,16 @@ module.exports = class GrammarRegistry {
let result = this.textmateRegistry.getGrammars();
if (!(params && params.includeTreeSitter)) return result;
- let includeModernTreeSitterGrammars =
- atom.config.get('core.useExperimentalModernTreeSitter') === true;
-
- const tsGrammars = Object.values(this.treeSitterGrammarsById)
+ let modernTsGrammars = Object.values(this.wasmTreeSitterGrammarsById)
.filter(g => g.scopeName);
- result = result.concat(tsGrammars);
+ result = result.concat(modernTsGrammars);
- if (includeModernTreeSitterGrammars) {
- let modernTsGrammars = Object.values(this.wasmTreeSitterGrammarsById)
- .filter(g => g.scopeName);
- result = result.concat(modernTsGrammars);
- }
+ // We must include all legacy Tree-sitter grammars here just in case the
+ // user has opted into `useTreeSitterGrammars` via a scope-specific
+ // setting.
+ const legacyTsGrammars = Object.values(this.treeSitterGrammarsById)
+ .filter(g => g.scopeName);
+ result = result.concat(legacyTsGrammars);
return result;
}
@@ -790,8 +788,10 @@ module.exports = class GrammarRegistry {
return this.textmateRegistry.scopeForId(id);
}
- // TODO: why is this being used? Can we remove it soon?
- treeSitterGrammarForLanguageString(languageString, type = 'original') {
+ // Match up a language string (of the sort generated by an injection point)
+ // with a grammar. Checks the `injectionRegex` property on grammars and
+ // returns the one with the longest match.
+ treeSitterGrammarForLanguageString(languageString, type = 'wasm') {
let longestMatchLength = 0;
let grammarWithLongestMatch = null;
let table = type === 'original' ? this.treeSitterGrammarsById : this.wasmTreeSitterGrammarsById;
diff --git a/src/main-process/win-shell.js b/src/main-process/win-shell.js
index 05353d064..26060cdd6 100644
--- a/src/main-process/win-shell.js
+++ b/src/main-process/win-shell.js
@@ -74,27 +74,16 @@ class ShellOption {
}
class PathOption {
- constructor(installType) {
- // installType MUST be 'User' or 'Machine'
+ constructor() {
this.HKPATH;
this.hive;
this.installReg = "\\SOFTWARE\\0949b555-c22c-56b7-873a-a960bdefa81f";
- this.installMode = installType;
- if (installType === "User") {
- this.HKPATH = "\\Environment";
- this.hive = "HKCU";
- } else if (installType === "Machine") {
- this.HKPATH = "\\SYSTEM\\CurrentControlSet\\Control\\Session Manager\\Environment";
- this.hive = "HKLM";
- }
+ // We no longer support an `installType`
+ // Only managing the path of the current user
+ this.HKPATH = "\\Environment";
+ this.hive = "HKCU";
- // Unfortunately, we can only manage the PATH for a per user installation.
- // While the PowerShell script does support setting the PATH for a Machine
- // install, we can't yet check that.
- // https://github.com/fresc81/node-winreg/tree/1.2.1#troubleshooting
- // This can only be done if Pulsar is run as Admin, with a user with Admin privs
- // So we will pretend a user install is all that matters here
this.isRegistered = this.isRegistered.bind(this);
this.register = this.register.bind(this);
this.deregister = this.deregister.bind(this);
@@ -129,19 +118,24 @@ class PathOption {
register(callback) {
this.getPulsarPath().then((pulsarPath) => {
const child = ChildProcess.execFile(
- `${pulsarPath}\\resources\\modifyWindowsPath.ps1`,
- ['-installMode', this.installMode, '-installdir', `"${pulsarPath}"`, '-remove', '0'],
+ `powershell.exe -File '${pulsarPath}\\resources\\modifyWindowsPath.ps1'`,
+ ['-installdir', `'${pulsarPath}'`, '-remove', 'FALSE'],
{ shell: "powershell.exe" },
(error, stdout, stderr) =>
{
if (error) {
- atom.notifications.addError(`Error Running Script: ${error.toString()}`);
+ console.log(`Add Pulsar to PATH: ${error}`);
+ atom.notifications.addError(`Error Running Script: ${error.toString()}`, { dismissable: true });
callback(error);
} else {
+ console.log("Add Pulsar to PATH:");
+ console.log(`stdout: ${stdout}`);
+ console.log(`stderr: ${stderr}`);
return callback();
}
});
}).catch((err) => {
+ console.error(`Add Pulsar to PATH error caught: ${err}`);
return callback(err);
});
}
@@ -151,19 +145,24 @@ class PathOption {
if (isRegistered) {
this.getPulsarPath().then((pulsarPath) => {
const child = ChildProcess.execFile(
- `${pulsarPath}\\resources\\modifyWindowsPath.ps1`,
- ['-installMode', this.installMode, '-installdir', `"${pulsarPath}"`, '-remove', '1'],
+ `powershell.exe -File '${pulsarPath}\\resources\\modifyWindowsPath.ps1'`,
+ ['-installdir', `'${pulsarPath}'`, '-remove', 'TRUE'],
{ shell: "powershell.exe" },
(error, stdout, stderr) =>
{
if (error) {
- atom.notifications.addError(`Error Running Script: ${error.toString()}`);
+ console.error(`Remove Pulsar from PATH: ${error}`);
+ atom.notifications.addError(`Error Running Script: ${error.toString()}`, { dismissable: true });
callback(error);
} else {
+ console.log("Remove Pulsar from PATH:");
+ console.log(`stdout: ${stdout}`);
+ console.log(`stderr: ${stderr}`);
return callback();
}
});
}).catch((err) => {
+ console.error(`Remove Pulsar from PATH error caught: ${err}`);
return callback(err);
});
} else {
@@ -180,20 +179,29 @@ class PathOption {
key: this.installReg
}).get("InstallLocation", (err, val) => {
if (err) {
- reject(err);
+ console.error(err);
+ let location = process.resourcesPath;
+ if (typeof location !== "string" || location.length < 1) {
+ console.error(`Unable to locate Pulsar PATH via fallback methods: '${location}'`);
+ reject(err);
+ } else {
+ resolve(Path.dirname(location));
+ }
} else {
pulsarPath = val.value;
if (pulsarPath.length === 0) {
- reject("Unable to find Pulsar Install Path");
+ console.error("Unable to find Pulsar Install Path");
+ let location = process.resourcesPath;
+ if (typeof location !== "string" || location.length < 1) {
+ console.error(`Unable to locate Pulsar PATH via fallback methods: '${location}'`);
+ reject("Unable to find Pulsar Install Path");
+ } else {
+ resolve(Path.dirname(location));
+ }
}
- // When we are modifying Machine values, we can't accept spaces in the
- // path. There's likely some combination of escapes to fix this, but
- // I was unable to find them. For now we will check for the default
- // Machine install location, and remove the space.
- let safePulsarPath = pulsarPath.replace("Program Files", "PROGRA~1");
- resolve(safePulsarPath);
+ resolve(pulsarPath);
}
});
});
@@ -241,4 +249,3 @@ exports.folderBackgroundContextMenu = new ShellOption(
JSON.parse(JSON.stringify(contextParts).replace('%1', '%V'))
);
exports.pathUser = new PathOption("User");
-exports.pathMachine = new PathOption("Machine");
diff --git a/src/scope-resolver.js b/src/scope-resolver.js
index 89f317932..8e8df72fa 100644
--- a/src/scope-resolver.js
+++ b/src/scope-resolver.js
@@ -1,3 +1,4 @@
+const { CompositeDisposable } = require('event-kit');
const { Point } = require('text-buffer');
const ScopeDescriptor = require('./scope-descriptor');
@@ -16,7 +17,7 @@ function comparePoints(a, b) {
}
}
-function rangeSpecToString (range) {
+function rangeSpecToString(range) {
let [sp, ep] = [range.startPosition, range.endPosition];
return `(${sp.row}, ${sp.column}) - (${ep.row}, ${ep.column})`;
}
@@ -63,6 +64,71 @@ function interpretPossibleKeyValuePair(rawValue, coerceValue = false) {
return [key, value];
}
+// `ScopeResolver`s can share a config cache if they have the same grammar.
+// There are many such `ScopeResolver`s (because there are many such
+// `LanguageLayer`s), and this consolidation cuts down on the number of
+// `onDidChange` handlers (which are costly when observing all config values).
+class ConfigCache {
+ constructor(config) {
+ this.subscriptions = new CompositeDisposable();
+ this.cachesByGrammar = new Map();
+ this.config = config;
+
+ this.subscriptions.add(
+ this.config.onDidChange(() => this.clearAll()),
+ atom.grammars.onDidAddGrammar(() => this.clearAll()),
+ atom.grammars.onDidUpdateGrammar(() => this.clearAll())
+ );
+ }
+
+ dispose() {
+ this.subscriptions.dispose();
+ }
+
+ clearAll() {
+ for (let cache of this.cachesByGrammar.values()) {
+ cache.clear();
+ }
+ }
+
+ getCacheForGrammar(grammar) {
+ let { scopeName } = grammar;
+ // We key on the scope name rather than the grammar instance. We need to be
+ // able to iterate over grammars, so we can't use a `WeakSet` here, and we
+ // have no lifecycle event to keep the map from getting stale.
+ //
+ // To prevent two different incarnations of the same grammar (after
+ // disabling and reenabling) from incorrectly sharing a cache, we clear all
+ // caches whenever grammars are added or updated.
+ let cache = this.cachesByGrammar.get(scopeName);
+ if (!cache) {
+ cache = new Map();
+ this.cachesByGrammar.set(scopeName, cache);
+ }
+ return cache;
+ }
+}
+
+// We can technically have more than one configuration object, though in
+// practice this will only point to `atom.config`. We do it this way for ease
+// of testing (e.g., if a test mocks a config object) and to avoid silly hacks.
+ConfigCache.CACHES_FOR_CONFIG_OBJECTS = new Map();
+
+ConfigCache.forConfig = (config) => {
+ let { CACHES_FOR_CONFIG_OBJECTS } = ConfigCache;
+ let configCache = CACHES_FOR_CONFIG_OBJECTS.get(config);
+ if (!configCache) {
+ configCache = new ConfigCache(config);
+ CACHES_FOR_CONFIG_OBJECTS.set(config, configCache);
+ }
+ return configCache;
+};
+
+ConfigCache.clear = () => {
+ ConfigCache.CACHES_FOR_CONFIG_OBJECTS.clear();
+};
+
+
// A data structure for storing scope information while processing capture
// data. The data is reset in between each task.
//
@@ -86,10 +152,8 @@ class ScopeResolver {
this.rangeData = new Map;
this.pointKeyCache = new Map;
this.patternCache = new Map;
- this.configCache = new Map;
- this.configSubscription = this.config.onDidChange(() => {
- this.configCache.clear();
- });
+ this.configCache = ConfigCache.forConfig(this.config)
+ .getCacheForGrammar(this.grammar);
}
getOrCompilePattern(pattern) {
@@ -132,6 +196,11 @@ class ScopeResolver {
('highlight.invalidateOnChange' in capture.setProperties);
}
+ shouldInvalidateFoldOnChange(capture) {
+ return capture.setProperties &&
+ ('fold.invalidateOnChange' in capture.setProperties);
+ }
+
// We want to index scope data on buffer position, but each `Point` (or
// ad-hoc point object) is a different object. We could normalize them to a
// string and use the string as the map key, but we'd have to convert them
@@ -327,6 +396,11 @@ class ScopeResolver {
return range;
}
+ isFinal(existingData = {}) {
+ return ('capture.final' in existingData) ||
+ ('final' in existingData);
+ }
+
// Given a syntax capture, test whether we should include its scope in the
// document.
test(capture, existingData) {
@@ -337,7 +411,7 @@ class ScopeResolver {
refutedProperties: refuted = {}
} = capture;
- if (existingData?.final || existingData?.['capture.final']) {
+ if (this.isFinal(existingData)) {
return false;
}
@@ -418,16 +492,22 @@ class ScopeResolver {
this.setDataForRange(range, props);
}
- if (name === '_IGNORE_') {
+ if (name === '_IGNORE_' || name.startsWith('_IGNORE_.')) {
// "@_IGNORE_" is a magical variable in an SCM file that will not be
// applied in the grammar, but which allows us to prevent other kinds of
// scopes from matching. We purposefully allowed this syntax node to set
// data for a given range, but not to apply its scope ID to any
// boundaries.
+ //
+ // A query can also use multiple different @_IGNORE_-style variables by
+ // adding segments after the @_IGNORE_, such as @_IGNORE_.foo.bar.
return false;
}
- let id = this.idForScope(name);
+ let id = this.idForScope(
+ name,
+ node.childCount === 0 ? node.text : undefined,
+ );
let {
startPosition: start,
@@ -477,8 +557,6 @@ class ScopeResolver {
this.reset();
this.patternCache.clear();
this.pointKeyCache.clear();
- this.configCache.clear();
- this.configSubscription.dispose();
}
*[Symbol.iterator]() {
@@ -512,16 +590,16 @@ ScopeResolver.interpolateName = (name, node) => {
// Special `#set!` predicates that work on βclaimedβ and βunclaimedβ ranges.
ScopeResolver.CAPTURE_SETTINGS = {
// Passes only if another capture has not already declared `final` for the
- // exact same range. If a capture is the first one to define `final`, then
+ // exact same range. If a capture is the first one to define `exact`, then
// all other captures for that same range are ignored, whether they try to
// define `final` or not.
- final(node, existingData) {
- let final = existingData?.final || existingData?.['capture.final'];
- return !(existingData && final);
+ final(_node, existingData) {
+ if (!existingData) return true;
+ return !('capture.final' in existingData) && !('final' in existingData);
},
// Passes only if no earlier capture has occurred for the exact same range.
- shy(node, existingData) {
+ shy(_node, existingData) {
return existingData === undefined;
}
};
@@ -910,5 +988,9 @@ ScopeResolver.ADJUSTMENTS = {
}
};
+ScopeResolver.clearConfigCache = () => {
+ ConfigCache.clear();
+};
+
module.exports = ScopeResolver;
diff --git a/src/task.js b/src/task.js
index a20e1f226..8cbfb43ec 100644
--- a/src/task.js
+++ b/src/task.js
@@ -73,9 +73,9 @@ module.exports = class Task {
const env = Object.assign({}, process.env, {userAgent: navigator.userAgent});
this.childProcess = ChildProcess.fork(require.resolve('./task-bootstrap'), [compileCachePath, taskPath], { env, silent: true});
- this.on("task:log", () => console.log(...arguments));
- this.on("task:warn", () => console.warn(...arguments));
- this.on("task:error", () => console.error(...arguments));
+ this.on("task:log", (...args) => console.log(...args) );
+ this.on("task:warn", (...args) => console.warn(...args) );
+ this.on("task:error", (...args) => console.error(...args));
this.on("task:deprecations", (deprecations) => {
for (let i = 0; i < deprecations.length; i++) {
@@ -157,7 +157,7 @@ module.exports = class Task {
}
once(eventName, callback) {
- var disposable = this.on(eventName, function(...args) {
+ var disposable = this.on(eventName, function (...args) {
disposable.dispose();
callback(...args);
});
diff --git a/src/text-editor-component.js b/src/text-editor-component.js
index a60676aad..789fa2872 100644
--- a/src/text-editor-component.js
+++ b/src/text-editor-component.js
@@ -274,7 +274,7 @@ module.exports = class TextEditorComponent {
this.remeasureCharacterDimensions = false;
}
- this.measureBlockDecorations();
+ if (this.isVisible()) this.measureBlockDecorations();
this.updateSyncBeforeMeasuringContent();
if (useScheduler === true) {
@@ -1689,6 +1689,13 @@ module.exports = class TextEditorComponent {
}
didFocusHiddenInput() {
+ // Focusing the hidden input when it is off-screen causes the browser to
+ // scroll it into view. Since we use synthetic scrolling this behavior
+ // causes all the lines to disappear so we counteract it by always setting
+ // the scroll position to 0.
+ this.refs.scrollContainer.scrollTop = 0;
+ this.refs.scrollContainer.scrollLeft = 0;
+
if (!this.focused) {
this.focused = true;
this.startCursorBlinking();
@@ -5114,7 +5121,7 @@ function debounce(fn, wait) {
}
}
- return function() {
+ return function () {
timestamp = Date.now();
if (!timeout) timeout = setTimeout(later, wait);
};
diff --git a/src/text-editor.js b/src/text-editor.js
index bc711070d..8132eb841 100644
--- a/src/text-editor.js
+++ b/src/text-editor.js
@@ -5757,6 +5757,7 @@ module.exports = class TextEditor {
let languageMode = this.buffer.getLanguageMode();
if (!languageMode.atTransactionEnd) return;
+ if (!languageMode.useAsyncParsing || !languageMode.useAsyncIndent) return;
let promise = languageMode.atTransactionEnd().then(
({ range, autoIndentRequests }) => {
@@ -5924,6 +5925,60 @@ module.exports = class TextEditor {
}
}
+ // Public: Return information about the appropriate comment delimiters to use
+ // at a given point in the buffer.
+ //
+ // Pulsar allows language bundles to define comment delimiters in several
+ // places. For instance, a grammar author can place delimiter metadata in the
+ // grammar definition file, or as scope-specific settings in the ordinary
+ // config system β or a combination of the two.
+ //
+ // In some languages, comment delimiters vary based on position in the
+ // buffer. (For instance, line comments can't always be used in JavaScript
+ // JSX blocks, so block comments are much safer.) This method will look for
+ // any such overrides and return what it thinks are the best delimiters to
+ // use at a given point.
+ //
+ // Some languages don't specify all their delimiters in their configuration,
+ // but this method will return all the information that it can discern.
+ //
+ // * point - A {Point} or point-compatible {Array}.
+ //
+ // Returns an {Object} with the following properties:
+ //
+ // * `line`: If present, a {String} representing a line comment delimiter.
+ // (If `undefined`, there is no known line comment delimiter for the given
+ // buffer position.)
+ // * `block`: If present, a two-item {Array} containing {String}s
+ // representing the starting and ending block comment delimiters. (If
+ // `undefined`, there are no known block comment delimiters for the given
+ // buffer position.)
+ //
+ getCommentDelimitersForBufferPosition(point) {
+ point = Point.fromObject(point);
+ const languageMode = this.buffer.getLanguageMode();
+ let {
+ commentStartString,
+ commentEndString,
+ commentDelimiters
+ } = languageMode.commentStringsForPosition(point);
+ if (commentDelimiters) {
+ return commentDelimiters;
+ } else {
+ // Build a delimiters object out of the other data we received. The
+ // `commentStartString` and `commentEndString` settings aren't meant to
+ // be comprehensive β they just tell you which delimiter(s) to use to
+ // comment out a given selection β but they're better than nothing.
+ if (commentStartString && commentEndString) {
+ return { block: [commentStartString.trim(), commentEndString.trim()] };
+ } else if (commentStartString && !commentEndString) {
+ return { line: commentStartString.trim() };
+ } else {
+ return null;
+ }
+ }
+ }
+
rowRangeForParagraphAtBufferRow(bufferRow) {
if (!NON_WHITESPACE_REGEXP.test(this.lineTextForBufferRow(bufferRow)))
return;
diff --git a/src/text-mate-language-mode.js b/src/text-mate-language-mode.js
index aa1b52c9a..f5092b157 100644
--- a/src/text-mate-language-mode.js
+++ b/src/text-mate-language-mode.js
@@ -11,6 +11,7 @@ const {
fromFirstMateScopeId
} = require('./first-mate-helpers');
const { selectorMatchesAnyScope } = require('./selectors');
+const { normalizeDelimiters, commentStringsFromDelimiters } = require('./comment-utils.js');
const NON_WHITESPACE_REGEX = /\S/;
@@ -29,7 +30,7 @@ class TextMateLanguageMode {
this.id = params.id != null ? params.id : nextId++;
this.buffer = params.buffer;
this.largeFileMode = params.largeFileMode;
- this.config = params.config;
+ this.config = params.config ?? atom.config;
this.largeFileMode =
params.largeFileMode != null
? params.largeFileMode
@@ -236,10 +237,19 @@ class TextMateLanguageMode {
const commentEndEntry = commentEndEntries.find(entry => {
return entry.scopeSelector === commentStartEntry.scopeSelector;
});
- return {
- commentStartString: commentStartEntry && commentStartEntry.value,
- commentEndString: commentEndEntry && commentEndEntry.value
- };
+ // If a `commentDelimiters` setting exists, return it in its entirety. This
+ // can contain more comprehensive delimiter metadata for snippets and other
+ // purposes.
+ const commentDelimiters = this.config.get('editor.commentDelimiters', { scope });
+ if (commentStartEntry) {
+ return {
+ commentStartString: commentStartEntry && commentStartEntry.value,
+ commentEndString: commentEndEntry && commentEndEntry.value,
+ commentDelimiters: commentDelimiters && normalizeDelimiters(commentDelimiters)
+ };
+ } else if (commentDelimiters) {
+ return commentStringsFromDelimiters(commentDelimiters);
+ }
}
/*
diff --git a/src/tree-sitter-grammar.js b/src/tree-sitter-grammar.js
index e76b8aed4..794dc79b6 100644
--- a/src/tree-sitter-grammar.js
+++ b/src/tree-sitter-grammar.js
@@ -70,7 +70,9 @@ module.exports = class TreeSitterGrammar {
return `TreeSitterGrammar {scopeName: ${this.scopeName}}`;
}
- idForScope(scopeName) {
+ // Though _text is unused here, some packages (eg semanticolor) use it to
+ // customize scopes on the fly.
+ idForScope(scopeName, _text) {
if (!scopeName) {
return undefined;
}
diff --git a/src/tree-sitter-language-mode.js b/src/tree-sitter-language-mode.js
index 4e7837e5b..d4970bca8 100644
--- a/src/tree-sitter-language-mode.js
+++ b/src/tree-sitter-language-mode.js
@@ -8,6 +8,7 @@ const TokenizedLine = require('./tokenized-line');
const TextMateLanguageMode = require('./text-mate-language-mode');
const { matcherForSelector } = require('./selectors');
const TreeIndenter = require('./tree-indenter');
+const { normalizeDelimiters, commentStringsFromDelimiters } = require('./comment-utils.js');
let nextId = 0;
const MAX_RANGE = new Range(Point.ZERO, Point.INFINITY).freeze();
@@ -169,7 +170,16 @@ class TreeSitterLanguageMode {
this.firstNonWhitespaceRange(position.row) ||
new Range(position, position);
const { grammar } = this.getSyntaxNodeAndGrammarContainingRange(range);
- return grammar.commentStrings;
+ const { grammar: originalPositionGrammar } = this.getSyntaxNodeAndGrammarContainingRange(new Range(position, position));
+
+ let result = commentStringsFromDelimiters(grammar.commentStrings);
+ console.log('result???', result, grammar.commentStrings);
+ if (originalPositionGrammar !== grammar) {
+ result.commentDelimiters = commentStringsFromDelimiters(
+ originalPositionGrammar.commentStrings
+ ).commentDelimiters;
+ }
+ return result;
}
isRowCommented(row) {
@@ -634,7 +644,8 @@ class TreeSitterLanguageMode {
grammarForLanguageString(languageString) {
return this.grammarRegistry.treeSitterGrammarForLanguageString(
- languageString
+ languageString,
+ 'original'
);
}
@@ -1126,6 +1137,7 @@ class LayerHighlightIterator {
}
seek(targetIndex, containingTags, containingTagStartIndices) {
+ // eslint-disable-next-line no-empty
while (this.treeCursor.gotoParent()) {}
this.atEnd = true;
diff --git a/src/ui.js b/src/ui.js
new file mode 100644
index 000000000..2a4662c44
--- /dev/null
+++ b/src/ui.js
@@ -0,0 +1,736 @@
+const path = require("path");
+const fs = require("fs");
+const MarkdownIt = require("markdown-it");
+const { TextEditor } = require("atom");
+const fuzzyNative = require("@pulsar-edit/fuzzy-native");
+
+// Helper Markdown Components
+const mdComponents = {
+ deps: {
+ domPurify: null,
+ yamlFrontMatter: null,
+ markdownItEmoji: null,
+ markdownItGitHubHeadings: null,
+ markdownItTaskCheckbox: null
+ },
+ // Regex Declarations
+ reg: {
+ localLinks: {
+ currentDir: new RegExp(/^\.\//),
+ rootDir: new RegExp(/^\//)
+ },
+ globalLinks: {
+ base64: new RegExp(/^data:image\/.*;base64/, "i")
+ },
+ atomLinks: {
+ package: new RegExp(/^https:\/\/atom\.io\/packages\/(.*)$/),
+ flightManual: new RegExp(/^https:\/\/flight-manual\.atom\.io\//)
+ }
+ },
+};
+
+/**
+ * @function renderMarkdown
+ * @memberof markdown
+ * @alias render
+ * @desc Takes a Markdown document and renders it as HTML.
+ * @param {string} content - The Markdown source material.
+ * @param {object} givenOpts - The optional arguments:
+ * @param {string} givenOpts.renderMode - Determines how the page is rendered.
+ * Valid values "full" or "fragment".
+ * @param {boolean} givenOpts.html - Whether HTML tags should be allowed.
+ * @param {boolean} givenOpts.sanitize - If the page content should be saniized via DOMPurify.
+ * @param {boolean} givenOpts.sanitizeAllowUnknownProtocols - Controls DOMPurify's
+ * own option of 'ALLOW_UNKNOWN_PROTOCOLS'.
+ * @param {boolean} givenOpts.sanitizeAllowSelfClose - Controls DOMPurify's
+ * own option of 'ALLOW_SELF_CLOSE'
+ * @param {boolean} givenOpts.breaks - If newlines should always be converted
+ * into breaklines.
+ * @param {boolean} givenOpts.handleFrontMatter - Whether frontmatter data should
+ * processed and displayed.
+ * @param {boolean} givenOpts.useDefaultEmoji - Whether `markdown-it-emoji` should be enabled.
+ * @param {boolean} givenOpts.useGitHubHeadings - Whether `markdown-it-github-headings`
+ * should be enabled. False by default.
+ * @param {boolean} givenOpts.useTaskCheckbox - Whether `markdown-it-task-checkbox`
+ * should be enabled. True by default.
+ * @param {boolean} givenOpts.taskCheckboxDisabled - Controls `markdown-it-task-checkbox`
+ * `disabled` option. True by default.
+ * @param {boolean} givenOpts.taskCheckboxDivWrap - Controls `markdown-it-task-checkboc`
+ * `divWrap` option. False by default.
+ * @param {boolean} givenOpts.transformImageLinks - Attempt to resolve image URLs.
+ * True by default.
+ * @param {boolean} givenOpts.transformAtomLinks - Attempt to resolve links
+ * pointing to Atom. True by Default.
+ * @param {boolean} givenOpts.transformNonFqdnLinks - Attempt to resolve links
+ * that are not fully qualified domain names. True by Default.
+ * @param {string} givenOpts.rootDomain - The root URL of the online resource.
+ * Useful when attempting to resolve any links on the page. Only works for online
+ * resources.
+ * @param {string} givenOpts.filePath - The local alternative to `rootDomain`.
+ * Used to resolve incomplete paths, but locally on the file system.
+ * @param {string} givenOpts.disabledMode - The level of disabling of markdown features.
+ * `none` by default. But supports: "none", "strict"
+ * @returns {string} Parsed HTML content.
+ */
+function renderMarkdown(content, givenOpts = {}) {
+ // First we will setup our markdown renderer instance according to the opts provided
+ const defaultOpts = {
+ renderMode: "full", // Determines if we are rendering a fragment or full page.
+ // Valid values: 'full', 'fragment'
+ html: true, // Enable HTML tags in source
+ sanitize: true, // Enable or disable sanitization
+ sanitizeAllowUnknownProtocols: true,
+ sanitizeAllowSelfClose: true,
+ breaks: false, // Convert `\n` in paragraphs into ` `
+ handleFrontMatter: true, // Determines if Front Matter content should be parsed
+ useDefaultEmoji: false, // Use `markdown-it-emoji`
+ useGitHubHeadings: false, // Use `markdown-it-github-headings`
+ useTaskCheckbox: true, // Use `markdown-it-task-checkbox`
+ taskCheckboxDisabled: true, // `markdown-it-task-checkbox`: Disable checkbox interactivity
+ taskCheckboxDivWrap: false, // `markdown-it-task-checkbox`: Wrap div arround checkboc
+ transformImageLinks: true, // Attempt to resolve image urls
+ transformAtomLinks: true, // Attempt to rewrite links to Atom pages, changing them to Pulsar
+ transformNonFqdnLinks: true, // Attempt to resolve non-FQDN links
+ rootDomain: "", // The root URL that should be used for the above 'transform' options
+ filePath: "", // The path to the file where this markdown is generated from,
+ disableMode: "none", // The level of disabling that should be done on the output.
+ // Provides helpful defaults to control how much or how little is disabled:
+ // - none: Nothing is disabled
+ // - strict: Everything possible is disabled, except what is otherwise needed
+ };
+
+ let opts = { ...defaultOpts, ...givenOpts };
+
+ const validateRootDomain = () => {
+ return typeof opts.rootDomain === "string" && opts.rootDomain.length > 1;
+ };
+
+ const cleanRootDomain = () => {
+ // We will also remove any trailing `/` as link resolvers down the line add them in
+ return opts.rootDomain.replace(".git", "").replace(/\/$/, "");
+ };
+
+ const markdownItOpts = {
+ html: opts.html,
+ breaks: opts.breaks
+ };
+
+ let md = new MarkdownIt(markdownItOpts);
+
+ if (opts.useDefaultEmoji) {
+ mdComponents.deps.markdownItEmoji ??= require("markdown-it-emoji");
+ md.use(mdComponents.deps.markdownItEmoji, {});
+ }
+ if (opts.useGitHubHeadings) {
+ mdComponents.deps.markdownItGitHubHeadings ??= require("markdown-it-github-headings");
+ md.use(mdComponents.deps.markdownItGitHubHeadings, {});
+ }
+ if (opts.useTaskCheckbox) {
+ mdComponents.deps.markdownItTaskCheckbox ??= require("markdown-it-task-checkbox");
+ md.use(mdComponents.deps.markdownItTaskCheckbox, {
+ disabled: opts.taskCheckboxDisabled,
+ divWrap: opts.taskCheckboxDivWrap
+ });
+ }
+ if (opts.transformImageLinks && validateRootDomain()) {
+ // Here we will take any links for images provided in the content, and do
+ // our best to ensure they can accurately resolve.
+ const defaultImageRenderer = md.renderer.rules.image; // We want to keep access to this
+
+ // Determines when we handle links if the item could be a local file or not
+ let couldBeLocalItem;
+ if (typeof opts.filePath != "string" || opts.filePath.length < 1) {
+ couldBeLocalItem = false;
+ } else {
+ couldBeLocalItem = true;
+ }
+
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
+ let token = tokens[idx];
+ let aIndex = token.attrIndex("src");
+
+ // Lets say content contains './my-cool-image.png'
+ // We need to turn it into something like this:
+ // https://github.com/USER/REPO/raw/HEAD/my-cool-image.png
+ if (mdComponents.reg.localLinks.currentDir.test(token.attrGet("src"))) {
+ let rawLink = token.attrGet("src");
+ rawLink = rawLink.replace(mdComponents.reg.localLinks.currentDir, "");
+ // Now we need to handle links for both the web and locally
+ // We can do this by first checking if the link resolves locally
+ if (couldBeLocalItem) {
+ let newSrc = path.resolve(path.dirname(opts.filePath, rawLink));
+ if (!fs.lstatSync(newSrc).isFile()) {
+ token.attrSet("src", newSrc);
+ } else {
+ token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`);
+ }
+ } else {
+ token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`);
+ }
+ } else if (mdComponents.reg.localLinks.rootDir.test(token.attrGet("src"))) {
+ let rawLink = token.attrGet("src");
+ rawLink = rawLink.replace(mdComponents.reg.localLinks.rootDir, "");
+ // Now to handle the possible web or local link
+ if (couldBeLocalItem) {
+ const [rootDirectory] = atom.project.relativePath(opts.filePath);
+ if (!fs.lstatSync(src).isFile() && rootDirectory) {
+ let newSrc = path.join(rootDirectory, rawLink);
+ token.attrSet("src", newSrc);
+ } else {
+ token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`);
+ }
+ } else {
+ token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`);
+ }
+ } else if (!token.attrGet("src").startsWith("http") && !mdComponents.reg.globalLinks.base64.test(token.attrGet("src"))) {
+ // Check for implicit relative urls
+ let rawLink = token.attrGet("src");
+ token.attrSet("src", `${cleanRootDomain()}/raw/HEAD/${rawLink}`);
+ } else if ([".gif", ".png", ".jpg", ".jpeg", ".webp"].find(ext => token.attrGet("src").endsWith(ext)) && token.attrGet("src").startsWith("https://github.com") && token.attrGet("src").includes("blob")) {
+ // Should match any image being distributed from GitHub that's using `blob` instead of `raw` causing images to not load correctly
+ let rawLink = token.attrGet("src");
+ token.attrSet("src", rawLink.replace("blob", "raw"));
+ }
+
+ // pass token to default renderer
+ return defaultImageRenderer(tokens, idx, options, env, self);
+ };
+ }
+ if (validateRootDomain() && opts.transformNonFqdnLinks) {
+ md.core.ruler.after("inline", "fix-links", (state) => {
+ state.tokens.forEach((blockToken) => {
+ if (blockToken.type === "inline" && blockToken.children) {
+ blockToken.children.forEach((token) => {
+ if (token.type === "link_open") {
+ token.attrs.forEach((attr) => {
+ if (attr[0] === "href") {
+ let link = attr[1];
+
+ if (opts.transformNonFqdnLinks && mdComponents.reg.localLinks.currentDir.test(link)) {
+ attr[1] = `${cleanRootDomain()}/blob/HEAD/${link.replace(mdComponents.reg.localLinks.currentDir, "")}`;
+ } else if (opts.transformNonFqdnLinks && mdComponents.reg.localLinks.rootDir.test(link)) {
+ attr[1] = `${cleanRootDomain()}/blob/HEAD/${link.replace(mdComponents.reg.localLinks.rootDir, "")}`;
+ } else if (opts.transformNonFqdnLinks && !link.startsWith("http")) {
+ attr[1] = `${cleanRootDomain()}/blob/HEAD/${link.replace(".git", "")}`;
+ }
+ }
+ });
+ }
+ });
+ }
+ });
+ });
+ } else if (opts.transformAtomLinks) {
+ // This is a separate if since transforming Atom links does not need a valid root domain provided
+ md.core.ruler.after("inline", "fix-atom-links", (state) => {
+ state.tokens.forEach((blockToken) => {
+ if (blockToken.type === "inline" && blockToken.children) {
+ blockToken.children.forEach((token) => {
+ if (token.type === "link_open") {
+ token.attrs.forEach((attr) => {
+ if (attr[0] === "href") {
+ let link = attr[1];
+
+ if (mdComponents.reg.atomLinks.package.test(link)) {
+ // Fix any links that attempt to point to packages on `https://atom.io/packages/...`
+ attr[1] = `https://web.pulsar-edit.dev/packages/${link.match(mdComponents.reg.atomLinks.package)[1]}`;
+ } else if (mdComponents.reg.atomLinks.flightManual.test(link)) {
+ // Resolve any links to the flight manual to web archive
+ attr[1] = link.replace(mdComponents.reg.atomLinks.flightManual, "https://web.archive.org/web/20221215003438/https://flight-manual.atom.io/");
+ }
+ }
+ });
+ }
+ });
+ }
+ });
+ });
+ }
+
+ // 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}
`;
+ };
+
+ // Process disables
+ if (opts.disableMode === "strict") {
+
+ // Easy Disable
+ md.disable("lheading");
+
+ // Disable Code Blocks
+ md.renderer.rules.code_block = (tokens, idx, _options, _env, _self) => {
+ if (tokens[idx].type === "code_block") {
+ return "";
+ }
+ };
+
+ // Disable Code Fences
+ md.renderer.rules.fence = (tokens, idx, _options, _env, _self) => {
+ if (tokens[idx].type === "fence") {
+ return "";
+ }
+ };
+
+ // Disable Images
+ md.renderer.rules.image = (tokens, idx, _options, _env, _self) => {
+ // Double check this is an image
+ if (tokens[idx].type === "image") {
+ return "";
+ }
+ };
+
+ // Only support line breaks in HTML that's inline
+ md.inline.ruler.before("html_inline", "only_allow_line_breaks", (state) => {
+ // Determine how to best handle this to only allow line breaks. Research needed
+ if (state.src.charAt(state.pos) === "<") {
+ // We only want to act once on the beginning of the inline element
+ // Then confirm if it's the item we expect
+ const textAfterPending = state.src.replace(state.pending, "");
+ const match = textAfterPending.match(/^ /);
+ if (match) {
+ // We define breakline as a custom Token Type
+ let token = state.push("html_inline", "breakline", 0);
+ token.content = " ";
+ state.pos += match[0].length;
+ return true;
+ }
+ }
+ });
+
+ // Disable Heading
+ md.block.ruler.before("heading", "strip_heading", (state, startLine, endLine) => {
+ let pos = state.bMarks[startLine] + state.tShift[startLine];
+
+ if (state.src.charAt(pos) === "#") {
+ let max = state.eMarks[startLine];
+
+ const isSpace = () => {
+ let code = state.src.charCodeAt(pos);
+ switch(code) {
+ case 0x09:
+ case 0x20:
+ return true;
+ }
+ return false;
+ };
+
+ let level = 1;
+
+ let ch = state.src.charAt(++pos);
+ while (ch === "#" && pos < max && level <= 6) {
+ level++;
+ ch = state.src.charAt(++pos);
+ }
+
+ if (level > 6 || (pos < max && !isSpace())) { return false; }
+ // Now that we are confident we are within a heading, lets strip it
+ state.pos += level;
+ state.line = startLine + 1;
+ return true;
+ }
+ });
+
+ const stripAllTokensTill = (tokens, initIdx, endType) => {
+ // This function will loop a given set of tokens, stripping them of all data
+ // (converting them to empty text tokens)
+ // until the specified token is reached. Which it will also strip to text,
+ // then return
+ let idx = initIdx;
+ while(idx < tokens.length) {
+ tokens[idx].type = "text";
+ tokens[idx].content = "";
+
+ if (tokens[idx].type == endType) {
+ break;
+ }
+
+ idx++;
+ }
+ return;
+ };
+
+ // Disable blockquotes
+ md.renderer.rules.blockquote_open = (tokens, idx, _options, _env, _self) => {
+ stripAllTokensTill(tokens, idx, "blockquote_close");
+ return "";
+ };
+
+ // Disable Bullet lists
+ md.renderer.rules.bullet_list_open = (tokens, idx, _options, _env, _self) => {
+ stripAllTokensTill(tokens, idx, "bullet_list_close");
+ return "";
+ };
+
+ // Disable Ordered lists
+ md.renderer.rules.ordered_list_open = (tokens, idx, _options, _env, _self) => {
+ stripAllTokensTill(tokens, idx, "ordered_list_close");
+ return "";
+ };
+
+ // Ensure that only breaklines are supported as inline raw HTML
+ md.renderer.rules.html_inline = (tokens, idx, _options, _env, _self) => {
+ if (tokens[idx].type === "html_inline") {
+ // Here we can build an allow list of inline HTML elements to keep.
+ if (
+ tokens[idx].tag !== "breakline"
+ ) {
+ return "";
+ } else {
+ return tokens[idx].content;
+ }
+ }
+ };
+
+ // Ensure nothing is supported as block HTML
+ md.renderer.rules.html_block = (tokens, idx, _options, _env, _self) => {
+ if (tokens[idx].type === "html_block") {
+ return "";
+ }
+ };
+
+ }
+
+ let textContent;
+
+ if (opts.handleFrontMatter) {
+ mdComponents.deps.yamlFrontMatter ??= require("yaml-front-matter");
+ const { __content, vars } = mdComponents.deps.yamlFrontMatter.loadFront(content);
+
+ const renderYamlTable = (variables) => {
+ if (typeof variables === "undefined") {
+ return "";
+ }
+
+ const entries = Object.entries(variables);
+
+ if (!entries.length) {
+ return "";
+ }
+
+ const markdownRows = [
+ entries.map(entry => entry[0]),
+ entries.map(entry => '--'),
+ entries.map((entry) => {
+ if (typeof entry[1] === "object" && !Array.isArray(entry[1])) {
+ // Remove all newlines, or they ruin formatting of parent table
+ return md.render(renderYamlTable(entry[1])).replace(/\n/g, "");
+ } else {
+ return entry[1];
+ }
+ })
+ ];
+
+ return (
+ markdownRows.map(row => "| " + row.join(" | ") + " |").join("\n") + "\n"
+ );
+ };
+
+ textContent = renderYamlTable(vars) + __content;
+ } else {
+ textContent = content;
+ }
+
+ let rendered = md.render(textContent);
+
+ if (opts.sanitize) {
+ mdComponents.deps.domPurify ??= require("dompurify");
+
+ let domPurifyOpts = {
+ ALLOW_UNKNOWN_PROTOCOLS: opts.sanitizeAllowUnknownProtocols,
+ ALLOW_SELF_CLOSE_IN_ATTR: opts.sanitizeAllowSelfClose
+ };
+
+ rendered = mdComponents.deps.domPurify.sanitize(rendered, opts);
+ }
+
+ return rendered;
+}
+
+/**
+ * @function applySyntaxHighlighting
+ * @memberof markdown
+ * @async
+ * @desc Uses Pulsar's built-in Syntax Highlighting system to apply the same syntax
+ * highlighting to code blocks within markdown. Modifies the existing object passed.
+ * @param {HTMLFragment} content - The HTML Node/Fragment to apply syntax highlighting on.
+ * Will modifyn the original object.
+ * @param {object} givenOpts - Optional Arguments:
+ * @param {function} givenOpts.syntaxScopeNameFunc - A function that can be called with
+ * any given language ID from a code block scope, and returns the grammar source id
+ * that should be used to preform syntax highlighting.
+ * @param {string} givenOpts.renderMode - Whether we are rdnering a document fragment
+ * or a full document. Valid values: "full", "fragment".
+ * @param {object} givenOpts.grammar - The grammar of the source file. Carryover from
+ * original `markdown-preview` functionality.
+ */
+function applySyntaxHighlighting(content, givenOpts = {}) {
+ const defaultOpts = {
+ syntaxScopeNameFunc: null, // Function used to resolve codeblock fences language id
+ // to a Pulsar Grammar source. Should be a function that takes the declared scope and returns a source,
+ grammar: null,
+ renderMode: "full", // Just like in `renderMarkdown` this can be full or fragment
+ };
+
+ const opts = { ...defaultOpts, ...givenOpts };
+
+ const scopeForFenceName = (fence) => {
+ if (typeof opts.syntaxScopeNameFunc == "function") {
+ return opts.syntaxScopeNameFunc(fence);
+ } else {
+ // We could build one in, or just return default
+ return "text.plain";
+ }
+ };
+
+ let defaultLanguage;
+ const fontFamily = atom.config.get("editor.fontFamily");
+
+ if ((opts.grammar != null ? opts.grammar.scopeName : undefined) === "source.litcoffee") {
+ // This behavior is carried over from `markdown-preview` but it's purpose and need
+ // is not fully understood.
+ defaultLanguage = "coffee";
+ } else {
+ defaultLanguage = "text";
+ }
+
+ if (fontFamily) {
+ for (const codeElement of content.querySelectorAll("code")) {
+ codeElement.style.fontFamily = fontFamily;
+ }
+ }
+
+ let editorCallback;
+
+ if (opts.renderMode === "fragment") {
+ editorCallback = makeAtomEditorNonInteractive;
+ } else {
+ // Captures full and defaults
+ editorCallback = convertAtomEditorToStandardElement;
+ }
+
+ const promises = [];
+ for (const preElement of content.querySelectorAll("pre")) {
+ const codeBlock = preElement.firstElementChild ?? preElement;
+ const className = codeBlock.getAttribute("class");
+ const fenceName =
+ className != null ? className.replace(/^language-/, "") : defaultLanguage;
+
+ const editor = new TextEditor({
+ readonly: true,
+ keyboardInputEnabled: false
+ });
+ const 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);
+
+ promises.push(editorCallback(editorElement, preElement));
+ }
+ return Promise.all(promises);
+}
+
+/**
+ * @function convertToDOM
+ * @memberof markdown
+ * @desc Takes a raw HTML string of data and returns a proper HTMLFragment.
+ * This should be done if you need access to APIs available on the DOM itself.
+ * @param {string} content - The HTML String.
+ * @returns {HTMLFragment}
+ */
+function convertToDOM(content) {
+ const template = document.createElement("template");
+ template.innerHTML = content;
+ const fragment = template.content.cloneNode(true);
+ return fragment;
+}
+
+/*
+ # Name: setCandidates
+ # Type: ClassMethod
+
+ Sets the candidates for a new matcher, or sets the candidates for an existing
+ matcher. Returns a {Matcher} that can be used to query for candidates.
+
+ * `matcherOrCandidates` - either a {Matcher} returned from a previous call
+ from `setCandidates`, or an array of string candidates to be filtered
+ * `candidates` - an array of string candidates to be filtered
+
+ ## Examples
+ ```js
+ const matcher = atom.ui.fuzzyMatcher.setCandidates(["hello", "world"])
+ matcher.match('he') // => will return [{value: "hello", score: }]
+ atom.ui.fuzzyMatcher.setCandidates(matcher, ["hello", "hope"])
+ matcher.match('he') // => will now return "hope" too, but it'll be at
+ // second position with a lower score
+ ```
+*/
+function setCandidates(matcherOrCandidates, candidates) {
+ if(candidates) {
+ matcherOrCandidates.fuzzyMatcher.setCandidates(
+ [...Array(candidates.length).keys()],
+ candidates
+ );
+ return matcherOrCandidates;
+ } else {
+ return new Matcher(
+ new fuzzyNative.Matcher(
+ [...Array(matcherOrCandidates.length).keys()],
+ matcherOrCandidates
+ )
+ );
+ }
+}
+
+/*
+ # Name: Matcher
+ # Type: Class
+
+ The result from a call to {fuzzyMatcher.setCandidates}.
+*/
+class Matcher {
+ constructor(fuzzyMatcher) {
+ // Some heuristics to get the default number of CPUs to make the filter
+ this.numCpus = Math.max(1, Math.round(4 * 0.8))
+ this.fuzzyMatcher = fuzzyMatcher
+ }
+
+ /*
+ # Name: match
+ # Type: InstanceMethod
+
+ Matches the current candidates to a query. Query must be a string
+
+ * `query` A string query to filter the pre-defined candidates
+ * `options` Key/map to customize the details of the search. All keys are
+ optional, meaning they all have defaults
+ * `algorithm` Either "fuzzaldrin" or "command-t". Defaults to "fuzzaldrin"
+ (the **opposite** of @pulsar-edit/fuzzy-native)
+ * `maxResults` The number of results to be returned. Defaults to Infinite,
+ meaning that it'll return _all results_ that did match. Please notice
+ that this have no difference in actual filtering speed
+ * `recordMatchIndexes` If `true`, returns also `matchIndexes`, an array
+ of numbers where each number is the index (0-based) of the character
+ that was matched. Defaults to `false`
+ * `numThreads` The number of threads to filter. Defaults to 80% of the
+ current CPUs
+ * `maxGap` (only "command-t") The number of maximum "character gap" between
+ consecutive letters. A smaller gap means a faster result. Defaults to
+ Infinite
+
+ Returns: an object containing:
+
+ * `id` The index of the candidate
+ * `value` The original (string) value of the filtered candidate
+ * `score` A number in the range 0 to 1. Higher scores are more relevant.
+ 0 denotes "no match" and will never be returned.
+ * `matchIndexes` (optional) Will be returned only if `recordMatchIndexes`
+ is set to true. It's an array of integer for each character index in
+ `value` for each character in `query`. This can be expensive to calculate.
+ */
+ match(query, options = {}) {
+ let {numThreads, algorithm} = options;
+ numThreads ||= this.numCpus;
+ algorithm ||= 'fuzzaldrin';
+ return this.fuzzyMatcher.match(query, {...options, numThreads, algorithm});
+ }
+
+ /*
+ # Name: setCandidates
+ # Type: InstanceMethod
+
+ Exactly the same as {setCandidates}, passing this {Matcher} as the first parameter
+ */
+ setCandidates(candidates) {
+ return setCandidates(this, candidates);
+ }
+}
+
+/*
+ Essential: The {fuzzyMatcher} API, the same used in the autocomplete,
+ fuzzy-finder core package, command pallete, etc.
+ An instance of this API is available via the `atom.ui.fuzzyMatcher` global.
+
+ This API have two parts - the filtering of an array of candidates, and the
+ scoring. Scoring is done via the {fuzzyMatcher.score}, and filtering is
+ done by returning a new {Matcher} using the {fuzzyMatcher.setCandidates}
+ method, then calling {Matcher#match}. You can _also use_ the
+ {fuzzyMatcher.match} to match a single candidate - it uses the same API and
+ options as {Matcher#match}.
+*/
+const fuzzyMatcher = {
+ setCandidates: setCandidates,
+
+ // Same as {setCandidates} passing a single candidate, and returning only
+ // the score. It can return `0` if there's no match.
+ score(candidate, query, opts = {}) {
+ return this.match(candidate, query)?.score || 0;
+ },
+
+ // The same as {setCandidates} with a single candidate. Returns just the
+ // match, if there's one (can return `undefined`).
+ match(candidate, query, opts = {}) {
+ const matcher = setCandidates([candidate]);
+ return matcher.match(query, opts)[0];
+ }
+}
+
+function makeAtomEditorNonInteractive(editorElement, preElement) {
+ preElement.remove();
+ editorElement.setAttributeNode(document.createAttribute("gutter-hidden")); // Hide gutter
+ editorElement.removeAttribute("tabindex"); // Make read-only
+
+ // Remove line decorations from code blocks.
+ for (const cursorLineDecoration of editorElement.getModel().cursorLineDecorations) {
+ cursorLineDecoration.destroy();
+ }
+}
+
+function convertAtomEditorToStandardElement(editorElement, preElement) {
+ return new Promise(function (resolve) {
+ const editor = editorElement.getModel();
+ const done = () =>
+ editor.component.getNextUpdatePromise().then(function () {
+ for (const line of editorElement.querySelectorAll(
+ ".line:not(.dummy)"
+ )) {
+ const line2 = document.createElement("div");
+ line2.className = "line";
+ line2.innerHTML = line.firstChild.innerHTML;
+ preElement.appendChild(line2);
+ }
+ editorElement.remove();
+ resolve();
+ })
+ const languageMode = editor.getBuffer().getLanguageMode();
+ if (languageMode.fullyTokenized || languageMode.tree) {
+ done();
+ } else {
+ editor.onDidTokenize(done);
+ }
+ });
+}
+
+// Markdown Exported Object
+/**
+ * @member markdown
+ * @memberof ui
+ * @desc The Markdown object exported from the UI API.
+ * Provides access to: ".render", ".applySyntaxHighlighting", ".convertToDOM"
+ */
+const markdown = {
+ render: renderMarkdown,
+ applySyntaxHighlighting: applySyntaxHighlighting,
+ convertToDOM: convertToDOM
+};
+
+module.exports = {
+ markdown,
+ fuzzyMatcher
+};
diff --git a/src/wasm-tree-sitter-grammar.js b/src/wasm-tree-sitter-grammar.js
index c14588fff..bed2fe8a6 100644
--- a/src/wasm-tree-sitter-grammar.js
+++ b/src/wasm-tree-sitter-grammar.js
@@ -3,9 +3,11 @@ const path = require('path');
const Parser = require('./web-tree-sitter');
const { CompositeDisposable, Emitter } = require('event-kit');
const { File } = require('pathwatcher');
+const { normalizeDelimiters } = require('./comment-utils.js');
const parserInitPromise = Parser.init();
+// Extended: This class holds an instance of a Tree-sitter grammar.
module.exports = class WASMTreeSitterGrammar {
constructor(registry, grammarPath, params) {
this.registry = registry;
@@ -55,7 +57,9 @@ module.exports = class WASMTreeSitterGrammar {
}
}
- idForScope(scopeName) {
+ // Though _text is unused here, some packages (eg semanticolor) use it to
+ // customize scopes on the fly.
+ idForScope(scopeName, _text) {
if (!scopeName) { return undefined; }
let id = this.idsByScope[scopeName];
if (!id) {
@@ -71,48 +75,42 @@ module.exports = class WASMTreeSitterGrammar {
return id;
}
- // Retrieve the comment delimiters for this grammar.
+ // Extended: Retrieve all known comment delimiters for this grammar.
+ //
+ // Some grammars may have different delimiters for different parts of a file
+ // (such as JSX within JavaScript). In these cases, you might want to call
+ // {TextEditor::getCommentDelimitersForBufferPosition} with a `{Point}` in the
+ // buffer.
+ //
+ // Returns an {Object} with the following properties:
+ //
+ // * `line`: If present, a {String} representing a line comment delimiter.
+ // (If `undefined`, there is no known line comment delimiter for the given
+ // buffer position.)
+ // * `block`: If present, a two-item {Array} containing {String}s
+ // representing the starting and ending block comment delimiters. (If
+ // `undefined`, there are no known block comment delimiters for the given
+ // buffer position.)
//
- // Traditionally, grammars specified only the delimiters needed for the
- // βToggle Line Commentsβ command β either a line comment (if it existed) or
- // a block comment. But other features might want to know _all_ of a
- // language's possible comment delimiters, so we've devised new config values.
getCommentDelimiters() {
- let meta = this.commentMetadata;
- if (!meta) { return null; }
+ // Prefer the config system. It's a better place for this data to live.
+ let commentDelimiters = atom.config.get(
+ 'editor.commentDelimiters',
+ { scope: [this.scopeName] }
+ );
+ if (commentDelimiters) return commentDelimiters;
- let result = {};
-
- // The new convention is to specify a `line` property and start/end
- // properties.
- let { line, block } = this.commentMetadata;
-
- // Failing that, we can deliver at least a partial result by inspecting the
- // older convention. If `start` exists but not `end`, we know `start` must
- // be a line comment delimiter.
- if (!line && meta.start && !meta.end) {
- line = meta.start;
- }
- // Likewise, if both `start` and `end` exist, we know they must be block
- // comment delimiters.
- if (!block && meta.start && meta.end) {
- block = { start: meta.start, end: meta.end };
+ // Failing that, try to extract useful information from this metadata.
+ if (this.commentMetadata) {
+ return normalizeDelimiters(this.commentMetadata);
}
- // Strip all whitespace from delimiters. Whatever is consuming them can
- // decide if it wants whitespace.
- if (line) {
- line = line.strip();
- result.line = line;
- }
+ // If even that doesn't exist, we can fall back onto the older config
+ // settings.
+ let start = atom.config.get('editor.commentStart', { scope: [this.scope] });
+ let end = atom.config.get('editor.commentEnd', { scope: [this.scope] });
- if (block) {
- block.start = block.start?.strip();
- block.end = block.end?.strip();
- result.block = block;
- }
-
- return result;
+ return normalizeDelimiters({ start, end });
}
classNameForScopeId(id) {
@@ -130,13 +128,21 @@ module.exports = class WASMTreeSitterGrammar {
return this._language;
}
- // Returns the Tree-sitter language instance associated with this grammar
- // once it loads. When the {Promise} returned by this method resolves, the
- // grammar is ready to perform parsing and to execute query captures.
+ // Extended: Returns the Tree-sitter language instance associated with this
+ // grammar once it loads.
+ //
+ // Returns a {Promise} that will resolve with a Tree-sitter `Language`
+ // instance. Once it resolves, the grammar is ready to perform parsing and to
+ // execute query captures.
async getLanguage() {
await parserInitPromise;
if (!this._language) {
- this._language = await Parser.Language.load(this.treeSitterGrammarPath);
+ try {
+ this._language = await Parser.Language.load(this.treeSitterGrammarPath);
+ } catch (err) {
+ console.error(`Error loading grammar for ${this.scopeName}; original error follows`);
+ throw err;
+ }
}
if (!this._queryFilesLoaded) {
@@ -146,10 +152,6 @@ module.exports = class WASMTreeSitterGrammar {
}
async loadQueryFiles(grammarPath, queryPaths) {
- if (!('highlightsQuery' in queryPaths)) {
- throw new Error(`Highlights query must be present`);
- }
-
if (this._loadQueryFilesPromise) {
return this._loadQueryFilesPromise;
}
@@ -241,11 +243,19 @@ module.exports = class WASMTreeSitterGrammar {
return query;
}
- // Async, but designed so that multiple near-simultaneous calls to `getQuery`
- // from multiple buffers will not cause multiple calls to `language.query`,
- // since it's a major bottleneck. Instead they all receive the same unsettled
- // promise.
+ // Extended: Given a kind of query, retrieves a Tree-sitter `Query` object
+ // in async fashion.
+ //
+ // * `queryType` A {String} describing the query type: typically one of
+ // `highlightsQuery`, `foldsQuery`, `tagsQuery`, or `indentsQuery`,
+ // but could be any other custom type.
+ //
+ // Returns a {Promise} that resolves to a Tree-sitter `Query` object.
getQuery(queryType) {
+ // Async, but designed so that multiple near-simultaneous calls to
+ // `getQuery` from multiple buffers will not cause multiple calls to
+ // `language.query`, since it's a major bottleneck. Instead they all
+ // receive the same unsettled promise.
// let inDevMode = atom.inDevMode();
let query = this.queryCache.get(queryType);
if (query) { return Promise.resolve(query); }
@@ -276,18 +286,28 @@ module.exports = class WASMTreeSitterGrammar {
return promise;
}
- // Creates an arbitrary query from this grammar. Package authors and end
- // users can use queries for whatever purposes they like.
+ // Extended: Creates an arbitrary query from this grammar. Package authors
+ // and end users can use queries for whatever purposes they like.
+ //
+ // * `queryContents` A {String} representing the entire contents of a query
+ // file. Can contain any number of queries.
+ //
+ // Returns a {Promise} that will resolve to a Tree-sitter `Query` object.
async createQuery(queryContents) {
let language = await this.getLanguage();
return language.query(queryContents);
}
- // Creates an arbitrary query from this grammar. Package authors and end
- // users can use queries for whatever purposes they like.
+ // Extended: Creates an arbitrary query from this grammar. Package authors
+ // and end users can use queries for whatever purposes they like.
//
// Synchronous; use only when you can be certain that the tree-sitter
// language has already loaded.
+ //
+ // * `queryContents` A {String} representing the entire contents of a query
+ // file. Can contain any number of queries.
+ //
+ // Returns a Tree-sitter `Query` object.
createQuerySync(queryContents) {
if (!this._language) {
throw new Error(`Language not loaded!`);
@@ -333,14 +353,16 @@ module.exports = class WASMTreeSitterGrammar {
}
}
- // Calls `callback` when any of this grammar's query files change.
+ // Extended: Calls `callback` when any of this grammar's query files change.
//
// The callback is invoked with an object argument with two keys:
//
- // - `filePath`: The path to the query file on disk.
- // - `queryType`: The type of query file, as denoted by its configuration key
- // in the grammar file. One of `highlightsQuery`, `indentsQuery`,
- // `foldsQuery`, or `localsQuery`.
+ // * `callback`: The callback to be invoked. Takes one argument:
+ // * `data`: An object with keys:
+ // * `filePath`: The path to the query file on disk.
+ // * `queryType`: The type of query file, as denoted by its
+ // configuration key in the grammar file. Usually one of
+ // `highlightsQuery`, `indentsQuery`, `foldsQuery`, or `tagsQuery`.
onDidChangeQueryFile(callback) {
return this.emitter.on('did-change-query-file', callback);
}
@@ -357,8 +379,8 @@ module.exports = class WASMTreeSitterGrammar {
this.queryCache.clear();
}
- // Define a set of rules for when this grammar should delegate to a different
- // grammar for certain regions of a buffer. Examples:
+ // Extended: Define a set of rules for when this grammar should delegate to a
+ // different grammar for certain regions of a buffer. Examples:
//
// * embedding one language inside another (e.g., JavaScript in HTML)
// * tokenizing certain structures with greater detail (e.g., regular
@@ -367,56 +389,58 @@ module.exports = class WASMTreeSitterGrammar {
// comments in JavaScript)
//
// This differs from TextMate-style injections, which operate at the scope
- // level and are currently incompatible with tree-sitter grammars.
+ // level and are currently incompatible with Tree-sitter grammars.
//
- // Expects an object with these keys:
+ // NOTE: Packages will call {::addInjectionPoint} with a given scope name,
+ // and that call will be delegated to any Tree-sitter grammars that match
+ // that scope name, whether they're legacy Tree-sitter or modern Tree-sitter.
+ // But modern Tree-sitter grammars cannot be injected into legacy Tree-sitter
+ // grammars, and vice versa.
//
- // * `type` (string): The type of node to inject into.
- // * `language` (function): Should return a string describing the language
- // that should inject into this area. Grammars that can inject into others
- // will define an `injectionRegex` property that will be tested against
- // this value; the longest match will win.
- // The function receives the node itself as an argument, so you can decide
- // the language based on the content of the node, or return `undefined` if
- // an injection should not take place.
- // * `content` (function): Receives the matching node and should return the
- // node that will actually be injected into. Usually this will be the same
- // node that was given, but could also be a specific child or descendant of
- // that node, or potentially any other node in the tree.
+ // * `options` The options for the injection point:
+ // * `type` A {String} describing type of node to inject into.
+ // * `language` A {Function} that should return a string describing the
+ // language that should inject into this area. The string should be a
+ // short, unambiguous description of the language; it will be tested
+ // against other grammarsβ `injectionRegex` properties. Receives one
+ // parameter:
+ // * `node` A Tree-sitter node.
+ // * `content` A {Function} that should return the node (or nodes) that
+ // will actually be injected into. Usually this will be the same node
+ // that was given, but could also be a specific child or descendant of
+ // that node.
+ // * `includeChildren` (optional) {Boolean} controlling whether the
+ // injection range should include the ranges of the content nodeβs
+ // children. Defaults to `false`, meaning that the range of each of this
+ // node's children will be "subtracted" from the injection range, and the
+ // remainder will be parsed as if those ranges of the buffer do not
+ // exist.
+ // * `includeAdjacentWhitespace` (optional) {Boolean} controlling whether
+ // the injection range should include whitespace that occurs between
+ // content nodes. Defaults to `false`. When `true`, if two injection
+ // ranges are separated from one another by only whitespace, that
+ // whitespace will be added to the injection range, and the ranges will
+ // be consolidated.
+ // * `newlinesBetween` (optional) {Boolean} controlling whether the
+ // injection range should include any newline characters that may exist
+ // in between injection ranges. Defaults to `false`. Grammars like ERB
+ // and EJS need this so that they do not interpret two different
+ // embedded code sections on different lines as occurring on the same
+ // line.
+ // * `coverShallowerScopes` (optional) {Boolean} controlling whether the
+ // injection should prevent the parent grammar (and any of its
+ // ancestors) from applying scope boundaries within its injection
+ // range(s). Defalts to `false`.
+ // * `languageScope` (optional) A value that determines what scope, if
+ // any, is added to the injection as its βbaseβ scope name. Can be a
+ // {String}, {null}, or a {Function} that returns either of these values.
+ // The base language scope that should be used by this injection.
+ // Defaults to the grammar's own `scopeName` property. Set this to a
+ // string to override the default scope name, or `null` to omit a base
+ // scope name altogether. Set this to a function if the scope name to be
+ // applied varies based on the grammar; the function will be called with
+ // a grammar instance as its only argument.
//
- // Understands the following optional keys:
- //
- // * `includeChildren` (boolean): Whether the injection range should include
- // the ranges of this node's children. Defaults to `false`, meaning that
- // the range of each of this node's children will be "subtracted" from the
- // injection range, and the remainder will be parsed as if those ranges of
- // the buffer do not exist.
- // * `includeAdjacentWhitespace` (boolean): Whether the injection range
- // should include whitespace that occurs between content nodes. Defaults to
- // `false`. When `true`, if two injection ranges are separated from one
- // another by only whitespace, that whitespace will be added to the
- // injection range, and the ranges will be consolidated.
- // * `newlinesBetween` (boolean): Whether the injection range should include
- // any newline characters that may exist in between injection ranges.
- // Defaults to `false`. Grammars like ERB and EJS need this so that they do
- // not interpret two different embedded code sections on different lines as
- // occurring on the same line.
- // * `coverShallowerScopes` (boolean): Whether the injection should prevent
- // the parent grammar (and any of its ancestors) from applying scope
- // boundaries within its injection range(s). Defalts to `false`.
- // * `languageScope` (string | function | null): The base language scope that
- // should be used by this injection. Defaults to the grammar's own
- // `scopeName` property. Set this to a string to override the default scope
- // name, or `null` to omit a base scope name altogether. Set this to a
- // function if the scope name to be applied varies based on the grammar;
- // the function will be called with a grammar instance as its only
- // argument.
- //
- // NOTE: Packages will call `atom.grammars.addInjectionPoint` with a given
- // scope name, and that call will be delegated to any tree-sitter grammars
- // that match that scope name, whether they're legacy-tree-sitter or
- // modern-tree-sitter. But modern-tree-sitter grammars cannot be injected
- // into by legacy-tree-sitter-grammars, and vice versa.
//
addInjectionPoint(injectionPoint) {
let { type } = injectionPoint;
diff --git a/src/wasm-tree-sitter-language-mode.js b/src/wasm-tree-sitter-language-mode.js
index f1b57e14d..d7f112763 100644
--- a/src/wasm-tree-sitter-language-mode.js
+++ b/src/wasm-tree-sitter-language-mode.js
@@ -8,6 +8,7 @@ const ScopeResolver = require('./scope-resolver');
const Token = require('./token');
const TokenizedLine = require('./tokenized-line');
const { matcherForSelector } = require('./selectors');
+const { commentStringsFromDelimiters, getDelimitersForScope } = require('./comment-utils.js');
const createTree = require('./rb-tree');
@@ -16,6 +17,11 @@ const FEATURE_ASYNC_PARSE = true;
const LINE_LENGTH_LIMIT_FOR_HIGHLIGHTING = 10000;
+// How many milliseconds we can spend on synchronous re-parses (for indentation
+// purposes) in a given transaction before we fall back to asynchronous
+// indentation instead. Only comes into play when async indentation is enabled.
+const REPARSE_BUDGET_PER_TRANSACTION_MILLIS = 10
+
const PARSE_JOB_LIMIT_MICROS = 3000;
const PARSERS_IN_USE = new Set();
@@ -149,10 +155,14 @@ class WASMTreeSitterLanguageMode {
this.id = nextLanguageModeId++;
this.buffer = buffer;
this.grammar = grammar;
- this.config = config;
+ this.config = config ?? atom.config;
this.grammarRegistry = grammars;
this.syncTimeoutMicros = syncTimeoutMicros ?? PARSE_JOB_LIMIT_MICROS;
+ this.useAsyncParsing = FEATURE_ASYNC_PARSE;
+ this.useAsyncIndent = FEATURE_ASYNC_INDENT;
+ this.transactionReparseBudgetMs = REPARSE_BUDGET_PER_TRANSACTION_MILLIS;
+ this.currentTransactionReparseBudgetMs = undefined;
this.injectionsMarkerLayer = buffer.addMarkerLayer();
@@ -337,17 +347,29 @@ class WASMTreeSitterLanguageMode {
this.resolveNextTransactionPromise();
this.transactionChangeCount = 0;
this.autoIndentRequests = 0;
+ // Since a new transaction is starting, we can reset our reparse
+ // budget.
+ this.currentTransactionReparseBudgetMs = this.transactionReparseBudgetMs;
}
});
}
- emitRangeUpdate(range) {
+ // Invalidate fold caches for the rows touched by the given range.
+ //
+ // Invalidating syntax highlighting also invalidates fold caches for the same
+ // range, but this method allows us to invalidate parts of the fold cache
+ // without affecting syntax highlighting.
+ emitFoldUpdate(range) {
const startRow = range.start.row;
const endRow = range.end.row;
for (let row = startRow; row < endRow; row++) {
this.isFoldableCache[row] = undefined;
}
this.prefillFoldCache(range);
+ }
+
+ emitRangeUpdate(range) {
+ this.emitFoldUpdate(range);
this.emitter.emit('did-change-highlighting', range);
}
@@ -490,8 +512,8 @@ class WASMTreeSitterLanguageMode {
return this.grammar.scopeNameForScopeId(scopeId);
}
- idForScope(name) {
- return this.grammar.idForScope(name);
+ idForScope(name, text) {
+ return this.grammar.idForScope(name, text);
}
// Behaves like `scopeDescriptorForPosition`, but returns a list of
@@ -565,6 +587,9 @@ class WASMTreeSitterLanguageMode {
let layers = this.languageLayersAtPoint(point);
let results = [];
for (let layer of layers) {
+ if (layer.grammar.scopeName === selector) {
+ return layer.getExtent();
+ }
let items = layer.scopeMapAtPosition(point);
for (let { capture, adjustedRange } of items) {
let range = rangeForNode(adjustedRange);
@@ -773,8 +798,8 @@ class WASMTreeSitterLanguageMode {
getSyntaxNodeAndGrammarContainingRange(range, where = FUNCTION_TRUE) {
if (!this.rootLanguageLayer) { return { node: null, grammar: null }; }
- let layersAtStart = this.languageLayersAtPoint(range.start);
- let layersAtEnd = this.languageLayersAtPoint(range.end);
+ let layersAtStart = this.languageLayersAtPoint(range.start, { exact: true });
+ let layersAtEnd = this.languageLayersAtPoint(range.end, { exact: true });
let sharedLayers = layersAtStart.filter(
layer => layersAtEnd.includes(layer)
);
@@ -790,16 +815,17 @@ class WASMTreeSitterLanguageMode {
let rootNode = layer.tree.rootNode;
if (!rootNode.range.containsRange(range)) {
- if (layer === this.rootLanguageLayer) {
- // This layer is responsible for the entire buffer, but our tree's
- // root node may not actually span that entire range. If the buffer
- // starts with empty lines, the tree may not start parsing until the
- // first non-whitespace character.
- //
- // But this is the root language layer, so we're going to pretend
- // that our tree's root node spans the entire buffer range.
- results.push({ node: rootNode, grammar, depth });
- }
+ // There's often a difference between (a) the areas that we consider to
+ // be our canonical content ranges for a layer and (b) the range
+ // covered by the layer's root node. Root tree nodes usually ignore any
+ // whitespace that occurs before the first meaningful content of the
+ // node, but we consider that space to be under the purview of the
+ // layer all the same.
+ //
+ // If we've gotten this far, we've already decided that this layer
+ // includes this range. So let's just pretend that the root node covers
+ // this area.
+ results.push({ node: rootNode, grammar, depth });
continue;
}
@@ -875,17 +901,18 @@ class WASMTreeSitterLanguageMode {
let { depth, grammar } = layer;
let rootNode = layer.tree.rootNode;
if (!rootNode.range.containsPoint(position)) {
- if (layer === this.rootLanguageLayer) {
- // This layer is responsible for the entire buffer, but our tree's
- // root node may not actually span that entire range. If the buffer
- // starts with empty lines, the tree may not start parsing until the
- // first non-whitespace character.
- //
- // But this is the root language layer, so we're going to pretend
- // that our tree's root node spans the entire buffer range.
- if (where(rootNode, grammar)) {
- results.push({ rootNode: node, depth });
- }
+ // There's often a difference between (a) the areas that we consider to
+ // be our canonical content ranges for a layer and (b) the range
+ // covered by the layer's root node. Root tree nodes usually ignore any
+ // whitespace that occurs before the first meaningful content of the
+ // node, but we consider that space to be under the purview of the
+ // layer all the same.
+ //
+ // If we've gotten this far, we've already decided that this layer
+ // includes this point. So let's just pretend that the root node covers
+ // this area.
+ if (where(rootNode, grammar)) {
+ results.push({ rootNode: node, depth });
}
continue;
}
@@ -929,8 +956,10 @@ class WASMTreeSitterLanguageMode {
*/
getFoldableRangeContainingPoint(point) {
point = this.buffer.clipPosition(point);
- let fold = this.getFoldRangeForRow(point.row);
- if (fold) { return fold; }
+ if (point.column >= this.buffer.lineLengthForRow(point.row)) {
+ let fold = this.getFoldRangeForRow(point.row);
+ if (fold) { return fold; }
+ }
// Move backwards until we find a fold range containing this row.
for (let row = point.row - 1; row >= 0; row--) {
@@ -945,12 +974,12 @@ class WASMTreeSitterLanguageMode {
if (!this.tokenized) { return []; }
let layers = this.getAllLanguageLayers();
- let folds = [];
+ let allFolds = [];
for (let layer of layers) {
let folds = layer.foldResolver.getAllFoldRanges();
- folds.push(...folds);
+ allFolds.push(...folds);
}
- return folds;
+ return allFolds;
}
// This method is improperly named, and is based on an assumption that every
@@ -1061,12 +1090,33 @@ class WASMTreeSitterLanguageMode {
// scope-specific setting for scenarios where a language has different
// comment delimiters for different contexts.
//
- // TODO: Our understanding of the correct delimiters for a given buffer
- // position is only as granular as the entire buffer row. This can bite us in
- // edge cases like JSX. It's the right decision if the user toggles a comment
- // with an empty selection, but if specific buffer text is selected, we
- // should look up the right delmiters for that specific range. This will
- // require a new branch in the βEditor: Toggle Line Commentsβ command.
+ // Returns `commentStartString` and (sometimes) `commentEndString`
+ // properties. If only the former is a {String}, then βToggle Line Commentsβ
+ // will insert a line comment; if both are {String}s, it'll insert a block
+ // comment.
+ //
+ // NOTE: This method also returns a `commentDelimiters` property with
+ // metadata about the comment delimiters at the given position. Since the
+ // main purpose of this method, historically, has been to determine which
+ // delimiter(s) to use for the βToggle Line Commentβ command, we adjust the
+ // position we're given to cover the first non-whitespace content on the line
+ // for more accurate results. But `commentDelimiters` contains unadjusted
+ // data wherever possible because we don't make assumptions about how the
+ // caller will use the data.
+ //
+ // This might produce surprising results sometimes β like `commentDelimiters`
+ // containing delimiters from a different language than the delimiters in the
+ // other returned properties. But that's OK. Consumers of this function will
+ // know why those properties disagree and which one they're most interested
+ // in, and it still makes sense for these different use cases to share code.
+ //
+ // TODO: When toggling comments on a line or buffer range, our understanding
+ // of the correct delimiters for a given buffer position is only as granular
+ // as the entire buffer row. This can bite us in edge cases like JSX. It's
+ // the right decision if the user toggles a comment with an empty selection,
+ // but if specific buffer text is selected, we should look up the right
+ // delimiters for that specific range. This will require a new branch in the
+ // βEditor: Toggle Line Commentsβ command.
commentStringsForPosition(position) {
const range = this.firstNonWhitespaceRange(position.row) ||
new Range(position, position);
@@ -1079,28 +1129,72 @@ class WASMTreeSitterLanguageMode {
const commentEndEntries = this.config.getAll(
'editor.commentEnd', { scope });
- const commentStartEntry = commentStartEntries[0];
- const commentEndEntry = commentEndEntries.find(entry => (
- entry.scopeSelector === commentStartEntry.scopeSelector
- ));
+ // If a `commentDelimiters` setting exists, attach it to the return object.
+ // This can contain more comprehensive delimiter metadata for snippets and
+ // other purposes.
+ //
+ // This is just general metadata. We don't know the user's intended use
+ // case. So we should look up the scope descriptor of the _original_
+ // position, not the one at the beginning of the line.
+ const originalScope = this.scopeDescriptorForPosition(position);
+ const commentDelimiters = getDelimitersForScope(originalScope);
+
+ // The two config entries are separate, but if they're paired, then we need
+ // to make sure we're reading them from the same layer. Otherwise we could
+ // wind up with, say, `