Merge branch 'master' into settings-view-update-package-snippets-view

This commit is contained in:
confused_techie 2023-04-04 21:56:24 -07:00 committed by GitHub
commit c04aea2fb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
86 changed files with 43549 additions and 1852 deletions

View File

@ -7,6 +7,10 @@
## [Unreleased]
- The settings-view package now lists a packages snippets more accurately
- Fixed some issues with some packages with WebComponents v0 (tablr package
should work now) by internalizing and patching document-register-element
- Migrated away from `node-oniguruma` in favor of `vscode-oniguruma` (WASM
version). This fixes issues with Electron 21
## 1.103.0

9
crowdin.yml Normal file
View File

@ -0,0 +1,9 @@
preserve_hierarchy: true
commit_message: "[skip ci] Translations from Crowdin"
append_commit_message: false
files:
- source: /i18n/en.json
translation: /i18n/%locale%.json
- source: /packages/**/i18n/en.json
translation: /packages/**/i18n/%locale%.json

View File

@ -98,6 +98,60 @@
"warnings": []
}</p>
</dd>
<dt><a href="#chromiumElementsShim">chromiumElementsShim</a></dt>
<dd><p>This file will manage the updating of <code>autocomplete-html</code> <code>completions.json</code>
We will partially utilize <code>@webref/elements</code> <code>.listAll()</code> function that returns
a full list of HTML Elements along with a defined <code>interface</code>.
To use this <code>interface</code> in any meaningful way, we will utilize the dataset
of Attributes that apply to each <code>interface</code> from Chromiums DevTools resource
<code>https://github.com/ChromeDevTools/devtools-frontend</code>.
Finally from here we will utilize <code>https://github.com/mdn/content</code> to parse
the Markdown docs of MDN&#39;s website to retreive descriptions for each element.</p>
<p> Now for a summary of our <code>completions.json</code> file we aim to generate.
There are two top level elements, <code>tags</code> and <code>attributes</code>, both objects.
Within <code>tags</code> we expect the following:
&quot;tags&quot;: {
&quot;a&quot;: {
&quot;attributes&quot;: [ &quot;href&quot;, &quot;hreflang&quot;, &quot;media&quot;, &quot;rel&quot;, &quot;target&quot;, &quot;type&quot; ],
&quot;description&quot;: &quot;.....&quot;
}
};</p>
<p> When an entry contains no <code>attributes</code> there is no empty array, the element
simply doesn&#39;t exist.</p>
<p> The <code>attributes</code> object contains keys of different elements that themselves
are objects that can contain several valid keys.</p>
<ul>
<li>global: Seems to be used exclusively for Global Attributes. Is a boolean
which when false, the key does not appear.</li>
<li>type: A ?type? for the attribute. It&#39;s meaning is not immediately known.
Nor a way to reliabley programatically collect it. Some discovered values:</li>
</ul>
<p>cssStyle: Exclusively used for <code>class</code> attribute
boolean: Attributes that only accept <code>true</code> or <code>false</code>
flag: For attributes that don&#39;t require or accept values. eg autoplay
cssId: Exclusively used for the <code>id</code> attribute
color: Exclusively used for the <code>bgcolor</code> attribute
style: Exclusively used for the <code>style</code> attribute</p>
<ul>
<li>description: A text description of the attribute</li>
<li>attribOption: A string array of valid values that can exist within the attribute.
Such as the case with <code>rel</code> where only so many valid options exist.</li>
</ul>
<p> Although with our data sources mentioned above, we are able to collect nearly
all the data needed. Except the <code>type</code> that is defined within our
<code>completions.json</code> as well as the <code>attribOption</code> within our completions.</p>
<p> Studying these closer reveals that all attributes listing with our <code>completions.json</code>
do not appear elsewhere, and are nearly all global attributes.</p>
<p> In this case since there is no sane way to collect this data, we will leave this
list as a manually maintained section of our <code>completions.json</code>.
This does mean that <code>curated-attributes.json</code> is a static document that
will require manual updating in the future. Or most ideally, will find a way
to automatically generate the needed data.</p>
</dd>
<dt><a href="#update">update</a></dt>
<dd><p>This file aims to run some short simple tests against <code>update.js</code>. Focusing
mainly on the Regex used within <code>sanitizeDescription()</code></p>
</dd>
<dt><a href="#fs">fs</a></dt>
<dd></dd>
<dt><a href="#dalek">dalek</a></dt>
@ -376,6 +430,68 @@ This file will manage the updating of `autocomplete-css` `completions.json`.
"warnings": []
}
**Kind**: global constant
<a name="chromiumElementsShim"></a>
## chromiumElementsShim
This file will manage the updating of `autocomplete-html` `completions.json`
We will partially utilize `@webref/elements` `.listAll()` function that returns
a full list of HTML Elements along with a defined `interface`.
To use this `interface` in any meaningful way, we will utilize the dataset
of Attributes that apply to each `interface` from Chromiums DevTools resource
`https://github.com/ChromeDevTools/devtools-frontend`.
Finally from here we will utilize `https://github.com/mdn/content` to parse
the Markdown docs of MDN's website to retreive descriptions for each element.
Now for a summary of our `completions.json` file we aim to generate.
There are two top level elements, `tags` and `attributes`, both objects.
Within `tags` we expect the following:
"tags": {
"a": {
"attributes": [ "href", "hreflang", "media", "rel", "target", "type" ],
"description": "....."
}
};
When an entry contains no `attributes` there is no empty array, the element
simply doesn't exist.
The `attributes` object contains keys of different elements that themselves
are objects that can contain several valid keys.
- global: Seems to be used exclusively for Global Attributes. Is a boolean
which when false, the key does not appear.
- type: A ?type? for the attribute. It's meaning is not immediately known.
Nor a way to reliabley programatically collect it. Some discovered values:
cssStyle: Exclusively used for `class` attribute
boolean: Attributes that only accept `true` or `false`
flag: For attributes that don't require or accept values. eg autoplay
cssId: Exclusively used for the `id` attribute
color: Exclusively used for the `bgcolor` attribute
style: Exclusively used for the `style` attribute
- description: A text description of the attribute
- attribOption: A string array of valid values that can exist within the attribute.
Such as the case with `rel` where only so many valid options exist.
Although with our data sources mentioned above, we are able to collect nearly
all the data needed. Except the `type` that is defined within our
`completions.json` as well as the `attribOption` within our completions.
Studying these closer reveals that all attributes listing with our `completions.json`
do not appear elsewhere, and are nearly all global attributes.
In this case since there is no sane way to collect this data, we will leave this
list as a manually maintained section of our `completions.json`.
This does mean that `curated-attributes.json` is a static document that
will require manual updating in the future. Or most ideally, will find a way
to automatically generate the needed data.
**Kind**: global constant
<a name="update"></a>
## update
This file aims to run some short simple tests against `update.js`. Focusing
mainly on the Regex used within `sanitizeDescription()`
**Kind**: global constant
<a name="fs"></a>

View File

@ -98,6 +98,60 @@
&quot;warnings&quot;: []
}</p>
</dd>
<dt><a href="#chromiumElementsShim">chromiumElementsShim</a></dt>
<dd><p>This file will manage the updating of <code>autocomplete-html</code> <code>completions.json</code>
We will partially utilize <code>@webref/elements</code> <code>.listAll()</code> function that returns
a full list of HTML Elements along with a defined <code>interface</code>.
To use this <code>interface</code> in any meaningful way, we will utilize the dataset
of Attributes that apply to each <code>interface</code> from Chromiums DevTools resource
<code>https://github.com/ChromeDevTools/devtools-frontend</code>.
Finally from here we will utilize <code>https://github.com/mdn/content</code> to parse
the Markdown docs of MDN&#39;s website to retreive descriptions for each element.</p>
<p> Now for a summary of our <code>completions.json</code> file we aim to generate.
There are two top level elements, <code>tags</code> and <code>attributes</code>, both objects.
Within <code>tags</code> we expect the following:
&quot;tags&quot;: {
&quot;a&quot;: {
&quot;attributes&quot;: [ &quot;href&quot;, &quot;hreflang&quot;, &quot;media&quot;, &quot;rel&quot;, &quot;target&quot;, &quot;type&quot; ],
&quot;description&quot;: &quot;.....&quot;
}
};</p>
<p> When an entry contains no <code>attributes</code> there is no empty array, the element
simply doesn&#39;t exist.</p>
<p> The <code>attributes</code> object contains keys of different elements that themselves
are objects that can contain several valid keys.</p>
<ul>
<li>global: Seems to be used exclusively for Global Attributes. Is a boolean
which when false, the key does not appear.</li>
<li>type: A ?type? for the attribute. It&#39;s meaning is not immediately known.
Nor a way to reliabley programatically collect it. Some discovered values:</li>
</ul>
<p>cssStyle: Exclusively used for <code>class</code> attribute
boolean: Attributes that only accept <code>true</code> or <code>false</code>
flag: For attributes that don&#39;t require or accept values. eg autoplay
cssId: Exclusively used for the <code>id</code> attribute
color: Exclusively used for the <code>bgcolor</code> attribute
style: Exclusively used for the <code>style</code> attribute</p>
<ul>
<li>description: A text description of the attribute</li>
<li>attribOption: A string array of valid values that can exist within the attribute.
Such as the case with <code>rel</code> where only so many valid options exist.</li>
</ul>
<p> Although with our data sources mentioned above, we are able to collect nearly
all the data needed. Except the <code>type</code> that is defined within our
<code>completions.json</code> as well as the <code>attribOption</code> within our completions.</p>
<p> Studying these closer reveals that all attributes listing with our <code>completions.json</code>
do not appear elsewhere, and are nearly all global attributes.</p>
<p> In this case since there is no sane way to collect this data, we will leave this
list as a manually maintained section of our <code>completions.json</code>.
This does mean that <code>curated-attributes.json</code> is a static document that
will require manual updating in the future. Or most ideally, will find a way
to automatically generate the needed data.</p>
</dd>
<dt><a href="#update">update</a></dt>
<dd><p>This file aims to run some short simple tests against <code>update.js</code>. Focusing
mainly on the Regex used within <code>sanitizeDescription()</code></p>
</dd>
<dt><a href="#fs">fs</a></dt>
<dd></dd>
<dt><a href="#dalek">dalek</a></dt>
@ -384,6 +438,68 @@ This file will manage the updating of `autocomplete-css` `completions.json`.
"warnings": []
}
**Kind**: global constant
<a name="chromiumElementsShim"></a>
## chromiumElementsShim
This file will manage the updating of `autocomplete-html` `completions.json`
We will partially utilize `@webref/elements` `.listAll()` function that returns
a full list of HTML Elements along with a defined `interface`.
To use this `interface` in any meaningful way, we will utilize the dataset
of Attributes that apply to each `interface` from Chromiums DevTools resource
`https://github.com/ChromeDevTools/devtools-frontend`.
Finally from here we will utilize `https://github.com/mdn/content` to parse
the Markdown docs of MDN's website to retreive descriptions for each element.
Now for a summary of our `completions.json` file we aim to generate.
There are two top level elements, `tags` and `attributes`, both objects.
Within `tags` we expect the following:
"tags": {
"a": {
"attributes": [ "href", "hreflang", "media", "rel", "target", "type" ],
"description": "....."
}
};
When an entry contains no `attributes` there is no empty array, the element
simply doesn't exist.
The `attributes` object contains keys of different elements that themselves
are objects that can contain several valid keys.
- global: Seems to be used exclusively for Global Attributes. Is a boolean
which when false, the key does not appear.
- type: A ?type? for the attribute. It's meaning is not immediately known.
Nor a way to reliabley programatically collect it. Some discovered values:
cssStyle: Exclusively used for `class` attribute
boolean: Attributes that only accept `true` or `false`
flag: For attributes that don't require or accept values. eg autoplay
cssId: Exclusively used for the `id` attribute
color: Exclusively used for the `bgcolor` attribute
style: Exclusively used for the `style` attribute
- description: A text description of the attribute
- attribOption: A string array of valid values that can exist within the attribute.
Such as the case with `rel` where only so many valid options exist.
Although with our data sources mentioned above, we are able to collect nearly
all the data needed. Except the `type` that is defined within our
`completions.json` as well as the `attribOption` within our completions.
Studying these closer reveals that all attributes listing with our `completions.json`
do not appear elsewhere, and are nearly all global attributes.
In this case since there is no sane way to collect this data, we will leave this
list as a manually maintained section of our `completions.json`.
This does mean that `curated-attributes.json` is a static document that
will require manual updating in the future. Or most ideally, will find a way
to automatically generate the needed data.
**Kind**: global constant
<a name="update"></a>
## update
This file aims to run some short simple tests against `update.js`. Focusing
mainly on the Regex used within `sanitizeDescription()`
**Kind**: global constant
<a name="fs"></a>

169
i18n/en.json Normal file
View File

@ -0,0 +1,169 @@
{
"menu": {
"pulsar": {
"about": "About Pulsar",
"view-license": "View License",
"version": "Version VERSION",
"restart-and-install-update": "Restart and Install Update",
"check-for-update": "Check for Update",
"checking-for-update": "Checking for Update",
"downloading-update": "Downloading Update",
"preferences": "Preferences",
"config": "Config",
"init-script": "Init Script",
"keymap": "Keymap",
"snippets": "Snippets",
"stylesheet": "Stylesheet",
"install-shell-commands": "Install Shell Commands",
"quit": "Quit Pulsar"
},
"macos": {
"services": "Services",
"hide-self": "Hide Pulsar",
"hide-others": "Hide Others",
"show-all": "Show All"
},
"file": {
"self": "File",
"new-window": "New Window",
"new-file": "New File",
"open": "Open...",
"open-file": "Open File...",
"open-folder": "Open Folder...",
"add-project-folder": "Add Project Folder",
"project-history": {
"reopen-project": "Reopen Project",
"clear": "Clear Project History"
},
"reopen-last-item": "Reopen Last Item",
"save": "Save",
"save-as": "Save As...",
"save-all": "Save All",
"close-tab": "Close Tab",
"close-pane": "Close Pane",
"close-window": "Close Window"
},
"edit": {
"self": "Edit",
"undo": "Undo",
"redo": "Redo",
"cut": "Cut",
"copy": "Copy",
"copy-path": "Copy Path",
"paste": "Paste",
"paste-without-reformatting": "Paste Without Reformatting",
"select-all": "Select All",
"toggle-comments": "Toggle Comments",
"lines": {
"self": "Lines",
"indent": "Indent",
"outdent": "Outdent",
"auto-indent": "Auto Indent",
"move-up": "Move Line Up",
"move-down": "Move Line Down",
"duplicate": "Duplicate Lines",
"delete": "Delete Line",
"join": "Join Lines"
},
"columns": {
"self": "Columns",
"move-selection-left": "Move Selection Left",
"move-selection-right": "Move Selection Right"
},
"text": {
"self": "Text",
"upper-case": "Upper Case",
"lower-case": "Lower Case",
"delete-to-end-of-word": "Delete to End of Word",
"delete-to-previous-word-boundary": "Delete to Previous Word Boundary",
"delete-to-next-word-boundary": "Delete to Next Word Boundary",
"delete-line": "Delete Line",
"transpose": "Transpose"
},
"folding": {
"self": "Folding",
"fold": "Fold",
"unfold": "Unfold",
"fold-all": "Fold All",
"unfold-all": "Unfold All",
"fold-level-1": "Fold Level 1",
"fold-level-2": "Fold Level 2",
"fold-level-3": "Fold Level 3",
"fold-level-4": "Fold Level 4",
"fold-level-5": "Fold Level 5",
"fold-level-6": "Fold Level 6",
"fold-level-7": "Fold Level 7",
"fold-level-8": "Fold Level 8",
"fold-level-9": "Fold Level 9"
}
},
"view": {
"self": "View",
"toggle-full-screen": "Toggle Full Screen",
"toggle-menu-bar": "Toggle Menu Bar",
"panes": {
"self": "Panes",
"split-up": "Split Up",
"split-down": "Split Down",
"split-left": "Split Left",
"split-right": "Split Right",
"focus-next": "Focus Next Pane",
"focus-previous": "Focus Previous Pane",
"focus-above": "Focus Pane Above",
"focus-below": "Focus Pane Below",
"focus-on-left": "Focus Pane On Left",
"focus-on-right": "Focus Pane On Right",
"close": "Close Pane"
},
"developer": {
"self": "Developer",
"open-in-dev-mode": "Open In Dev Mode",
"reload-window": "Reload Window",
"run-package-specs": "Run Package Specs",
"toggle-dev-tools": "Toggle Developer Tools"
},
"increase-font-size": "Increase Font Size",
"decrease-font-size": "Decrease Font Size",
"reset-font-size": "Reset Font Size",
"toggle-soft-wrap": "Toggle Soft Wrap"
},
"selection": {
"self": "Selection",
"add-above": "Add Selection Above",
"add-below": "Add Selection Below",
"single": "Single Selection",
"split-into-lines": "Split into Lines",
"to-top": "Select to Top",
"to-bottom": "Select to Bottom",
"line": "Select Line",
"word": "Select Word",
"to-beginning-of-word": "Select to Beginning of Word",
"to-beginning-of-line": "Select to Beginning of Line",
"to-first-char-of-line": "Select to First Character of Line",
"to-end-of-word": "Select to End of Word",
"to-end-of-line": "Select to End of Line"
},
"find": {
"self": "Find"
},
"packages": {
"self": "Packages",
"open-package-manager": "Open Package Manager"
},
"window": {
"self": "Window",
"minimise": "Minimise",
"zoom": "Zoom",
"bring-all-to-front": "Bring All to Front"
},
"help": {
"self": "Help",
"terms-of-use": "Terms of Use",
"docs": "Documentation",
"faq": "Frequently Asked Questions",
"community-discussions": "Community Discussions",
"report-issue": "Report Issue",
"search-issues": "Search Issues"
}
}
}

View File

@ -2,227 +2,227 @@
{
label: 'Pulsar'
submenu: [
{ label: 'About Pulsar', command: 'application:about' }
{ label: 'View License', command: 'application:open-license' }
{ label: 'VERSION', enabled: false }
{ label: 'Restart and Install Update', command: 'application:install-update', visible: false}
{ label: 'Check for Update', command: 'application:check-for-update', visible: false}
{ label: 'Checking for Update', enabled: false, visible: false}
{ label: 'Downloading Update', enabled: false, visible: false}
{ localisedLabel: 'core.menu.pulsar.about', command: 'application:about' }
{ localisedLabel: 'core.menu.pulsar.view-license', command: 'application:open-license' }
{ localisedLabel: 'core.menu.pulsar.version', enabled: false }
{ localisedLabel: 'core.menu.pulsar.restart-and-install-update', command: 'application:install-update', visible: false }
{ localisedLabel: 'core.menu.pulsar.check-for-update', command: 'application:check-for-update', visible: false }
{ localisedLabel: 'core.menu.pulsar.checking-for-update', enabled: false, visible: false }
{ localisedLabel: 'core.menu.pulsar.downloading-update', enabled: false, visible: false }
{ type: 'separator' }
{ label: 'Preferences…', command: 'application:show-settings' }
{ localisedLabel: 'core.menu.pulsar.preferences', command: 'application:show-settings' }
{ type: 'separator' }
{ label: 'Config…', command: 'application:open-your-config' }
{ label: 'Init Script…', command: 'application:open-your-init-script' }
{ label: 'Keymap…', command: 'application:open-your-keymap' }
{ label: 'Snippets…', command: 'application:open-your-snippets' }
{ label: 'Stylesheet…', command: 'application:open-your-stylesheet' }
{ localisedLabel: 'core.menu.pulsar.config', command: 'application:open-your-config' }
{ localisedLabel: 'core.menu.pulsar.init-script', command: 'application:open-your-init-script' }
{ localisedLabel: 'core.menu.pulsar.keymap', command: 'application:open-your-keymap' }
{ localisedLabel: 'core.menu.pulsar.snippets', command: 'application:open-your-snippets' }
{ localisedLabel: 'core.menu.pulsar.stylesheet', command: 'application:open-your-stylesheet' }
{ type: 'separator' }
{ label: 'Install Shell Commands', command: 'window:install-shell-commands' }
{ localisedLabel: 'core.menu.pulsar.install-shell-commands', command: 'window:install-shell-commands' }
{ type: 'separator' }
{ label: 'Services', role: 'services', submenu: [] }
{ localisedLabel: 'core.menu.macos.services', role: 'services', submenu: [] }
{ type: 'separator' }
{ label: 'Hide Pulsar', command: 'application:hide' }
{ label: 'Hide Others', command: 'application:hide-other-applications' }
{ label: 'Show All', command: 'application:unhide-all-applications' }
{ localisedLabel: 'core.menu.macos.hide-self', command: 'application:hide' }
{ localisedLabel: 'core.menu.macos.hide-others', command: 'application:hide-other-applications' }
{ localisedLabel: 'core.menu.macos.show-all', command: 'application:unhide-all-applications' }
{ type: 'separator' }
{ label: 'Quit Pulsar', command: 'application:quit' }
{ localisedLabel: 'core.menu.pulsar.quit', command: 'application:quit' }
]
}
{
label: 'File'
localisedLabel: 'core.menu.file.self'
submenu: [
{ label: 'New Window', command: 'application:new-window' }
{ label: 'New File', command: 'application:new-file' }
{ label: 'Open…', command: 'application:open' }
{ label: 'Add Project Folder…', command: 'application:add-project-folder' }
{ localisedLabel: 'core.menu.file.new-window', command: 'application:new-window' }
{ localisedLabel: 'core.menu.file.new-file', command: 'application:new-file' }
{ localisedLabel: 'core.menu.file.open', command: 'application:open' }
{ localisedLabel: 'core.menu.file.add-project-folder', command: 'application:add-project-folder' }
{
label: 'Reopen Project',
localisedLabel: 'core.menu.file.project-history.reopen-project'
submenu: [
{ label: 'Clear Project History', command: 'application:clear-project-history' }
{ localisedLabel: 'core.menu.file.project-history.clear', command: 'application:clear-project-history' }
{ type: 'separator' }
]
}
{ label: 'Reopen Last Item', command: 'pane:reopen-closed-item' }
{ localisedLabel: 'core.menu.file.reopen-last-item', command: 'pane:reopen-closed-item' }
{ type: 'separator' }
{ label: 'Save', command: 'core:save' }
{ label: 'Save As…', command: 'core:save-as' }
{ label: 'Save All', command: 'window:save-all' }
{ localisedLabel: 'core.menu.file.save', command: 'core:save' }
{ localisedLabel: 'core.menu.file.save-as', command: 'core:save-as' }
{ localisedLabel: 'core.menu.file.save-all', command: 'window:save-all' }
{ type: 'separator' }
{ label: 'Close Tab', command: 'core:close' }
{ label: 'Close Pane', command: 'pane:close' }
{ label: 'Close Window', command: 'window:close' }
{ localisedLabel: 'core.menu.file.close-tab', command: 'core:close' }
{ localisedLabel: 'core.menu.file.close-pane', command: 'pane:close' }
{ localisedLabel: 'core.menu.file.close-window', command: 'window:close' }
]
}
{
label: 'Edit'
localisedLabel: 'core.menu.edit.self'
submenu: [
{ label: 'Undo', command: 'core:undo' }
{ label: 'Redo', command: 'core:redo' }
{ localisedLabel: 'core.menu.edit.undo', command: 'core:undo' }
{ localisedLabel: 'core.menu.edit.redo', command: 'core:redo' }
{ type: 'separator' }
{ label: 'Cut', command: 'core:cut' }
{ label: 'Copy', command: 'core:copy' }
{ label: 'Copy Path', command: 'editor:copy-path' }
{ label: 'Paste', command: 'core:paste' }
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
{ label: 'Select All', command: 'core:select-all' }
{ localisedLabel: 'core.menu.edit.cut', command: 'core:cut' }
{ localisedLabel: 'core.menu.edit.copy', command: 'core:copy' }
{ localisedLabel: 'core.menu.edit.copy-path', command: 'editor:copy-path' }
{ localisedLabel: 'core.menu.edit.paste', command: 'core:paste' }
{ localisedLabel: 'core.menu.edit.paste-without-reformatting', command: 'editor:paste-without-reformatting' }
{ localisedLabel: 'core.menu.edit.select-all', command: 'core:select-all' }
{ type: 'separator' }
{ label: 'Toggle Comments', command: 'editor:toggle-line-comments' }
{ localisedLabel: 'core.menu.edit.toggle-comments', command: 'editor:toggle-line-comments' }
{
label: 'Lines',
localisedLabel: 'core.menu.edit.lines.self',
submenu: [
{ label: 'Indent', command: 'editor:indent-selected-rows' }
{ label: 'Outdent', command: 'editor:outdent-selected-rows' }
{ label: 'Auto Indent', command: 'editor:auto-indent' }
{ localisedLabel: 'core.menu.edit.lines.indent', command: 'editor:indent-selected-rows' }
{ localisedLabel: 'core.menu.edit.lines.outdent', command: 'editor:outdent-selected-rows' }
{ localisedLabel: 'core.menu.edit.lines.auto-indent', command: 'editor:auto-indent' }
{ type: 'separator' }
{ label: 'Move Line Up', command: 'editor:move-line-up' }
{ label: 'Move Line Down', command: 'editor:move-line-down' }
{ label: 'Duplicate Lines', command: 'editor:duplicate-lines' }
{ label: 'Delete Line', command: 'editor:delete-line' }
{ label: 'Join Lines', command: 'editor:join-lines' }
{ localisedLabel: 'core.menu.edit.lines.move-up', command: 'editor:move-line-up' }
{ localisedLabel: 'core.menu.edit.lines.move-down', command: 'editor:move-line-down' }
{ localisedLabel: 'core.menu.edit.lines.duplicate', command: 'editor:duplicate-lines' }
{ localisedLabel: 'core.menu.edit.lines.delete', command: 'editor:delete-line' }
{ localisedLabel: 'core.menu.edit.lines.join', command: 'editor:join-lines' }
]
}
{
label: 'Columns',
localisedLabel: 'core.menu.edit.columns.self'
submenu: [
{ label: 'Move Selection Left', command: 'editor:move-selection-left' }
{ label: 'Move Selection Right', command: 'editor:move-selection-right' }
{ localisedLabel: 'core.menu.edit.columns.move-selection-left', command: 'editor:move-selection-left' }
{ localisedLabel: 'core.menu.edit.columns.move-selection-right', command: 'editor:move-selection-right' }
]
}
{
label: 'Text',
localisedLabel: 'core.menu.edit.text.self'
submenu: [
{ label: 'Upper Case', command: 'editor:upper-case' }
{ label: 'Lower Case', command: 'editor:lower-case' }
{ localisedLabel: 'core.menu.edit.text.upper-case', command: 'editor:upper-case' }
{ localisedLabel: 'core.menu.edit.text.lower-case', command: 'editor:lower-case' }
{ type: 'separator' }
{ label: 'Delete to End of Word', command: 'editor:delete-to-end-of-word' }
{ label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' }
{ label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' }
{ label: 'Delete Line', command: 'editor:delete-line' }
{ localisedLabel: 'core.menu.edit.text.delete-to-end-of-word', command: 'editor:delete-to-end-of-word' }
{ localisedLabel: 'core.menu.edit.text.delete-to-previous-word-boundary', command: 'editor:delete-to-previous-word-boundary' }
{ localisedLabel: 'core.menu.edit.text.delete-to-next-word-boundary', command: 'editor:delete-to-next-word-boundary' }
{ localisedLabel: 'core.menu.edit.text.delete-line', command: 'editor:delete-line' }
{ type: 'separator' }
{ label: 'Transpose', command: 'editor:transpose' }
{ localisedLabel: 'core.menu.edit.text.transpose', command: 'editor:transpose' }
]
}
{
label: 'Folding',
localisedLabel: 'core.menu.edit.folding.self'
submenu: [
{ label: 'Fold', command: 'editor:fold-current-row' }
{ label: 'Unfold', command: 'editor:unfold-current-row' }
{ label: 'Fold All', command: 'editor:fold-all' }
{ label: 'Unfold All', command: 'editor:unfold-all' }
{ localisedLabel: 'core.menu.edit.folding.fold', command: 'editor:fold-current-row' }
{ localisedLabel: 'core.menu.edit.folding.unfold', command: 'editor:unfold-current-row' }
{ localisedLabel: 'core.menu.edit.folding.fold-all', command: 'editor:fold-all' }
{ localisedLabel: 'core.menu.edit.folding.unfold-all', command: 'editor:unfold-all' }
{ type: 'separator' }
{ label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' }
{ label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' }
{ label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' }
{ label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' }
{ label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' }
{ label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' }
{ label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' }
{ label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' }
{ label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-1', command: 'editor:fold-at-indent-level-1' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-2', command: 'editor:fold-at-indent-level-2' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-3', command: 'editor:fold-at-indent-level-3' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-4', command: 'editor:fold-at-indent-level-4' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-5', command: 'editor:fold-at-indent-level-5' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-6', command: 'editor:fold-at-indent-level-6' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-7', command: 'editor:fold-at-indent-level-7' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-8', command: 'editor:fold-at-indent-level-8' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-9', command: 'editor:fold-at-indent-level-9' }
]
}
]
}
{
label: 'View'
localisedLabel: 'core.menu.view.self'
submenu: [
{ label: 'Toggle Full Screen', command: 'window:toggle-full-screen' }
{ localisedLabel: 'core.menu.view.toggle-full-screen', command: 'window:toggle-full-screen' }
{
label: 'Panes'
localisedLabel: 'core.menu.view.panes.self'
submenu: [
{ label: 'Split Up', command: 'pane:split-up-and-copy-active-item' }
{ label: 'Split Down', command: 'pane:split-down-and-copy-active-item' }
{ label: 'Split Left', command: 'pane:split-left-and-copy-active-item' }
{ label: 'Split Right', command: 'pane:split-right-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-up', command: 'pane:split-up-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-down', command: 'pane:split-down-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-left', command: 'pane:split-left-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-right', command: 'pane:split-right-and-copy-active-item' }
{ type: 'separator' }
{ label: 'Focus Next Pane', command: 'window:focus-next-pane' }
{ label: 'Focus Previous Pane', command: 'window:focus-previous-pane' }
{ localisedLabel: 'core.menu.view.panes.focus-next', command: 'window:focus-next-pane' }
{ localisedLabel: 'core.menu.view.panes.focus-previous', command: 'window:focus-previous-pane' }
{ type: 'separator' }
{ label: 'Focus Pane Above', command: 'window:focus-pane-above' }
{ label: 'Focus Pane Below', command: 'window:focus-pane-below' }
{ label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' }
{ label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' }
{ localisedLabel: 'core.menu.view.panes.focus-above', command: 'window:focus-pane-above' }
{ localisedLabel: 'core.menu.view.panes.focus-below', command: 'window:focus-pane-below' }
{ localisedLabel: 'core.menu.view.panes.focus-on-left', command: 'window:focus-pane-on-left' }
{ localisedLabel: 'core.menu.view.panes.focus-on-right', command: 'window:focus-pane-on-right' }
{ type: 'separator' }
{ label: 'Close Pane', command: 'pane:close' }
{ localisedLabel: 'core.menu.view.panes.close', command: 'pane:close' }
]
}
{
label: 'Developer'
localisedLabel: 'core.menu.view.developer.self'
submenu: [
{ label: 'Open In Dev Mode…', command: 'application:open-dev' }
{ label: 'Reload Window', command: 'window:reload' }
{ label: 'Run Package Specs', command: 'window:run-package-specs' }
{ label: 'Toggle Developer Tools', command: 'window:toggle-dev-tools' }
{ localisedLabel: 'core.menu.view.developer.open-in-dev-mode', command: 'application:open-dev' }
{ localisedLabel: 'core.menu.view.developer.reload-window', command: 'window:reload' }
{ localisedLabel: 'core.menu.view.developer.run-package-specs', command: 'window:run-package-specs' }
{ localisedLabel: 'core.menu.view.developer.toggle-dev-tools', command: 'window:toggle-dev-tools' }
]
}
{ type: 'separator' }
{ label: 'Increase Font Size', command: 'window:increase-font-size' }
{ label: 'Decrease Font Size', command: 'window:decrease-font-size' }
{ label: 'Reset Font Size', command: 'window:reset-font-size' }
{ localisedLabel: 'core.menu.view.increase-font-size', command: 'window:increase-font-size' }
{ localisedLabel: 'core.menu.view.decrease-font-size', command: 'window:decrease-font-size' }
{ localisedLabel: 'core.menu.view.reset-font-size', command: 'window:reset-font-size' }
{ type: 'separator' }
{ label: 'Toggle Soft Wrap', command: 'editor:toggle-soft-wrap' }
{ localisedLabel: 'core.menu.view.toggle-soft-wrap', command: 'editor:toggle-soft-wrap' }
]
}
{
label: 'Selection'
localisedLabel: 'core.menu.selection.self'
submenu: [
{ label: 'Add Selection Above', command: 'editor:add-selection-above' }
{ label: 'Add Selection Below', command: 'editor:add-selection-below' }
{ label: 'Single Selection', command: 'editor:consolidate-selections'}
{ label: 'Split into Lines', command: 'editor:split-selections-into-lines'}
{ localisedLabel: 'core.menu.selection.add-above', command: 'editor:add-selection-above' }
{ localisedLabel: 'core.menu.selection.add-below', command: 'editor:add-selection-below' }
{ localisedLabel: 'core.menu.selection.single', command: 'editor:consolidate-selections' }
{ localisedLabel: 'core.menu.selection.split-into-lines', command: 'editor:split-selections-into-lines' }
{ type: 'separator' }
{ label: 'Select to Top', command: 'core:select-to-top' }
{ label: 'Select to Bottom', command: 'core:select-to-bottom' }
{ localisedLabel: 'core.menu.selection.to-top', command: 'core:select-to-top' }
{ localisedLabel: 'core.menu.selection.to-bottom', command: 'core:select-to-bottom' }
{ type: 'separator' }
{ label: 'Select Line', command: 'editor:select-line' }
{ label: 'Select Word', command: 'editor:select-word' }
{ label: 'Select to Beginning of Word', command: 'editor:select-to-beginning-of-word' }
{ label: 'Select to Beginning of Line', command: 'editor:select-to-beginning-of-line' }
{ label: 'Select to First Character of Line', command: 'editor:select-to-first-character-of-line' }
{ label: 'Select to End of Word', command: 'editor:select-to-end-of-word' }
{ label: 'Select to End of Line', command: 'editor:select-to-end-of-line' }
{ localisedLabel: 'core.menu.selection.line', command: 'editor:select-line' }
{ localisedLabel: 'core.menu.selection.word', command: 'editor:select-word' }
{ localisedLabel: 'core.menu.selection.to-beginning-of-word', command: 'editor:select-to-beginning-of-word' }
{ localisedLabel: 'core.menu.selection.to-beginning-of-line', command: 'editor:select-to-beginning-of-line' }
{ localisedLabel: 'core.menu.selection.to-first-char-of-line', command: 'editor:select-to-first-character-of-line' }
{ localisedLabel: 'core.menu.selection.to-end-of-word', command: 'editor:select-to-end-of-word' }
{ localisedLabel: 'core.menu.selection.to-end-of-line', command: 'editor:select-to-end-of-line' }
]
}
{
label: 'Find'
localisedLabel: 'core.menu.find.self'
submenu: []
}
{
label: 'Packages'
localisedLabel: 'core.menu.packages.self'
submenu: [
{ label: 'Open Package Manager', command: 'settings-view:view-installed-packages' }
{ localisedLabel: 'core.menu.packages.open-package-manager', command: 'settings-view:view-installed-packages' }
{ type: 'separator' }
]
}
{
label: 'Window'
localisedLabel: 'core.menu.window.self'
role: 'window'
submenu: [
{ label: 'Minimize', command: 'application:minimize' }
{ label: 'Zoom', command: 'application:zoom' }
{ localisedLabel: 'core.menu.window.minimise', command: 'application:minimize' }
{ localisedLabel: 'core.menu.window.zoom', command: 'application:zoom' }
{ type: 'separator' }
{ label: 'Bring All to Front', command: 'application:bring-all-windows-to-front' }
{ localisedLabel: 'core.menu.window.bring-all-to-front', command: 'application:bring-all-windows-to-front' }
]
}
{
label: 'Help'
localisedLabel: 'core.menu.help.self'
role: 'help'
submenu: [
{ label: 'Terms of Use', command: 'application:open-terms-of-use' }
{ label: 'Documentation', command: 'application:open-documentation' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ localisedLabel: 'core.menu.help.terms-of-use', command: 'application:open-terms-of-use' }
{ localisedLabel: 'core.menu.help.docs', command: 'application:open-documentation' }
{ localisedLabel: 'core.menu.help.faq', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }
{ label: 'Report Issue', command: 'application:report-issue' }
{ label: 'Search Issues', command: 'application:search-issues' }
{ localisedLabel: 'core.menu.help.community-discussions', command: 'application:open-discussions' }
{ localisedLabel: 'core.menu.help.report-issue', command: 'application:report-issue' }
{ localisedLabel: 'core.menu.help.search-issues', command: 'application:search-issues' }
{ type: 'separator' }
]
}

View File

@ -1,204 +1,204 @@
'menu': [
{
label: '&File'
localisedLabel: 'core.menu.file.self' # accelerator F
submenu: [
{ label: 'New &Window', command: 'application:new-window' }
{ label: '&New File', command: 'application:new-file' }
{ label: '&Open File…', command: 'application:open-file' }
{ label: 'Open Folder…', command: 'application:open-folder' }
{ label: 'Add Project Folder…', command: 'application:add-project-folder' }
{ localisedLabel: 'core.menu.file.new-window', command: 'application:new-window' } # accelerator W
{ localisedLabel: 'core.menu.file.new-file', command: 'application:new-file' } # accelerator N
{ localisedLabel: 'core.menu.file.open-file', command: 'application:open-file' } # accelerator O
{ localisedLabel: 'core.menu.file.open-folder', command: 'application:open-folder' }
{ localisedLabel: 'core.menu.file.add-project-folder', command: 'application:add-project-folder' }
{
label: 'Reopen Project',
localisedLabel: 'core.menu.file.project-history.reopen-project'
submenu: [
{ label: 'Clear Project History', command: 'application:clear-project-history' }
{ localisedLabel: 'core.menu.file.project-history.clear', command: 'application:clear-project-history' }
{ type: 'separator' }
]
}
{ label: 'Reopen Last &Item', command: 'pane:reopen-closed-item' }
{ localisedLabel: 'core.menu.file.reopen-last-item', command: 'pane:reopen-closed-item' } # accelerator I
{ type: 'separator' }
{ label: '&Save', command: 'core:save' }
{ label: 'Save &As…', command: 'core:save-as' }
{ label: 'Save A&ll', command: 'window:save-all' }
{ localisedLabel: 'core.menu.file.save', command: 'core:save' } # accelerator S
{ localisedLabel: 'core.menu.file.save-as', command: 'core:save-as' } # accelerator A
{ localisedLabel: 'core.menu.file.save-all', command: 'window:save-all' } # accelerator L
{ type: 'separator' }
{ label: '&Close Tab', command: 'core:close' }
{ label: 'Close &Pane', command: 'pane:close' }
{ label: 'Clos&e Window', command: 'window:close' }
{ localisedLabel: 'core.menu.file.close-tab', command: 'core:close' } # accelerator C
{ localisedLabel: 'core.menu.file.close-pane', command: 'pane:close' } # accelerator P
{ localisedLabel: 'core.menu.file.close-window', command: 'window:close' } # accelerator E
{ type: 'separator' }
{ label: 'Quit', command: 'application:quit' }
{ localisedLabel: 'core.menu.pulsar.quit', command: 'application:quit' }
]
}
{
label: '&Edit'
localisedLabel: 'core.menu.edit.self' # accelerator E
submenu: [
{ label: '&Undo', command: 'core:undo' }
{ label: '&Redo', command: 'core:redo' }
{ localisedLabel: 'core.menu.edit.undo', command: 'core:undo' } # accelerator U
{ localisedLabel: 'core.menu.edit.redo', command: 'core:redo' } # accelerator R
{ type: 'separator' }
{ label: '&Cut', command: 'core:cut' }
{ label: 'C&opy', command: 'core:copy' }
{ label: 'Copy Pat&h', command: 'editor:copy-path' }
{ label: '&Paste', command: 'core:paste' }
{ label: 'Paste Without Reformatting', command: 'editor:paste-without-reformatting' }
{ label: 'Select &All', command: 'core:select-all' }
{ localisedLabel: 'core.menu.edit.cut', command: 'core:cut' } # accelerator C
{ localisedLabel: 'core.menu.edit.copy', command: 'core:copy' } # accelerator O
{ localisedLabel: 'core.menu.edit.copy-path', command: 'editor:copy-path' } # accelerator H
{ localisedLabel: 'core.menu.edit.paste', command: 'core:paste' } # accelerator P
{ localisedLabel: 'core.menu.edit.paste-without-reformatting', command: 'editor:paste-without-reformatting' }
{ localisedLabel: 'core.menu.edit.select-all', command: 'core:select-all' } # accelerator A
{ type: 'separator' }
{ label: '&Toggle Comments', command: 'editor:toggle-line-comments' }
{ localisedLabel: 'core.menu.edit.toggle-comments', command: 'editor:toggle-line-comments' } # accelerator T
{
label: 'Lines',
localisedLabel: 'core.menu.edit.lines.self'
submenu: [
{ label: '&Indent', command: 'editor:indent-selected-rows' }
{ label: '&Outdent', command: 'editor:outdent-selected-rows' }
{ label: '&Auto Indent', command: 'editor:auto-indent' }
{ localisedLabel: 'core.menu.edit.lines.indent', command: 'editor:indent-selected-rows' } # accelerator I
{ localisedLabel: 'core.menu.edit.lines.outdent', command: 'editor:outdent-selected-rows' } # accelerator O
{ localisedLabel: 'core.menu.edit.lines.auto-indent', command: 'editor:auto-indent' } # accelerator A
{ type: 'separator' }
{ label: 'Move Line &Up', command: 'editor:move-line-up' }
{ label: 'Move Line &Down', command: 'editor:move-line-down' }
{ label: 'Du&plicate Lines', command: 'editor:duplicate-lines' }
{ label: 'D&elete Line', command: 'editor:delete-line' }
{ label: '&Join Lines', command: 'editor:join-lines' }
{ localisedLabel: 'core.menu.edit.lines.move-up', command: 'editor:move-line-up' } # accelerator U
{ localisedLabel: 'core.menu.edit.lines.move-down', command: 'editor:move-line-down' } # accelerator D
{ localisedLabel: 'core.menu.edit.lines.duplicate', command: 'editor:duplicate-lines' } # accelerator P
{ localisedLabel: 'core.menu.edit.lines.delete', command: 'editor:delete-line' } # accelerator E
{ localisedLabel: 'core.menu.edit.lines.join', command: 'editor:join-lines' } # accelerator J
]
}
{
label: 'Columns',
localisedLabel: 'core.menu.edit.columns.self'
submenu: [
{ label: 'Move Selection &Left', command: 'editor:move-selection-left' }
{ label: 'Move Selection &Right', command: 'editor:move-selection-right' }
{ localisedLabel: 'core.menu.edit.columns.move-selection-left', command: 'editor:move-selection-left' } # accelerator L
{ localisedLabel: 'core.menu.edit.columns.move-selection-right', command: 'editor:move-selection-right' } # accelerator R
]
}
{
label: 'Text',
localisedLabel: 'core.menu.edit.text.self'
submenu: [
{ label: '&Upper Case', command: 'editor:upper-case' }
{ label: '&Lower Case', command: 'editor:lower-case' }
{ localisedLabel: 'core.menu.edit.text.upper-case', command: 'editor:upper-case' } # accelerator U
{ localisedLabel: 'core.menu.edit.text.lower-case', command: 'editor:lower-case' } # accelerator L
{ type: 'separator' }
{ label: 'Delete to End of &Word', command: 'editor:delete-to-end-of-word' }
{ label: 'Delete to Previous Word Boundary', command: 'editor:delete-to-previous-word-boundary' }
{ label: 'Delete to Next Word Boundary', command: 'editor:delete-to-next-word-boundary' }
{ label: '&Delete Line', command: 'editor:delete-line' }
{ localisedLabel: 'core.menu.edit.text.delete-to-end-of-word', command: 'editor:delete-to-end-of-word' } # accelerator W
{ localisedLabel: 'core.menu.edit.text.delete-to-previous-word-boundary', command: 'editor:delete-to-previous-word-boundary' }
{ localisedLabel: 'core.menu.edit.text.delete-to-next-word-boundary', command: 'editor:delete-to-next-word-boundary' }
{ localisedLabel: 'core.menu.edit.text.delete-line', command: 'editor:delete-line' } # accelerator D
{ type: 'separator' }
{ label: '&Transpose', command: 'editor:transpose' }
{ localisedLabel: 'core.menu.edit.text.transpose', command: 'editor:transpose' } # accelerator T
]
}
{
label: 'Folding',
localisedLabel: 'core.menu.edit.folding.self'
submenu: [
{ label: '&Fold', command: 'editor:fold-current-row' }
{ label: '&Unfold', command: 'editor:unfold-current-row' }
{ label: 'Fol&d All', command: 'editor:fold-all' }
{ label: 'Unfold &All', command: 'editor:unfold-all' }
{ localisedLabel: 'core.menu.edit.folding.fold', command: 'editor:fold-current-row' } # accelerator F
{ localisedLabel: 'core.menu.edit.folding.unfold', command: 'editor:unfold-current-row' } # accelerator U
{ localisedLabel: 'core.menu.edit.folding.fold-all', command: 'editor:fold-all' } # accelerator D
{ localisedLabel: 'core.menu.edit.folding.unfold-all', command: 'editor:unfold-all' } # accelerator A
{ type: 'separator' }
{ label: 'Fold Level 1', command: 'editor:fold-at-indent-level-1' }
{ label: 'Fold Level 2', command: 'editor:fold-at-indent-level-2' }
{ label: 'Fold Level 3', command: 'editor:fold-at-indent-level-3' }
{ label: 'Fold Level 4', command: 'editor:fold-at-indent-level-4' }
{ label: 'Fold Level 5', command: 'editor:fold-at-indent-level-5' }
{ label: 'Fold Level 6', command: 'editor:fold-at-indent-level-6' }
{ label: 'Fold Level 7', command: 'editor:fold-at-indent-level-7' }
{ label: 'Fold Level 8', command: 'editor:fold-at-indent-level-8' }
{ label: 'Fold Level 9', command: 'editor:fold-at-indent-level-9' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-1', command: 'editor:fold-at-indent-level-1' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-2', command: 'editor:fold-at-indent-level-2' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-3', command: 'editor:fold-at-indent-level-3' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-4', command: 'editor:fold-at-indent-level-4' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-5', command: 'editor:fold-at-indent-level-5' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-6', command: 'editor:fold-at-indent-level-6' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-7', command: 'editor:fold-at-indent-level-7' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-8', command: 'editor:fold-at-indent-level-8' }
{ localisedLabel: 'core.menu.edit.folding.fold-level-9', command: 'editor:fold-at-indent-level-9' }
]
}
{ type: 'separator' }
{ label: '&Preferences', command: 'application:show-settings' }
{ localisedLabel: 'core.menu.pulsar.preferences', command: 'application:show-settings' } # accelerator P
{ type: 'separator' }
{ label: 'Config…', command: 'application:open-your-config' }
{ label: 'Init Script…', command: 'application:open-your-init-script' }
{ label: 'Keymap…', command: 'application:open-your-keymap' }
{ label: 'Snippets…', command: 'application:open-your-snippets' }
{ label: 'Stylesheet…', command: 'application:open-your-stylesheet' }
{ localisedLabel: 'core.menu.pulsar.config', command: 'application:open-your-config' }
{ localisedLabel: 'core.menu.pulsar.init-script', command: 'application:open-your-init-script' }
{ localisedLabel: 'core.menu.pulsar.keymap', command: 'application:open-your-keymap' }
{ localisedLabel: 'core.menu.pulsar.snippets', command: 'application:open-your-snippets' }
{ localisedLabel: 'core.menu.pulsar.stylesheet', command: 'application:open-your-stylesheet' }
{ type: 'separator' }
]
}
{
label: '&View'
localisedLabel: 'core.menu.view.self' # accelerator V
submenu: [
{ label: 'Toggle &Full Screen', command: 'window:toggle-full-screen' }
{ label: 'Toggle Menu Bar', command: 'window:toggle-menu-bar' }
{ localisedLabel: 'core.menu.view.toggle-full-screen', command: 'window:toggle-full-screen' } # accelerator F
{ localisedLabel: 'core.menu.view.toggle-menu-bar', command: 'window:toggle-menu-bar' }
{
label: 'Panes'
localisedLabel: 'core.menu.view.panes.self'
submenu: [
{ label: 'Split Up', command: 'pane:split-up-and-copy-active-item' }
{ label: 'Split Down', command: 'pane:split-down-and-copy-active-item' }
{ label: 'Split Left', command: 'pane:split-left-and-copy-active-item' }
{ label: 'Split Right', command: 'pane:split-right-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-up', command: 'pane:split-up-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-down', command: 'pane:split-down-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-left', command: 'pane:split-left-and-copy-active-item' }
{ localisedLabel: 'core.menu.view.panes.split-right', command: 'pane:split-right-and-copy-active-item' }
{ type: 'separator' }
{ label: 'Focus Next Pane', command: 'window:focus-next-pane' }
{ label: 'Focus Previous Pane', command: 'window:focus-previous-pane' }
{ localisedLabel: 'core.menu.view.panes.focus-next', command: 'window:focus-next-pane' }
{ localisedLabel: 'core.menu.view.panes.focus-previous', command: 'window:focus-previous-pane' }
{ type: 'separator' }
{ label: 'Focus Pane Above', command: 'window:focus-pane-above' }
{ label: 'Focus Pane Below', command: 'window:focus-pane-below' }
{ label: 'Focus Pane On Left', command: 'window:focus-pane-on-left' }
{ label: 'Focus Pane On Right', command: 'window:focus-pane-on-right' }
{ localisedLabel: 'core.menu.view.panes.focus-above', command: 'window:focus-pane-above' }
{ localisedLabel: 'core.menu.view.panes.focus-below', command: 'window:focus-pane-below' }
{ localisedLabel: 'core.menu.view.panes.focus-on-left', command: 'window:focus-pane-on-left' }
{ localisedLabel: 'core.menu.view.panes.focus-on-right', command: 'window:focus-pane-on-right' }
{ type: 'separator' }
{ label: 'Close Pane', command: 'pane:close' }
{ localisedLabel: 'core.menu.view.panes.close', command: 'pane:close' }
]
}
{
label: 'Developer'
localisedLabel: 'core.menu.view.developer.self'
submenu: [
{ label: 'Open In &Dev Mode…', command: 'application:open-dev' }
{ label: '&Reload Window', command: 'window:reload' }
{ label: 'Run Package &Specs', command: 'window:run-package-specs' }
{ label: 'Toggle Developer &Tools', command: 'window:toggle-dev-tools' }
{ localisedLabel: 'core.menu.view.developer.open-in-dev-mode', command: 'application:open-dev' } # accelerator D
{ localisedLabel: 'core.menu.view.developer.reload-window', command: 'window:reload' } # accelerator R
{ localisedLabel: 'core.menu.view.developer.run-package-specs', command: 'window:run-package-specs' } # accelerator S
{ localisedLabel: 'core.menu.view.developer.toggle-dev-tools', command: 'window:toggle-dev-tools' } # accelerator T
]
}
{ type: 'separator' }
{ label: '&Increase Font Size', command: 'window:increase-font-size' }
{ label: '&Decrease Font Size', command: 'window:decrease-font-size' }
{ label: 'Re&set Font Size', command: 'window:reset-font-size' }
{ localisedLabel: 'core.menu.view.increase-font-size', command: 'window:increase-font-size' } # accelerator I
{ localisedLabel: 'core.menu.view.decrease-font-size', command: 'window:decrease-font-size' } # accelerator D
{ localisedLabel: 'core.menu.view.reset-font-size', command: 'window:reset-font-size' } # accelerator S
{ type: 'separator' }
{ label: 'Toggle Soft &Wrap', command: 'editor:toggle-soft-wrap' }
{ localisedLabel: 'core.menu.view.toggle-soft-wrap', command: 'editor:toggle-soft-wrap' } # accelerator W
]
}
{
label: '&Selection'
localisedLabel: 'core.menu.selection.self' # accelerator S
submenu: [
{ label: 'Add Selection &Above', command: 'editor:add-selection-above' }
{ label: 'Add Selection &Below', command: 'editor:add-selection-below' }
{ label: 'S&plit into Lines', command: 'editor:split-selections-into-lines'}
{ label: 'Single Selection', command: 'editor:consolidate-selections'}
{ localisedLabel: 'core.menu.selection.add-above', command: 'editor:add-selection-above' } # accelerator A
{ localisedLabel: 'core.menu.selection.add-below', command: 'editor:add-selection-below' } # accelerator B
{ localisedLabel: 'core.menu.selection.single', command: 'editor:consolidate-selections' }
{ localisedLabel: 'core.menu.selection.split-into-lines', command: 'editor:split-selections-into-lines' } # accelerator P
{ type: 'separator' }
{ label: 'Select to &Top', command: 'core:select-to-top' }
{ label: 'Select to Botto&m', command: 'core:select-to-bottom' }
{ localisedLabel: 'core.menu.selection.to-top', command: 'core:select-to-top' } # accelerator T
{ localisedLabel: 'core.menu.selection.to-bottom', command: 'core:select-to-bottom' } # accelerator M
{ type: 'separator' }
{ label: 'Select &Line', command: 'editor:select-line' }
{ label: 'Select &Word', command: 'editor:select-word' }
{ label: 'Select to Beginning of W&ord', command: 'editor:select-to-beginning-of-word' }
{ label: 'Select to Beginning of L&ine', command: 'editor:select-to-beginning-of-line' }
{ label: 'Select to First &Character of Line', command: 'editor:select-to-first-character-of-line' }
{ label: 'Select to End of Wor&d', command: 'editor:select-to-end-of-word' }
{ label: 'Select to End of Lin&e', command: 'editor:select-to-end-of-line' }
{ localisedLabel: 'core.menu.selection.line', command: 'editor:select-line' } # accelerator L
{ localisedLabel: 'core.menu.selection.word', command: 'editor:select-word' } # accelerator W
{ localisedLabel: 'core.menu.selection.to-beginning-of-word', command: 'editor:select-to-beginning-of-word' } # accelerator O
{ localisedLabel: 'core.menu.selection.to-beginning-of-line', command: 'editor:select-to-beginning-of-line' } # accelerator I
{ localisedLabel: 'core.menu.selection.to-first-char-of-line', command: 'editor:select-to-first-character-of-line' } # accelerator C
{ localisedLabel: 'core.menu.selection.to-end-of-word', command: 'editor:select-to-end-of-word' } # accelerator D
{ localisedLabel: 'core.menu.selection.to-end-of-line', command: 'editor:select-to-end-of-line' } # accelerator E
]
}
{
label: 'F&ind'
localisedLabel: 'core.menu.find.self' # accelerator I
submenu: []
}
{
label: '&Packages'
localisedLabel: 'core.menu.packages.self' # accelerator P
submenu: [
{ label: 'Open Package Manager', command: 'settings-view:view-installed-packages' }
{ localisedLabel: 'core.menu.packages.open-package-manager', command: 'settings-view:view-installed-packages' }
{ type: 'separator' }
]
}
{
label: '&Help'
localisedLabel: 'core.menu.help.self' # accelerator H
submenu: [
{ label: 'View &Terms of Use', command: 'application:open-terms-of-use' }
{ label: 'View &License', command: 'application:open-license' }
{ label: "VERSION", enabled: false }
{ localisedLabel: 'core.menu.help.terms-of-use', command: 'application:open-terms-of-use' } # accelerator T
{ localisedLabel: 'core.menu.pulsar.view-license', command: 'application:open-license' } # accelerator L
{ localisedLabel: 'core.menu.pulsar.version', enabled: false }
{ type: 'separator' }
{ label: '&Documentation', command: 'application:open-documentation' }
{ label: 'Frequently Asked Questions', command: 'application:open-faq' }
{ localisedLabel: 'core.menu.help.docs', command: 'application:open-documentation' } # accelerator D
{ localisedLabel: 'core.menu.help.faq', command: 'application:open-faq' }
{ type: 'separator' }
{ label: 'Community Discussions', command: 'application:open-discussions' }
{ label: 'Report Issue', command: 'application:report-issue' }
{ label: 'Search Issues', command: 'application:search-issues' }
{ localisedLabel: 'core.menu.help.community-discussions', command: 'application:open-discussions' }
{ localisedLabel: 'core.menu.help.report-issue', 'application:report-issue' }
{ localisedLabel: 'core.menu.help.search-issues', command: 'application:search-issues' }
{ type: 'separator' }
{ label: 'About Pulsar', command: 'application:about' }
{ localisedLabel: 'core.menu.pulsar.about', command: 'application:about' }
{ type: 'separator' }
]
}

View File

@ -1,6 +1,6 @@
{
"name": "pulsar",
"author": "Pulsar Community <noreply@pulsar-edit.com>",
"author": "Pulsar-Edit <admin@pulsar-edit.dev>",
"productName": "Pulsar",
"version": "1.103.0-dev",
"description": "A Community-led Hyper-Hackable Text Editor",
@ -24,6 +24,7 @@
"dependencies": {
"@atom/source-map-support": "^0.3.4",
"@babel/core": "7.18.6",
"@formatjs/icu-messageformat-parser": "^2.3.0",
"about": "file:packages/about",
"archive-view": "file:packages/archive-view",
"async": "3.2.4",
@ -45,7 +46,7 @@
"base16-tomorrow-dark-theme": "file:packages/base16-tomorrow-dark-theme",
"base16-tomorrow-light-theme": "file:packages/base16-tomorrow-light-theme",
"bookmarks": "file:packages/bookmarks",
"bracket-matcher": "https://github.com/pulsar-edit/bracket-matcher.git#c877977",
"bracket-matcher": "file:packages/bracket-matcher",
"chai": "4.3.4",
"clear-cut": "^2.0.2",
"coffeescript": "1.12.7",
@ -55,14 +56,14 @@
"dedent": "^0.7.0",
"deprecation-cop": "file:packages/deprecation-cop",
"dev-live-reload": "file:packages/dev-live-reload",
"document-register-element": "^1.14.10",
"document-register-element": "https://github.com/pulsar-edit/document-register-element.git#1f5868f",
"encoding-selector": "file:packages/encoding-selector",
"etch": "0.14.1",
"event-kit": "^2.5.3",
"exception-reporting": "file:packages/exception-reporting",
"find-and-replace": "https://github.com/atom-community/find-and-replace/archive/refs/tags/v0.220.1.tar.gz",
"find-parent-dir": "^0.3.0",
"first-mate": "7.4.3",
"second-mate": "https://github.com/pulsar-edit/second-mate.git#14aa7bd",
"focus-trap": "6.3.0",
"fs-admin": "0.19.0",
"fs-plus": "^3.1.1",
@ -77,6 +78,7 @@
"grim": "2.0.3",
"image-view": "file:packages/image-view",
"incompatible-packages": "file:packages/incompatible-packages",
"intl-messageformat": "^10.3.3",
"jasmine-json": "~0.0",
"jasmine-reporters": "1.1.0",
"jasmine-tagged": "^1.1.4",

View File

@ -16,7 +16,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **autocomplete-atom-api** | [`atom/autocomplete-atom-api`][autocomplete-atom-api] | |
| **autocomplete-css** | [`./autocomplete-css`](./autocomplete-css) | |
| **autocomplete-html** | [`./autocomplete-html`](./autocomplete-html) | |
| **autocomplete-plus** | [`./autocomplete-plus`][./autocomplete-plus] | |
| **autocomplete-plus** | [`./autocomplete-plus`](./autocomplete-plus) | |
| **autocomplete-snippets** | [`./autocomplete-snippets`](./autocomplete-snippets) | |
| **autoflow** | [`./autoflow`](./autoflow) | |
| **autosave** | [`pulsar-edit/autosave`][autosave] | [#17834](https://github.com/atom/atom/issues/17834) |
@ -76,7 +76,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **language-yaml** | [`./language-yaml`](./language-yaml) | |
| **line-ending-selector** | [`./line-ending-selector`](./line-ending-selector) | |
| **link** | [`./link`](./link) | |
| **markdown-preview** | [`./markdown-preview`][./markdown-preview] | |
| **markdown-preview** | [`./markdown-preview`](./markdown-preview) | |
| **notifications** | [`atom/notifications`][notifications] | [#18277](https://github.com/atom/atom/issues/18277) |
| **one-dark-syntax** | [`./one-dark-syntax`](./one-dark-syntax) | |
| **one-dark-ui** | [`./one-dark-ui`](./one-dark-ui) | |
@ -90,7 +90,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | |
| **spell-check** | [`atom/spell-check`][spell-check] | |
| **status-bar** | [`./status-bar`](./status-bar) | |
| **styleguide** | [`./styleguide`][./styleguide] | |
| **styleguide** | [`./styleguide`](./styleguide) | |
| **symbols-view** | [`pulsar-edit/symbols-view`][symbols-view] | |
| **tabs** | [`./tabs`](./tabs) | |
| **timecop** | [`pulsar-edit/timecop`][timecop] | [#18272](https://github.com/atom/atom/issues/18272) |
@ -98,7 +98,7 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **update-package-dependencies** | [`./update-package-dependencies`](./update-package-dependencies) | |
| **welcome** | [`./welcome`](./welcome) | |
| **whitespace** | [`./whitespace`](./whitespace) | |
| **wrap-guide** | [`./wrap-guide`][./wrap-guide] | |
| **wrap-guide** | [`./wrap-guide`](./wrap-guide) | |
[autocomplete-atom-api]: https://github.com/pulsar-edit/autocomplete-atom-api
[autosave]: https://github.com/pulsar-edit/autosave

View File

@ -7,4 +7,4 @@ Descriptions are powered by [MDN](https://developer.mozilla.org).
![html-completions](https://cloud.githubusercontent.com/assets/2766036/25668197/ffd24928-2ff3-11e7-85fc-b327ac2287e6.gif)
You can update the prebuilt list of tags and attributes names and values by running the `update.js` file at the root of the repository and then checking-in the changed `completions.json` file.
You can update the prebuilt list of tags and attributes names and values by running `npm run update` at the root of the package and then checking-in the changed `completions.json` file.

File diff suppressed because it is too large Load Diff

View File

@ -1,113 +0,0 @@
const path = require('path')
const fs = require('fs')
const request = require('request')
const mdnHTMLURL = 'https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes'
const mdnJSONAPI = 'https://developer.mozilla.org/en-US/search.json?topic=html&highlight=false'
const AttributesURL = 'https://raw.githubusercontent.com/adobe/brackets/master/src/extensions/default/HTMLCodeHints/HtmlAttributes.json'
const fetch = () => {
const attributesPromise = new Promise((resolve) => {
request({json: true, url: AttributesURL}, (error, response, attributes) => {
if (error) {
console.error(error.message)
resolve(null)
}
if (response.statusCode !== 200) {
console.error(`Request for HtmlAttributes.json failed: ${response.statusCode}`)
resolve(null)
}
resolve(attributes)
})
})
attributesPromise.then((attributes) => {
if (!attributes) return
const MAX = 10
const queue = []
for (let attribute in attributes) {
// MDN is missing docs for aria attributes and on* event handlers
const options = attributes[attribute]
if (options.global && !attribute.startsWith('aria') && !attribute.startsWith('on') && (attribute !== 'role')) {
queue.push(attribute)
}
}
const running = []
const docs = {}
return new Promise((resolve) => {
const checkEnd = () => {
if ((queue.length === 0) && (running.length === 0)) resolve(docs)
}
const removeRunning = (attributeName) => {
const index = running.indexOf(attributeName)
if (index > -1) { running.splice(index, 1) }
}
const runNext = () => {
checkEnd()
if (queue.length !== 0) {
const attributeName = queue.pop()
running.push(attributeName)
run(attributeName)
}
}
var run = (attributeName) => {
const url = `${mdnJSONAPI}&q=${attributeName}`
request({json: true, url}, (error, response, searchResults) => {
if (!error && response.statusCode === 200) {
handleRequest(attributeName, searchResults)
} else {
console.error(`Req failed ${url}; ${response.statusCode}, ${error}`)
}
removeRunning(attributeName)
runNext()
})
}
var handleRequest = (attributeName, searchResults) => {
if (searchResults.documents) {
for (let doc of searchResults.documents) {
if (doc.url === `${mdnHTMLURL}/${attributeName}`) {
docs[attributeName] = filterExcerpt(attributeName, doc.excerpt)
return
}
}
}
console.log(`Could not find documentation for ${attributeName}`)
}
for (let i = 0; i <= MAX; i++) runNext()
})
})
}
var filterExcerpt = (attributeName, excerpt) => {
const beginningPattern = /^the [a-z-]+ global attribute (is )?(\w+)/i
excerpt = excerpt.replace(beginningPattern, (match) => {
const matches = beginningPattern.exec(match)
const firstWord = matches[2]
return firstWord[0].toUpperCase() + firstWord.slice(1)
})
const periodIndex = excerpt.indexOf('.')
if (periodIndex > -1) { excerpt = excerpt.slice(0, periodIndex + 1) }
return excerpt
}
// Save a file if run from the command line
if (require.main === module) {
fetch().then((docs) => {
if (docs) {
fs.writeFileSync(path.join(__dirname, 'global-attribute-docs.json'), `${JSON.stringify(docs, null, ' ')}\n`)
} else {
console.error('No docs')
}
})
}
module.exports = fetch

View File

@ -1,120 +0,0 @@
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const path = require('path')
const fs = require('fs')
const request = require('request')
const mdnHTMLURL = 'https://developer.mozilla.org/en-US/docs/Web/HTML/Element'
const mdnJSONAPI = 'https://developer.mozilla.org/en-US/search.json?topic=html&highlight=false'
const TagsURL = 'https://raw.githubusercontent.com/adobe/brackets/master/src/extensions/default/HTMLCodeHints/HtmlTags.json'
const fetch = () => {
const tagsPromise = new Promise((resolve) => {
request({json: true, url: TagsURL}, (error, response, tags) => {
if (error != null) {
console.error(error.message)
resolve(null)
}
if (response.statusCode !== 200) {
console.error(`Request for HtmlTags.json failed: ${response.statusCode}`)
resolve(null)
}
resolve(tags)
})
})
return tagsPromise.then((tags) => {
if (!tags) return
const MAX = 10
const queue = Object.keys(tags)
const running = []
const docs = {}
return new Promise((resolve) => {
const checkEnd = () => {
if ((queue.length === 0) && (running.length === 0)) resolve(docs)
}
const removeRunning = (tagName) => {
const index = running.indexOf(tagName)
if (index > -1) { return running.splice(index, 1) }
}
const runNext = () => {
checkEnd()
if (queue.length !== 0) {
const tagName = queue.pop()
running.push(tagName)
run(tagName)
}
}
var run = (tagName) => {
const url = `${mdnJSONAPI}&q=${tagName}`
request({json: true, url}, (error, response, searchResults) => {
if ((error == null) && (response.statusCode === 200)) {
handleRequest(tagName, searchResults)
} else {
console.error(`Req failed ${url}; ${response.statusCode}, ${error}`)
}
removeRunning(tagName)
runNext()
})
}
var handleRequest = (tagName, searchResults) => {
if (searchResults.documents != null) {
for (let doc of searchResults.documents) {
// MDN groups h1 through h6 under a single "Heading Elements" page
if ((doc.url === `${mdnHTMLURL}/${tagName}`) || (/^h\d$/.test(tagName) && (doc.url === `${mdnHTMLURL}/Heading_Elements`))) {
if (doc.tags.includes('Obsolete')) {
docs[tagName] = `The ${tagName} element is obsolete. Avoid using it and update existing code if possible.`
} else if (doc.tags.includes('Deprecated')) {
docs[tagName] = `The ${tagName} element is deprecated. Avoid using it and update existing code if possible.`
} else {
docs[tagName] = filterExcerpt(tagName, doc.excerpt)
}
return
}
}
}
console.log(`Could not find documentation for ${tagName}`)
}
for (let i = 0; i <= MAX; i++) { runNext() }
})
})
}
var filterExcerpt = (tagName, excerpt) => {
const beginningPattern = /^the html [a-z-]+ element (\([^)]+\) )?(is )?(\w+)/i
excerpt = excerpt.replace(beginningPattern, (match) => {
const matches = beginningPattern.exec(match)
const firstWord = matches[3]
return firstWord[0].toUpperCase() + firstWord.slice(1)
})
const periodIndex = excerpt.indexOf('.')
if (periodIndex > -1) { excerpt = excerpt.slice(0, periodIndex + 1) }
return excerpt
}
// Save a file if run from the command line
if (require.main === module) {
fetch().then((docs) => {
if (docs != null) {
fs.writeFileSync(path.join(__dirname, 'tag-docs.json'), `${JSON.stringify(docs, null, ' ')}\n`)
} else {
console.error('No docs')
}
})
}
module.exports = fetch

View File

@ -109,7 +109,7 @@ function getTagAttributes (tag) {
}
function getLocalAttributeDocsURL (attribute, tag) {
return `${getTagDocsURL(tag)}#attr-${attribute}`
return `${getTagDocsURL(tag)}#attributes`
}
function getGlobalAttributeDocsURL (attribute) {

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,10 @@
"engines": {
"atom": ">=0.174.0 <2.0.0"
},
"scripts": {
"update": "node ./update/update.js",
"update:test": "jest ./update/update.test.js"
},
"providedServices": {
"autocomplete.provider": {
"versions": {
@ -16,6 +20,12 @@
}
},
"devDependencies": {
"request": "^2.53.0"
"@webref/elements": "^2.1.0",
"chrome-devtools-frontend": "^1.0.1070764",
"content": "github:mdn/content",
"esm": "^3.2.25",
"jest": "^29.4.3",
"joi": "^17.8.3",
"ts-import": "^2.0.40"
}
}

View File

@ -12,6 +12,26 @@ describe('HTML autocompletions', () => {
return provider.getSuggestions({editor, bufferPosition, scopeDescriptor, prefix})
}
function isValueInCompletions(value, array, attribute) {
attribute ??= 'text'
let result = [];
for (const i of array) {
result.push(i[attribute]);
}
return result.includes(value);
}
function getValueInCompletionsIndex(value, array, attribute) {
attribute ??= 'text';
for (let i = 0; i < array.length; i++) {
if (array[i][attribute] === value) {
return i;
}
}
// We never did find the value in our array
return -1;
}
beforeEach(() => {
waitsForPromise(() => atom.packages.activatePackage('autocomplete-html'))
waitsForPromise(() => atom.packages.activatePackage('language-html'))
@ -59,9 +79,9 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 1])
const completions = getCompletions()
expect(completions.length).toBe(113)
expect(completions[0].description).toContain('Creates a hyperlink to other web pages')
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/a')).toBe(true)
expect(completions.length).toBeGreaterThan(113) // Fun Fact last check this was 232
expect(completions[0].description.length).toBeGreaterThan(0)
expect(completions[0].descriptionMoreURL.length).toBeGreaterThan(0)
for (let completion of completions) {
expect(completion.text.length).toBeGreaterThan(0)
@ -75,36 +95,35 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 2])
let completions = getCompletions()
expect(completions.length).toBe(9)
expect(completions.length).toBeGreaterThan(9) // Fun fact last check was 14
expect(completions[0].text).toBe('datalist')
expect(isValueInCompletions('datalist', completions)).toBe(true)
expect(completions[0].type).toBe('tag')
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/datalist')).toBe(true)
expect(completions[1].text).toBe('dd')
expect(completions[2].text).toBe('del')
expect(completions[3].text).toBe('details')
expect(completions[4].text).toBe('dfn')
expect(completions[5].text).toBe('dialog')
expect(completions[6].text).toBe('div')
expect(completions[7].text).toBe('dl')
expect(completions[8].text).toBe('dt')
expect(isValueInCompletions('dd', completions)).toBe(true)
expect(isValueInCompletions('del', completions)).toBe(true)
expect(isValueInCompletions('details', completions)).toBe(true)
expect(isValueInCompletions('dfn', completions)).toBe(true)
expect(isValueInCompletions('dialog', completions)).toBe(true)
expect(isValueInCompletions('div', completions)).toBe(true)
expect(isValueInCompletions('dl', completions)).toBe(true)
expect(isValueInCompletions('dt', completions)).toBe(true)
editor.setText('<D')
editor.setCursorBufferPosition([0, 2])
completions = getCompletions()
expect(completions.length).toBe(9)
expect(completions.length).toBeGreaterThan(9) // Fun fact last check was 14
expect(completions[0].text).toBe('datalist')
expect(isValueInCompletions('datalist', completions)).toBe(true)
expect(completions[0].type).toBe('tag')
expect(completions[1].text).toBe('dd')
expect(completions[2].text).toBe('del')
expect(completions[3].text).toBe('details')
expect(completions[4].text).toBe('dfn')
expect(completions[5].text).toBe('dialog')
expect(completions[6].text).toBe('div')
expect(completions[7].text).toBe('dl')
expect(completions[8].text).toBe('dt')
expect(isValueInCompletions('dd', completions)).toBe(true)
expect(isValueInCompletions('del', completions)).toBe(true)
expect(isValueInCompletions('details', completions)).toBe(true)
expect(isValueInCompletions('dfn', completions)).toBe(true)
expect(isValueInCompletions('dialog', completions)).toBe(true)
expect(isValueInCompletions('div', completions)).toBe(true)
expect(isValueInCompletions('dl', completions)).toBe(true)
expect(isValueInCompletions('dt', completions)).toBe(true)
})
it("does not autocomplete tag names if there's a space after the <", () => {
@ -122,15 +141,16 @@ describe('HTML autocompletions', () => {
})
it('does not provide a descriptionMoreURL if the tag does not have a unique description', () => {
// ilayer does not have an associated MDN page as of April 27, 2017
// isindex does not have an associated MDN page as of March 25, 2023
editor.setText('<i')
editor.setCursorBufferPosition([0, 2])
const completions = getCompletions()
const loc = getValueInCompletionsIndex('isindex', completions)
expect(completions[2].text).toBe('ilayer')
expect(completions[2].description).toBe('HTML <ilayer> tag')
expect(completions[2].descriptionMoreURL).toBeNull()
expect(isValueInCompletions('isindex', completions)).toBe(true)
expect(completions[loc].description).toBe("HTML <isindex> tag")
expect(completions[loc].descriptionMoreURL).toBeNull()
})
it('autocompletes attribute names without a prefix', () => {
@ -138,9 +158,9 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 5])
let completions = getCompletions()
expect(completions.length).toBe(86)
expect(completions[0].description).toContain('Provides a hint for generating a keyboard shortcut')
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Global_attributes/accesskey')).toBe(true)
expect(completions.length).toBeGreaterThan(86) // Fun fact last check this was 264
expect(completions[0].description.length).toBeGreaterThan(0)
expect(completions[0].descriptionMoreURL.length).toBeGreaterThan(0)
for (var completion of completions) {
expect(completion.snippet.length).toBeGreaterThan(0)
@ -153,9 +173,9 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 9])
completions = getCompletions()
expect(completions.length).toBe(98)
expect(completions.length).toBeGreaterThan(98) // Last check 274
expect(completions[0].rightLabel).toBe('<marquee>')
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/marquee#attr-align')).toBe(true)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/marquee#attributes')).toBe(true)
for (completion of completions) {
expect(completion.snippet.length).toBeGreaterThan(0)
@ -186,57 +206,59 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 6])
let completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions.length).toBeGreaterThan(3) // Last check 9
expect(completions[0].snippet).toBe('class="$1"$0')
expect(completions[0].displayText).toBe('class')
expect(completions[0].type).toBe('attribute')
expect(completions[1].displayText).toBe('contenteditable')
expect(completions[2].displayText).toBe('contextmenu')
let loc = getValueInCompletionsIndex('class', completions, 'displayText')
expect(completions[loc].snippet).toBe('class="$1"$0')
expect(completions[loc].displayText).toBe('class')
expect(completions[loc].type).toBe('attribute')
expect(isValueInCompletions('contenteditable', completions, 'displayText'))
expect(isValueInCompletions('contextmenu', completions, 'displayText'))
editor.setText('<div C')
editor.setCursorBufferPosition([0, 6])
completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions.length).toBeGreaterThan(3) // Last check 9
expect(completions[0].displayText).toBe('class')
expect(completions[1].displayText).toBe('contenteditable')
expect(completions[2].displayText).toBe('contextmenu')
expect(isValueInCompletions('class', completions, 'displayText'))
expect(isValueInCompletions('contenteditable', completions, 'displayText'))
expect(isValueInCompletions('contextmenu', completions, 'displayText'))
editor.setText('<div c>')
editor.setCursorBufferPosition([0, 6])
completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions.length).toBeGreaterThan(3)
expect(completions[0].displayText).toBe('class')
expect(completions[1].displayText).toBe('contenteditable')
expect(completions[2].displayText).toBe('contextmenu')
expect(isValueInCompletions('class', completions, 'displayText'))
expect(isValueInCompletions('contenteditable', completions, 'displayText'))
expect(isValueInCompletions('contextmenu', completions, 'displayText'))
editor.setText('<div c></div>')
editor.setCursorBufferPosition([0, 6])
completions = getCompletions()
expect(completions.length).toBe(3)
expect(completions.length).toBeGreaterThan(3)
expect(completions[0].displayText).toBe('class')
expect(completions[1].displayText).toBe('contenteditable')
expect(completions[2].displayText).toBe('contextmenu')
expect(isValueInCompletions('class', completions, 'displayText'))
expect(isValueInCompletions('contenteditable', completions, 'displayText'))
expect(isValueInCompletions('contextmenu', completions, 'displayText'))
editor.setText('<marquee di')
editor.setCursorBufferPosition([0, 12])
completions = getCompletions()
expect(completions[0].displayText).toBe('direction')
expect(completions[1].displayText).toBe('dir')
expect(isValueInCompletions('direction', completions, 'displayText'))
expect(isValueInCompletions('dir', completions, 'displayText'))
editor.setText('<marquee dI')
editor.setCursorBufferPosition([0, 12])
completions = getCompletions()
expect(completions[0].displayText).toBe('direction')
expect(completions[1].displayText).toBe('dir')
expect(isValueInCompletions('direction', completions, 'displayText'))
expect(isValueInCompletions('dir', completions, 'displayText'))
})
it('autocompletes attribute names without a prefix surrounded by whitespace', () => {
@ -245,7 +267,7 @@ describe('HTML autocompletions', () => {
const completions = getCompletions()
for (let completion of completions) { expect(completion.type).toBe('attribute') }
expect(completions[0].displayText).toBe('autofocus')
expect(isValueInCompletions('autofocus', completions, 'displayText'))
})
it('autocompletes attribute names with a prefix surrounded by whitespace', () => {
@ -254,7 +276,7 @@ describe('HTML autocompletions', () => {
const completions = getCompletions()
for (let completion of completions) { expect(completion.type).toBe('attribute') }
expect(completions[0].displayText).toBe('onabort')
expect(isValueInCompletions('onabort', completions, 'displayText'))
})
it("respects the 'flag' type when autocompleting attribute names", () => {
@ -262,7 +284,7 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 8])
const completions = getCompletions()
expect(completions[0].snippet).toBe('autofocus')
expect(isValueInCompletions('autofocus', completions, 'snippet'))
})
it('does not autocomplete attribute names in between an attribute name and value', () => {
@ -308,7 +330,7 @@ describe('HTML autocompletions', () => {
editor.setCursorBufferPosition([0, 6])
const completions = getCompletions()
expect(completions[0].displayText).toBe('onafterprint')
expect(isValueInCompletions('onafterprint', completions, 'displayText'))
})
it('does not provide a descriptionMoreURL if the attribute does not have a unique description', () => {
@ -317,9 +339,11 @@ describe('HTML autocompletions', () => {
const completions = getCompletions()
expect(completions[0].displayText).toBe('onabort')
expect(completions[0].description).toBe('Global onabort attribute')
expect(completions[0].descriptionMoreURL).toBeNull()
const loc = getValueInCompletionsIndex('onabort', completions, 'displayText')
expect(completions[loc].displayText).toBe('onabort')
expect(completions[loc].description).toBe('Global onabort attribute')
expect(completions[loc].descriptionMoreURL).toBeNull()
})
it('autocompletes attribute values without a prefix', () => {
@ -332,7 +356,7 @@ describe('HTML autocompletions', () => {
expect(completions[0].text).toBe('scroll')
expect(completions[0].type).toBe('value')
expect(completions[0].description.length).toBeGreaterThan(0)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/marquee#attr-behavior')).toBe(true)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/marquee#attributes')).toBe(true)
expect(completions[1].text).toBe('slide')
expect(completions[2].text).toBe('alternate')
@ -419,7 +443,7 @@ describe('HTML autocompletions', () => {
expect(completions[0].text).toBe('button')
expect(completions[0].type).toBe('value')
expect(completions[0].description.length).toBeGreaterThan(0)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/button#attr-type')).toBe(true)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/button#attributes')).toBe(true)
expect(completions[1].text).toBe('reset')
expect(completions[2].text).toBe('submit')
@ -433,7 +457,7 @@ describe('HTML autocompletions', () => {
expect(completions[0].text).toBe('alternate')
expect(completions[0].type).toBe('value')
expect(completions[0].description.length).toBeGreaterThan(0)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/link#attr-rel')).toBe(true)
expect(completions[0].descriptionMoreURL.endsWith('/HTML/Element/link#attributes')).toBe(true)
})
it("provides 'true' and 'false' suggestions when autocompleting boolean attributes", () => {

View File

@ -1,73 +0,0 @@
const path = require('path')
const fs = require('fs')
const request = require('request')
const fetchTagDescriptions = require('./fetch-tag-docs')
const fetchGlobalAttributeDescriptions = require('./fetch-global-attribute-docs')
const TagsURL = 'https://raw.githubusercontent.com/adobe/brackets/master/src/extensions/default/HTMLCodeHints/HtmlTags.json'
const AttributesURL = 'https://raw.githubusercontent.com/adobe/brackets/master/src/extensions/default/HTMLCodeHints/HtmlAttributes.json'
const tagsPromise = new Promise((resolve) => {
request({json: true, url: TagsURL}, (error, response, tags) => {
if (error != null) {
console.error(error.message)
resolve(null)
}
if (response.statusCode !== 200) {
console.error(`Request for HtmlTags.json failed: ${response.statusCode}`)
resolve(null)
}
for (let tag in tags) {
const options = tags[tag]
if ((options.attributes != null ? options.attributes.length : undefined) === 0) { delete options.attributes }
}
resolve(tags)
})
})
const tagDescriptionsPromise = fetchTagDescriptions()
const attributesPromise = new Promise((resolve) => {
return request({json: true, url: AttributesURL}, (error, response, attributes) => {
if (error != null) {
console.error(error.message)
resolve(null)
}
if (response.statusCode !== 200) {
console.error(`Request for HtmlAttributes.json failed: ${response.statusCode}`)
resolve(null)
}
for (let attribute in attributes) {
const options = attributes[attribute]
if ((options.attribOption != null ? options.attribOption.length : undefined) === 0) { delete options.attribOption }
}
resolve(attributes)
})
})
const globalAttributeDescriptionsPromise = fetchGlobalAttributeDescriptions()
Promise.all([tagsPromise, tagDescriptionsPromise, attributesPromise, globalAttributeDescriptionsPromise]).then((values) => {
const tags = values[0]
const tagDescriptions = values[1]
const attributes = values[2]
const attributeDescriptions = values[3]
for (let tag in tags) {
tags[tag].description = tagDescriptions[tag]
}
for (let attribute in attributes) {
const options = attributes[attribute]
if (options.global) { attributes[attribute].description = attributeDescriptions[attribute] }
}
const completions = {tags, attributes}
fs.writeFileSync(path.join(__dirname, 'completions.json'), `${JSON.stringify(completions, null, ' ')}\n`)
})

View File

@ -0,0 +1,44 @@
require = require("esm")(module)
module.exports = require("./chromium-elements-shim.mjs")
/**
Used to aid in the automatic updating of our `completions.json`
So while this, coupled with `chromium-elements-shim.mjs` is a rather ugly solution
heres why this exists:
Essentially, we need the values declared within `DOMPinnedProperties.ts`.
Those values will be essential in creating our final `completions.json`
But the problems with using this file in the regular JavaScript the `update.js`
file is created in are three fold:
1) This file, obviously, is TypeScript
2) This file is an ECMAScript Module
3) While this is a TypeScript file there's no `./dist` folder or compiled version
readily available, instead relying on custom built tooling to transpile.
So while this solution is ugly, essentially we make it so we can simply
`require()` this file within our JavaScript.
We literally use `const chromiumElementsShim = require("./chromium-elements-shim.js");`
Then just have to call `chromiumElementsShim.bootstrap()` to transpile the file.
We require this file, which then loads `esm` which allows us to import a ESM
module in a CommonJS module by making a small shim. Exporting it as CommonJS.
But the file is still TypeScript. So we use `ts-import` in `chromium-elements-shim.mjs`
to read the file from disk, and transpile it, finally exporting the async function
that actually does the transpiling.
So from here we import that module, and shim it.
A word of caution, we are specifically using the older version of `ts-import`
because this was the version that was used before the developer upgraded the Node
version supported. Which beyond this they use `node:fs` to import the FS Module,
which isn't supported on the version of Pulsar we are currently using.
So once we upgrade our version of NodeJS we will also be able to upgrade `ts-import`
to it's latest version. That will also mean we have to rewrite `chromium-elements-shim.mjs`
as the API has completely changed since the specific version we are using.
*/

View File

@ -0,0 +1,11 @@
import * as tsImport from 'ts-import';
export const bootstrap = async () => {
const filePath = "./node_modules/chrome-devtools-frontend/front_end/models/javascript_metadata/DOMPinnedProperties.ts";
const compiled = await tsImport.default.tsImport.compile(filePath);
return compiled;
};
// Used to aid in the automated process of updating `completions.json`
// More information in `chromium-elements-shim.js`

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,267 @@
/**
This file will manage the updating of `autocomplete-html` `completions.json`
We will partially utilize `@webref/elements` `.listAll()` function that returns
a full list of HTML Elements along with a defined `interface`.
To use this `interface` in any meaningful way, we will utilize the dataset
of Attributes that apply to each `interface` from Chromiums DevTools resource
`https://github.com/ChromeDevTools/devtools-frontend`.
Finally from here we will utilize `https://github.com/mdn/content` to parse
the Markdown docs of MDN's website to retreive descriptions for each element.
Now for a summary of our `completions.json` file we aim to generate.
There are two top level elements, `tags` and `attributes`, both objects.
Within `tags` we expect the following:
"tags": {
"a": {
"attributes": [ "href", "hreflang", "media", "rel", "target", "type" ],
"description": "....."
}
};
When an entry contains no `attributes` there is no empty array, the element
simply doesn't exist.
The `attributes` object contains keys of different elements that themselves
are objects that can contain several valid keys.
- global: Seems to be used exclusively for Global Attributes. Is a boolean
which when false, the key does not appear.
- type: A ?type? for the attribute. It's meaning is not immediately known.
Nor a way to reliabley programatically collect it. Some discovered values:
* cssStyle: Exclusively used for `class` attribute
* boolean: Attributes that only accept `true` or `false`
* flag: For attributes that don't require or accept values. eg autoplay
* cssId: Exclusively used for the `id` attribute
* color: Exclusively used for the `bgcolor` attribute
* style: Exclusively used for the `style` attribute
- description: A text description of the attribute
- attribOption: A string array of valid values that can exist within the attribute.
Such as the case with `rel` where only so many valid options exist.
Although with our data sources mentioned above, we are able to collect nearly
all the data needed. Except the `type` that is defined within our
`completions.json` as well as the `attribOption` within our completions.
Studying these closer reveals that all attributes listing with our `completions.json`
do not appear elsewhere, and are nearly all global attributes.
In this case since there is no sane way to collect this data, we will leave this
list as a manually maintained section of our `completions.json`.
This does mean that `curated-attributes.json` is a static document that
will require manual updating in the future. Or most ideally, will find a way
to automatically generate the needed data.
*/
const chromiumElementsShim = require("./chromium-elements-shim.js");
const curatedAttributes = require("./curated-attributes.json");
const validate = require("./validate.js");
const elements = require("@webref/elements");
const fs = require("fs");
let GLOBAL_ATTRIBUTES = [];
async function update() {
const chromiumElements = await chromiumElementsShim.bootstrap();
const htmlElementsRaw = await elements.listAll();
// Lets then validate our current data
if (!validate.htmlElementsRaw(htmlElementsRaw)) {
console.log(validate.htmlElementsRaw(htmlElementsRaw));
process.exit(1);
}
// Then validate the devtools data
if (!validate.devToolsDom(chromiumElements.DOMPinnedProperties)) {
console.log(validate.devToolsDom(chromiumElements.DOMPinnedProperties));
process.exit(1);
}
const fullArrayHtmlElements = buildHtmlElementsArray(htmlElementsRaw);
const tagsWithAttrs = matchElementInterface(fullArrayHtmlElements, chromiumElements.DOMPinnedProperties);
// tagsWithAttrs gives us an already built object for tags. Including their description.
// Like mentioned in the docs above, instead of manually curate and organize
// several aspects of the attributes portion of the file
// we will simply just insert the manually curated file itself, allowing for any
// change to occur to it later on as needed, as no good data source specifies
// what is needed to create it.
const completion = {
tags: tagsWithAttrs,
attributes: curatedAttributes
};
// Now to write our file
fs.writeFileSync("./completions.json", JSON.stringify(completion, null, 2));
// Now to check if our attributes contains all of the global values that we saw.
const missingGlobals = confirmGlobals(curatedAttributes, GLOBAL_ATTRIBUTES);
if (missingGlobals.length > 0) {
console.log(missingGlobals);
console.log("Above are the globals found during updating that do not exist in Curated Attributes.");
console.log(`Total Missing Global Attributes: ${missingGlobals.length}`);
}
console.log("Updated all `autocomplete-html` completions.");
}
function confirmGlobals(have, want) {
// have is an object, meanwhile want's is an array
let result = [];
for (const w of want) {
if (typeof have[w] !== "object") {
result.push(w);
}
}
return result;
}
function buildHtmlElementsArray(elements) {
let elementArray = [];
for (const spec in elements) {
if (Array.isArray(elements[spec].elements)) {
for (const ele of elements[spec].elements) {
elementArray.push(ele);
}
}
}
return elementArray;
}
function matchElementInterface(elements, domProperties) {
let outElements = {};
for (const ele of elements) {
let tags = resolveElementInterfaceAttrs(ele, domProperties);
outElements[ele.name] = {};
if (tags.length > 0) {
outElements[ele.name].attributes = tags;
}
let desc = getElementDescription(ele.name);
outElements[ele.name].description = desc;
}
return outElements;
}
function resolveElementInterfaceAttrs(element, domProperties) {
let attrs = [];
let interfaceArray = [];
if (typeof element.interface === "string") {
interfaceArray.push(element.interface);
}
// Now to loop through every interface and resolve the tags within.
while (interfaceArray.length > 0) {
let inter = interfaceArray[0];
if (domProperties[inter]) {
// First add all immediate props
for (const prop in domProperties[inter].props) {
if (!domProperties[inter].props[prop].global && inter !== "GlobalEventHandlers") {
// Seems that according to the previous completions.json
// We don't want to include any global values in our individual attributes
attrs.push(prop);
} else {
// We don't want global attributes on our actual completions. But do want them tracked
if (!GLOBAL_ATTRIBUTES.includes(prop)) {
GLOBAL_ATTRIBUTES.push(prop);
}
}
}
// Now resolve any additional interfaces, by adding them to our existing array
if (typeof domProperties[inter].inheritance === "string") {
interfaceArray.push(domProperties[inter].inheritance);
}
if (Array.isArray(domProperties[inter].includes)) {
interfaceArray = interfaceArray.concat(domProperties[inter].includes);
}
}
// Now we have done everything needed for this one interface to be resolved,
// so we can just remove the first element of the array and let the while
// loop continue
interfaceArray.shift();
}
// Return our final list of attributes
return attrs;
}
function getElementDescription(element) {
// We will gather a description by checking if there's a document written
// on MDN for our Element and then extract a summary from there.
// Some elements are similar enough they exist in a single folder of a different
// name. So we will manually intervein in those cases.
if (element.match(/^h[1-6]$/)) {
element = 'heading_elements';
}
let file;
// First lets find the file, but when not initially found, we will continue
// to search valid locations. Since MDN has content of valid tags seperated
// by essentially the spec they exist in.
const filePath = [ "html", "svg", "mathml" ].map(path =>
`./node_modules/content/files/en-us/web/${path}/element/${element}/index.md`
).find(f => fs.existsSync(f));
if (filePath) {
file = fs.readFileSync(filePath, { encoding: "utf8" });
}
if (typeof file === "string") {
// Now lets parse the file, as long as it was assigned at some point in
// the above checks.
// This logic is largely borrowed from `autocomplete-css` update.js Which does the same thing.
let breaks = file.split("---");
// The first two breaks should be the yaml metadata block
let data = breaks[2].replace(/\{\{\S+\}\}\{\{\S+\}\}/gm, "")
.replace(/\{\{HTMLSidebar\}\}/gm, "") // Used when within `web/html`
.replace(/\{\{SVGRef\}\}/gm, "") // used when within `web/svg`
.replace(/\{\{MathMLRef\}\}/gm, ""); // used when within `web/mathml`
let summaryRaw = data.split("\n");
// In case the first few lines is an empty line break
for (let i = 0; i < summaryRaw.length; i++) {
if (summaryRaw[i].length > 1) {
return sanitizeDescription(summaryRaw[i]);
}
}
} else {
// No proper file was ever found. Return an empty description
return "";
}
}
function sanitizeDescription(input) {
return input
.replace(/\{\{\S+\("(\S+)"\)\}\}/g, '$1')
// ^ Parses special MDN Markdown Links.
// eg. {{htmlattrxref("title")}} => title
// Where we still want to keep the text within
.replace(/[\*\`\{\}\"]/g, "") // Removes special Markdown based characters
.replace(/\[([A-Za-z0-9-_* ]+)\]\(\S+\)/g, '$1');
// ^ Parses Markdown links, extracting only the linked text
// eg. [HTML](/en-US/docs/Web/HTML) => HTML
}
update();
module.exports = {
sanitizeDescription,
};

View File

@ -0,0 +1,34 @@
/**
This file aims to run some short simple tests against `update.js`. Focusing
mainly on the Regex used within `sanitizeDescription()`
*/
const update = require("./update.js");
describe("Parses Descriptions Properly from Markdown", () => {
test("Extracts Markdown Links text", () => {
const text = "Here is my very important [link](https://github.com/pulsar-edit/pulsar)!";
const out = update.sanitizeDescription(text);
expect(out).toBe("Here is my very important link!");
});
test("Removes some Markdown characters", () => {
const text = "Some *Bolded* text, some **italic** text";
const out = update.sanitizeDescription(text);
expect(out).toBe("Some Bolded text, some italic text");
});
test("Extracts Text from MDNs special Links", () => {
const text = 'What about {{htmlattrxref("this?")}}';
const out = update.sanitizeDescription(text);
expect(out).toBe("What about this?");
});
});

View File

@ -0,0 +1,77 @@
const Joi = require("joi");
function htmlElementsRaw(obj) {
// Function to validate structure of the `@webref/elements` `.listAll()`
const innerSchema = Joi.object({
"spec": Joi.object({ // Details on the spec
"title": Joi.string().required(), // The title of the spec
"url": Joi.string().required(), // URL to the definition of the spec
}).required(),
"elements": Joi.array().items( // the elements object is an array of objects
Joi.object({
"name": Joi.string().required(), // The name of the element
"interface": Joi.string().optional(), // The optional interface of the element
// ^ The interface name will match one from DevTools data.
"obsolete": Joi.boolean().truthy().optional()
// ^ The optional and uncommon boolean to indicate an element is absolete
}).required()
).required()
});
let valid = true;
let error = [];
for (const ele in obj) {
// The first key is the name of the spec the element is apart of.
let validation = innerSchema.validate(obj[ele]);
if (validation.error) {
error.push(validation.error);
valid = false;
}
}
if (!valid) {
console.log(error);
return false;
} else {
return true;
}
}
function devToolsDom(obj) {
// Validates the structure of the data returned from Chromiums DevTools
// DOMPinnedProperties
// Read More: https://github.com/ChromeDevTools/devtools-frontend/blob/main/front_end/models/javascript_metadata/DOMPinnedProperties.ts
const innerSchema = Joi.object({
"inheritance": Joi.string().optional(), // An interface to inherit
"includes": Joi.array().items(Joi.string()), // An array of interfaces included with our current interface
"props": Joi.object(), // An object list of properties. Where the key is the property name
}).unknown();
let valid = true;
let error = [];
for (const inter in obj) {
// The first key is the name of an interface
let validation = innerSchema.validate(obj[inter]);
if (validation.error) {
error.push(validation.error);
valid = false;
}
}
if (!valid) {
console.log(error);
return false;
} else {
return true;
}
}
module.exports = {
htmlElementsRaw,
devToolsDom,
};

View File

@ -0,0 +1 @@
[See how you can contribute](https://github.com/pulsar-edit/.github/blob/main/CONTRIBUTING.md)

View File

@ -0,0 +1,20 @@
Copyright (c) 2014 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.

View File

@ -0,0 +1,52 @@
# Bracket Matcher package
Highlights and jumps between `[]`, `()`, and `{}`. Also highlights matching XML
and HTML tags.
Autocompletes `[]`, `()`, `{}`, `""`, `''`, `“”`, ``, `«»`, ``, and
backticks by default.
Use <kbd>ctrl-m</kbd> to jump to the bracket matching the one adjacent to the cursor.
It jumps to the nearest enclosing bracket when there's no adjacent bracket,
Use <kbd>ctrl-cmd-m</kbd> to select all the text inside the current brackets.
Use <kbd>alt-cmd-.</kbd> to close the current XML/HTML tag.
---
### Configuration
Matching brackets and quotes are sensibly inserted for you. If you dislike this
functionality, you can disable it from the Bracket Matcher section of the
Settings View.
#### Custom Pairs
You can customize matching pairs in Bracket Matcher at any time. You can do so either globally via the Settings View or at the scope level via your `config.cson`. Changes take effect immediately.
* **Autocomplete Characters** - Comma-separated pairs that the editor will treat as brackets / quotes. Entries in this field override the package defaults.
* For example: `<>, (), []`
* **Pairs With Extra Newline** - Comma-separated pairs that enhance the editor's auto indent feature. When used, a newline is automatically added between the pair when enter is pressed between them. Note: This feature is meant to be used in combination with brackets defined for indentation by the active language package (`increaseIndentPattern` / `decreaseIndentPattern`).
Example:
```
fn main() {
| <---- Cursor positioned at one indent level higher
}
```
#### Scoped settings
In addition to the global settings, you are also able to add scope-specific modifications to Pulsar in your `config.cson`. This is especially useful for editor rule changes specific to each language. Scope-specific settings override package defaults _and_ global settings.
Example:
```cson
".rust.source":
"bracket-matcher":
autocompleteCharacters: [
"()"
"[]"
"{}"
"<>"
"\"\""
"``"
]
```

View File

@ -0,0 +1,18 @@
'atom-text-editor':
'ctrl-m': 'bracket-matcher:go-to-matching-bracket'
'ctrl-]': 'bracket-matcher:remove-brackets-from-selection'
'.platform-darwin atom-text-editor':
'ctrl-cmd-m': 'bracket-matcher:select-inside-brackets'
'alt-cmd-.': 'bracket-matcher:close-tag'
'ctrl-backspace': 'bracket-matcher:remove-matching-brackets'
'.platform-linux atom-text-editor':
'ctrl-alt-,': 'bracket-matcher:select-inside-brackets'
'ctrl-alt-.': 'bracket-matcher:close-tag'
'ctrl-alt-backspace': 'bracket-matcher:remove-matching-brackets'
'.platform-win32 atom-text-editor':
'ctrl-alt-,': 'bracket-matcher:select-inside-brackets'
'ctrl-alt-.': 'bracket-matcher:close-tag'
'ctrl-alt-backspace': 'bracket-matcher:remove-matching-brackets'

View File

@ -0,0 +1,585 @@
const {CompositeDisposable} = require('atom')
const _ = require('underscore-plus')
const {Range, Point} = require('atom')
const TagFinder = require('./tag-finder')
const MAX_ROWS_TO_SCAN = 10000
const ONE_CHAR_FORWARD_TRAVERSAL = Object.freeze(Point(0, 1))
const ONE_CHAR_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -1))
const TWO_CHARS_BACKWARD_TRAVERSAL = Object.freeze(Point(0, -2))
const MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL = Object.freeze(Point(MAX_ROWS_TO_SCAN, 0))
const MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL = Object.freeze(Point(-MAX_ROWS_TO_SCAN, 0))
module.exports =
class BracketMatcherView {
constructor (editor, editorElement, matchManager) {
this.destroy = this.destroy.bind(this)
this.updateMatch = this.updateMatch.bind(this)
this.editor = editor
this.matchManager = matchManager
this.gutter = this.editor.gutterWithName('line-number')
this.subscriptions = new CompositeDisposable()
this.tagFinder = new TagFinder(this.editor)
this.pairHighlighted = false
this.tagHighlighted = false
// ranges for possible selection
this.bracket1Range = null
this.bracket2Range = null
this.subscriptions.add(
this.editor.onDidTokenize(this.updateMatch),
this.editor.getBuffer().onDidChangeText(this.updateMatch),
this.editor.onDidChangeGrammar(this.updateMatch),
this.editor.onDidChangeSelectionRange(this.updateMatch),
this.editor.onDidAddCursor(this.updateMatch),
this.editor.onDidRemoveCursor(this.updateMatch),
atom.commands.add(editorElement, 'bracket-matcher:go-to-matching-bracket', () =>
this.goToMatchingBracket()
),
atom.commands.add(editorElement, 'bracket-matcher:go-to-enclosing-bracket', () =>
this.gotoPrecedingStartBracket()
),
atom.commands.add(editorElement, 'bracket-matcher:select-inside-brackets', () =>
this.selectInsideBrackets()
),
atom.commands.add(editorElement, 'bracket-matcher:close-tag', () =>
this.closeTag()
),
atom.commands.add(editorElement, 'bracket-matcher:remove-matching-brackets', () =>
this.removeMatchingBrackets()
),
atom.commands.add(editorElement, 'bracket-matcher:select-matching-brackets', () =>
this.selectMatchingBrackets()
),
this.editor.onDidDestroy(this.destroy)
)
this.updateMatch()
}
destroy () {
this.subscriptions.dispose()
}
updateMatch () {
if (this.pairHighlighted) {
this.editor.destroyMarker(this.startMarker.id)
this.editor.destroyMarker(this.endMarker.id)
}
this.pairHighlighted = false
this.tagHighlighted = false
if (!this.editor.getLastSelection().isEmpty()) return
const {position, matchPosition} = this.findCurrentPair()
let startRange = null
let endRange = null
let highlightTag = false
let highlightPair = false
if (position && matchPosition) {
this.bracket1Range = (startRange = Range(position, position.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
this.bracket2Range = (endRange = Range(matchPosition, matchPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)))
highlightPair = true
} else {
this.bracket1Range = null
this.bracket2Range = null
if (this.hasSyntaxTree()) {
({startRange, endRange} = this.findMatchingTagNameRangesWithSyntaxTree())
} else {
({startRange, endRange} = this.tagFinder.findMatchingTags())
if (this.isCursorOnCommentOrString()) return
}
if (startRange) {
highlightTag = true
highlightPair = true
}
}
if (!highlightTag && !highlightPair) return
this.startMarker = this.createMarker(startRange)
this.endMarker = this.createMarker(endRange)
this.pairHighlighted = highlightPair
this.tagHighlighted = highlightTag
}
selectMatchingBrackets () {
if (!this.bracket1Range && !this.bracket2Range) return
this.editor.setSelectedBufferRanges([this.bracket1Range, this.bracket2Range])
this.matchManager.changeBracketsMode = true
}
removeMatchingBrackets () {
if (this.editor.hasMultipleCursors()) {
this.editor.backspace()
return
}
this.editor.transact(() => {
if (this.editor.getLastSelection().isEmpty()) {
this.editor.selectLeft()
}
const text = this.editor.getSelectedText()
this.editor.moveRight()
// check if the character to the left is part of a pair
if (
this.matchManager.pairedCharacters.hasOwnProperty(text) ||
this.matchManager.pairedCharactersInverse.hasOwnProperty(text)
) {
let {position, matchPosition, bracket} = this.findCurrentPair()
if (position && matchPosition) {
this.editor.setCursorBufferPosition(matchPosition)
this.editor.delete()
// if on the same line and the cursor is in front of an end pair
// offset by one to make up for the missing character
if (position.row === matchPosition.row && this.matchManager.pairedCharactersInverse.hasOwnProperty(bracket)) {
position = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
}
this.editor.setCursorBufferPosition(position)
this.editor.delete()
} else {
this.editor.backspace()
}
} else {
this.editor.backspace()
}
})
}
findMatchingEndBracket (startBracketPosition, startBracket, endBracket) {
if (startBracket === endBracket) return
if (this.hasSyntaxTree()) {
return this.findMatchingEndBracketWithSyntaxTree(startBracketPosition, startBracket, endBracket)
} else {
const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(startBracketPosition)
if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
return this.findMatchingEndBracketWithRegexSearch(startBracketPosition, startBracket, endBracket)
}
}
findMatchingStartBracket (endBracketPosition, startBracket, endBracket) {
if (startBracket === endBracket) return
if (this.hasSyntaxTree()) {
return this.findMatchingStartBracketWithSyntaxTree(endBracketPosition, startBracket, endBracket)
} else {
const scopeDescriptor = this.editor.scopeDescriptorForBufferPosition(endBracketPosition)
if (this.isScopeCommentedOrString(scopeDescriptor.getScopesArray())) return
return this.findMatchingStartBracketWithRegexSearch(endBracketPosition, startBracket, endBracket)
}
}
findMatchingEndBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
let result
const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
new Range(bracketPosition, bracketEndPosition),
node => {
if (bracketEndPosition.isGreaterThan(node.startPosition) && bracketEndPosition.isLessThan(node.endPosition)) {
const matchNode = node.children.find(child =>
bracketEndPosition.isLessThanOrEqual(child.startPosition) &&
child.type === endBracket
)
if (matchNode) result = Point.fromObject(matchNode.startPosition)
return true
}
}
)
return result
}
findMatchingStartBracketWithSyntaxTree (bracketPosition, startBracket, endBracket) {
let result
const bracketEndPosition = bracketPosition.traverse([0, startBracket.length])
this.editor.buffer.getLanguageMode().getSyntaxNodeContainingRange(
new Range(bracketPosition, bracketEndPosition),
node => {
if (bracketPosition.isGreaterThan(node.startPosition)) {
const matchNode = node.children.find(child =>
bracketPosition.isGreaterThanOrEqual(child.endPosition) &&
child.type === startBracket
)
if (matchNode) result = Point.fromObject(matchNode.startPosition)
return true
}
}
)
return result
}
findMatchingTagNameRangesWithSyntaxTree () {
const position = this.editor.getCursorBufferPosition()
const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
if (startTag && (startTag.range.containsPoint(position) || endTag.range.containsPoint(position))) {
if (startTag === endTag) {
const {range} = startTag.child(1)
return {startRange: range, endRange: range}
} else if (endTag.firstChild.type === '</') {
return {
startRange: startTag.child(1).range,
endRange: endTag.child(1).range
}
} else {
return {
startRange: startTag.child(1).range,
endRange: endTag.child(2).range
}
}
} else {
return {}
}
}
findMatchingTagsWithSyntaxTree () {
const position = this.editor.getCursorBufferPosition()
const {startTag, endTag} = this.findContainingTagsWithSyntaxTree(position)
if (startTag) {
return {startRange: startTag.range, endRange: endTag.range}
} else {
return {}
}
}
findContainingTagsWithSyntaxTree (position) {
let startTag, endTag
if (position.column === this.editor.buffer.lineLengthForRow(position.row)) position.column--;
this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(position, node => {
if (node.type.includes('element') && node.childCount > 0) {
const {firstChild, lastChild} = node
if (
firstChild.childCount > 2 &&
firstChild.firstChild.type === '<'
) {
if (lastChild === firstChild && firstChild.lastChild.type === '/>') {
startTag = firstChild
endTag = firstChild
} else if (
lastChild.childCount > 2 &&
(lastChild.firstChild.type === '</' ||
lastChild.firstChild.type === '<' && lastChild.child(1).type === '/')
) {
startTag = firstChild
endTag = lastChild
}
}
return true
}
})
return {startTag, endTag}
}
findMatchingEndBracketWithRegexSearch (startBracketPosition, startBracket, endBracket) {
const scanRange = new Range(
startBracketPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL),
startBracketPosition.traverse(MAX_ROWS_TO_SCAN_FORWARD_TRAVERSAL)
)
let endBracketPosition = null
let unpairedCount = 0
this.editor.scanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
if (this.isRangeCommentedOrString(result.range)) return
switch (result.match[0]) {
case startBracket:
unpairedCount++
break
case endBracket:
unpairedCount--
if (unpairedCount < 0) {
endBracketPosition = result.range.start
result.stop()
}
break
}
})
return endBracketPosition
}
findMatchingStartBracketWithRegexSearch (endBracketPosition, startBracket, endBracket) {
const scanRange = new Range(
endBracketPosition.traverse(MAX_ROWS_TO_SCAN_BACKWARD_TRAVERSAL),
endBracketPosition
)
let startBracketPosition = null
let unpairedCount = 0
this.editor.backwardsScanInBufferRange(this.matchManager.pairRegexes[startBracket], scanRange, result => {
if (this.isRangeCommentedOrString(result.range)) return
switch (result.match[0]) {
case startBracket:
unpairedCount--
if (unpairedCount < 0) {
startBracketPosition = result.range.start
result.stop()
break
}
break
case endBracket:
unpairedCount++
}
})
return startBracketPosition
}
findPrecedingStartBracket (cursorPosition) {
if (this.hasSyntaxTree()) {
return this.findPrecedingStartBracketWithSyntaxTree(cursorPosition)
} else {
return this.findPrecedingStartBracketWithRegexSearch(cursorPosition)
}
}
findPrecedingStartBracketWithSyntaxTree (cursorPosition) {
let result
this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition(cursorPosition, node => {
for (const child of node.children) {
if (cursorPosition.isLessThanOrEqual(child.startPosition)) break
if (
child.type in this.matchManager.pairedCharacters ||
child.type in this.matchManager.pairedCharactersInverse
) {
result = Point.fromObject(child.startPosition)
return true
}
}
})
return result
}
findPrecedingStartBracketWithRegexSearch (cursorPosition) {
const scanRange = new Range(Point.ZERO, cursorPosition)
const startBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharacters).join(''))
const endBracket = _.escapeRegExp(_.keys(this.matchManager.pairedCharactersInverse).join(''))
const combinedRegExp = new RegExp(`[${startBracket}${endBracket}]`, 'g')
const startBracketRegExp = new RegExp(`[${startBracket}]`, 'g')
const endBracketRegExp = new RegExp(`[${endBracket}]`, 'g')
let startPosition = null
let unpairedCount = 0
this.editor.backwardsScanInBufferRange(combinedRegExp, scanRange, result => {
if (this.isRangeCommentedOrString(result.range)) return
if (result.match[0].match(endBracketRegExp)) {
unpairedCount++
} else if (result.match[0].match(startBracketRegExp)) {
unpairedCount--
if (unpairedCount < 0) {
startPosition = result.range.start
result.stop()
}
}
})
return startPosition
}
createMarker (bufferRange) {
const marker = this.editor.markBufferRange(bufferRange)
this.editor.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
if (atom.config.get('bracket-matcher.highlightMatchingLineNumber', {scope: this.editor.getRootScopeDescriptor()}) && this.gutter) {
this.gutter.decorateMarker(marker, {type: 'highlight', class: 'bracket-matcher', deprecatedRegionClass: 'bracket-matcher'})
}
return marker
}
findCurrentPair () {
const currentPosition = this.editor.getCursorBufferPosition()
const previousPosition = currentPosition.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
const nextPosition = currentPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL)
const currentCharacter = this.editor.getTextInBufferRange(new Range(currentPosition, nextPosition))
const previousCharacter = this.editor.getTextInBufferRange(new Range(previousPosition, currentPosition))
let position, matchPosition, currentBracket, matchingBracket
if ((matchingBracket = this.matchManager.pairedCharacters[currentCharacter])) {
position = currentPosition
currentBracket = currentCharacter
matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
} else if ((matchingBracket = this.matchManager.pairedCharacters[previousCharacter])) {
position = previousPosition
currentBracket = previousCharacter
matchPosition = this.findMatchingEndBracket(position, currentBracket, matchingBracket)
} else if ((matchingBracket = this.matchManager.pairedCharactersInverse[previousCharacter])) {
position = previousPosition
currentBracket = previousCharacter
matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
} else if ((matchingBracket = this.matchManager.pairedCharactersInverse[currentCharacter])) {
position = currentPosition
currentBracket = currentCharacter
matchPosition = this.findMatchingStartBracket(position, matchingBracket, currentBracket)
}
return {position, matchPosition, bracket: currentBracket}
}
goToMatchingBracket () {
if (!this.pairHighlighted) return this.gotoPrecedingStartBracket()
const position = this.editor.getCursorBufferPosition()
if (this.tagHighlighted) {
let tagCharacterOffset
let startRange = this.startMarker.getBufferRange()
const tagLength = startRange.end.column - startRange.start.column
let endRange = this.endMarker.getBufferRange()
if (startRange.compare(endRange) > 0) {
[startRange, endRange] = [endRange, startRange]
}
// include the <
startRange = new Range(startRange.start.traverse(ONE_CHAR_BACKWARD_TRAVERSAL), endRange.end.traverse(ONE_CHAR_BACKWARD_TRAVERSAL))
// include the </
endRange = new Range(endRange.start.traverse(TWO_CHARS_BACKWARD_TRAVERSAL), endRange.end.traverse(TWO_CHARS_BACKWARD_TRAVERSAL))
if (position.isLessThan(endRange.start)) {
tagCharacterOffset = position.column - startRange.start.column
if (tagCharacterOffset > 0) { tagCharacterOffset++ }
tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 2) // include </
this.editor.setCursorBufferPosition(endRange.start.traverse([0, tagCharacterOffset]))
} else {
tagCharacterOffset = position.column - endRange.start.column
if (tagCharacterOffset > 1) { tagCharacterOffset-- }
tagCharacterOffset = Math.min(tagCharacterOffset, tagLength + 1) // include <
this.editor.setCursorBufferPosition(startRange.start.traverse([0, tagCharacterOffset]))
}
} else {
const previousPosition = position.traverse(ONE_CHAR_BACKWARD_TRAVERSAL)
const startPosition = this.startMarker.getStartBufferPosition()
const endPosition = this.endMarker.getStartBufferPosition()
if (position.isEqual(startPosition)) {
this.editor.setCursorBufferPosition(endPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
} else if (previousPosition.isEqual(startPosition)) {
this.editor.setCursorBufferPosition(endPosition)
} else if (position.isEqual(endPosition)) {
this.editor.setCursorBufferPosition(startPosition.traverse(ONE_CHAR_FORWARD_TRAVERSAL))
} else if (previousPosition.isEqual(endPosition)) {
this.editor.setCursorBufferPosition(startPosition)
}
}
}
gotoPrecedingStartBracket () {
if (this.pairHighlighted) return
const matchPosition = this.findPrecedingStartBracket(this.editor.getCursorBufferPosition())
if (matchPosition) {
this.editor.setCursorBufferPosition(matchPosition)
} else {
let startRange, endRange
if (this.hasSyntaxTree()) {
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
} else {
({startRange, endRange} = this.tagFinder.findStartEndTags())
}
if (startRange) {
if (startRange.compare(endRange) > 0) {
[startRange, endRange] = [endRange, startRange]
}
this.editor.setCursorBufferPosition(startRange.start)
}
}
}
multiCursorSelect() {
this.editor.getCursorBufferPositions().forEach(position => {
let startPosition = this.findPrecedingStartBracket(position)
if(startPosition) {
const startBracket = this.editor.getTextInRange(Range.fromPointWithDelta(startPosition, 0, 1))
const endPosition = this.findMatchingEndBracket(startPosition, startBracket, this.matchManager.pairedCharacters[startBracket])
startPosition = startPosition.traverse([0, 1])
if (startPosition && endPosition) {
const rangeToSelect = new Range(startPosition, endPosition)
this.editor.addSelectionForBufferRange(rangeToSelect)
}
} else {
let startRange, endRange;
if (this.hasSyntaxTree()) {
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
} else {
({startRange, endRange} = this.tagFinder.findStartEndTags(true))
if (startRange && startRange.compare(endRange) > 0) {
[startRange, endRange] = [endRange, startRange]
}
}
if (startRange) {
const startPosition = startRange.end
const endPosition = endRange.start
const rangeToSelect = new Range(startPosition, endPosition)
this.editor.setSelectedBufferRange(rangeToSelect)
}
}
})
}
selectInsideBrackets () {
let endPosition, endRange, startPosition, startRange
if (this.pairHighlighted) {
startRange = this.startMarker.getBufferRange()
endRange = this.endMarker.getBufferRange()
if (this.tagHighlighted) {
if (this.hasSyntaxTree()) {
({startRange, endRange} = this.findMatchingTagsWithSyntaxTree())
} else {
({startRange, endRange} = this.tagFinder.findStartEndTags(true))
if (startRange && startRange.compare(endRange) > 0) {
[startRange, endRange] = [endRange, startRange]
}
}
}
startPosition = startRange.end
endPosition = endRange.start
const rangeToSelect = new Range(startPosition, endPosition)
this.editor.setSelectedBufferRange(rangeToSelect)
} else {
this.multiCursorSelect();
}
}
// Insert at the current cursor position a closing tag if there exists an
// open tag that is not closed afterwards.
closeTag () {
const cursorPosition = this.editor.getCursorBufferPosition()
const preFragment = this.editor.getTextInBufferRange([Point.ZERO, cursorPosition])
const postFragment = this.editor.getTextInBufferRange([cursorPosition, Point.INFINITY])
const tag = this.tagFinder.closingTagForFragments(preFragment, postFragment)
if (tag) {
this.editor.insertText(`</${tag}>`)
}
}
isCursorOnCommentOrString () {
return this.isScopeCommentedOrString(this.editor.getLastCursor().getScopeDescriptor().getScopesArray())
}
isRangeCommentedOrString (range) {
return this.isScopeCommentedOrString(this.editor.scopeDescriptorForBufferPosition(range.start).getScopesArray())
}
isScopeCommentedOrString (scopesArray) {
for (let scope of scopesArray.reverse()) {
scope = scope.split('.')
if (scope.includes('embedded') && scope.includes('source')) return false
if (scope.includes('comment') || scope.includes('string')) return true
}
return false
}
hasSyntaxTree () {
return this.editor.buffer.getLanguageMode().getSyntaxNodeAtPosition
}
}

View File

@ -0,0 +1,299 @@
const _ = require('underscore-plus')
const {CompositeDisposable} = require('atom')
const SelectorCache = require('./selector-cache')
module.exports =
class BracketMatcher {
constructor (editor, editorElement, matchManager) {
this.insertText = this.insertText.bind(this)
this.insertNewline = this.insertNewline.bind(this)
this.backspace = this.backspace.bind(this)
this.editor = editor
this.matchManager = matchManager
this.subscriptions = new CompositeDisposable()
this.bracketMarkers = []
this.origEditorInsertText = this.editor.insertText.bind(this.editor)
_.adviseBefore(this.editor, 'insertText', this.insertText)
_.adviseBefore(this.editor, 'insertNewline', this.insertNewline)
_.adviseBefore(this.editor, 'backspace', this.backspace)
this.subscriptions.add(
atom.commands.add(editorElement, 'bracket-matcher:remove-brackets-from-selection', event => {
if (!this.removeBrackets()) event.abortKeyBinding()
}),
this.editor.onDidDestroy(() => this.unsubscribe())
)
}
insertText (text, options) {
if (!text) return true
if ((options && options.select) || (options && options.undo === 'skip')) return true
let autoCompleteOpeningBracket, bracketMarker, pair
if (this.matchManager.changeBracketsMode) {
this.matchManager.changeBracketsMode = false
if (this.isClosingBracket(text)) {
text = this.matchManager.pairedCharactersInverse[text]
}
if (this.isOpeningBracket(text)) {
this.editor.mutateSelectedText(selection => {
const selectionText = selection.getText()
if (this.isOpeningBracket(selectionText)) {
selection.insertText(text)
}
if (this.isClosingBracket(selectionText)) {
selection.insertText(this.matchManager.pairedCharacters[text])
}
})
return false
}
}
if (this.wrapSelectionInBrackets(text)) return false
if (this.editor.hasMultipleCursors()) return true
const cursorBufferPosition = this.editor.getCursorBufferPosition()
const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
const previousCharacter = previousCharacters.slice(-1)
const hasWordAfterCursor = /\w/.test(nextCharacter)
const hasWordBeforeCursor = /\w/.test(previousCharacter)
const hasQuoteBeforeCursor = this.isQuote(previousCharacter) && (previousCharacter === text[0])
const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
const hasEscapeSequenceBeforeCursor = endsWithEscapeSequence(previousCharacters)
if (text === '#' && this.isCursorOnInterpolatedString()) {
autoCompleteOpeningBracket = this.getScopedSetting('bracket-matcher.autocompleteBrackets') && !hasEscapeCharacterBeforeCursor
text += '{'
pair = '}'
} else {
autoCompleteOpeningBracket = (
this.isOpeningBracket(text) &&
!hasWordAfterCursor &&
this.getScopedSetting('bracket-matcher.autocompleteBrackets') &&
!(this.isQuote(text) && (hasWordBeforeCursor || hasQuoteBeforeCursor || hasEscapeSequenceBeforeCursor)) &&
!hasEscapeCharacterBeforeCursor
)
pair = this.matchManager.pairedCharacters[text]
}
let skipOverExistingClosingBracket = false
if (this.isClosingBracket(text) && (nextCharacter === text) && !hasEscapeCharacterBeforeCursor) {
bracketMarker = this.bracketMarkers.find(marker => marker.isValid() && marker.getBufferRange().end.isEqual(cursorBufferPosition))
if (bracketMarker || this.getScopedSetting('bracket-matcher.alwaysSkipClosingPairs')) {
skipOverExistingClosingBracket = true
}
}
if (skipOverExistingClosingBracket) {
if (bracketMarker) bracketMarker.destroy()
_.remove(this.bracketMarkers, bracketMarker)
this.editor.moveRight()
return false
} else if (autoCompleteOpeningBracket) {
this.editor.transact(() => {
this.origEditorInsertText(text + pair)
this.editor.moveLeft()
})
const range = [cursorBufferPosition, cursorBufferPosition.traverse([0, text.length])]
this.bracketMarkers.push(this.editor.markBufferRange(range))
return false
}
}
insertNewline () {
if (this.editor.hasMultipleCursors()) return
if (!this.editor.getLastSelection().isEmpty()) return
const cursorBufferPosition = this.editor.getCursorBufferPosition()
const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
const previousCharacter = previousCharacters.slice(-1)
const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
if (
this.matchManager.pairsWithExtraNewline[previousCharacter] === nextCharacter &&
!hasEscapeCharacterBeforeCursor
) {
this.editor.transact(() => {
this.origEditorInsertText('\n\n')
this.editor.moveUp()
if (this.getScopedSetting('editor.autoIndent')) {
const cursorRow = this.editor.getCursorBufferPosition().row
this.editor.autoIndentBufferRows(cursorRow, cursorRow + 1)
}
})
return false
}
}
backspace () {
if (this.editor.hasMultipleCursors()) return
if (!this.editor.getLastSelection().isEmpty()) return
const cursorBufferPosition = this.editor.getCursorBufferPosition()
const previousCharacters = this.editor.getTextInBufferRange([[cursorBufferPosition.row, 0], cursorBufferPosition])
const nextCharacter = this.editor.getTextInBufferRange([cursorBufferPosition, cursorBufferPosition.traverse([0, 1])])
const previousCharacter = previousCharacters.slice(-1)
const hasEscapeCharacterBeforeCursor = endsWithEscapeCharacter(previousCharacters)
if (
this.matchManager.pairedCharacters[previousCharacter] === nextCharacter &&
!hasEscapeCharacterBeforeCursor &&
this.getScopedSetting('bracket-matcher.autocompleteBrackets')
) {
this.editor.transact(() => {
this.editor.moveLeft()
this.editor.delete()
this.editor.delete()
})
return false
}
}
removeBrackets () {
let bracketsRemoved = false
this.editor.mutateSelectedText(selection => {
let selectionEnd
if (!this.selectionIsWrappedByMatchingBrackets(selection)) return
const range = selection.getBufferRange()
const options = {reversed: selection.isReversed()}
const selectionStart = range.start
if (range.start.row === range.end.row) {
selectionEnd = range.end.traverse([0, -2])
} else {
selectionEnd = range.end.traverse([0, -1])
}
const text = selection.getText()
selection.insertText(text.substring(1, text.length - 1))
selection.setBufferRange([selectionStart, selectionEnd], options)
bracketsRemoved = true
})
return bracketsRemoved
}
wrapSelectionInBrackets (bracket) {
let pair
if (bracket === '#') {
if (!this.isCursorOnInterpolatedString()) return false
bracket = '#{'
pair = '}'
} else {
if (!this.isOpeningBracket(bracket)) return false
pair = this.matchManager.pairedCharacters[bracket]
}
if (!this.editor.selections.some(s => !s.isEmpty())) return false
if (!this.getScopedSetting('bracket-matcher.wrapSelectionsInBrackets')) return false
let selectionWrapped = false
this.editor.mutateSelectedText(selection => {
let selectionEnd
if (selection.isEmpty()) return
// Don't wrap in #{} if the selection spans more than one line
if ((bracket === '#{') && !selection.getBufferRange().isSingleLine()) return
selectionWrapped = true
const range = selection.getBufferRange()
const options = {reversed: selection.isReversed()}
selection.insertText(`${bracket}${selection.getText()}${pair}`)
const selectionStart = range.start.traverse([0, bracket.length])
if (range.start.row === range.end.row) {
selectionEnd = range.end.traverse([0, bracket.length])
} else {
selectionEnd = range.end
}
selection.setBufferRange([selectionStart, selectionEnd], options)
})
return selectionWrapped
}
isQuote (string) {
return /['"`]/.test(string)
}
isCursorOnInterpolatedString () {
const cursor = this.editor.getLastCursor()
const languageMode = this.editor.getBuffer().getLanguageMode()
if (languageMode.getSyntaxNodeAtPosition) {
const node = languageMode.getSyntaxNodeAtPosition(
cursor.getBufferPosition(),
(node, grammar) => grammar.scopeName === 'source.ruby' && /string|symbol/.test(node.type)
)
if (node) {
const {firstChild} = node
if (firstChild) {
return ['"', ':"', '%('].includes(firstChild.text)
}
}
return false
} else {
if (this.interpolatedStringSelector == null) {
const segments = [
'*.*.*.interpolated.ruby',
'string.interpolated.ruby',
'string.regexp.interpolated.ruby',
'string.quoted.double.coffee',
'string.unquoted.heredoc.ruby',
'string.quoted.double.livescript',
'string.quoted.double.heredoc.livescript',
'string.quoted.double.elixir',
'string.quoted.double.heredoc.elixir',
'comment.documentation.heredoc.elixir'
]
this.interpolatedStringSelector = SelectorCache.get(segments.join(' | '))
}
return this.interpolatedStringSelector.matches(this.editor.getLastCursor().getScopeDescriptor().getScopesArray())
}
}
isOpeningBracket (string) {
return this.matchManager.pairedCharacters.hasOwnProperty(string)
}
isClosingBracket (string) {
return this.matchManager.pairedCharactersInverse.hasOwnProperty(string)
}
selectionIsWrappedByMatchingBrackets (selection) {
if (selection.isEmpty()) return false
const selectedText = selection.getText()
const firstCharacter = selectedText[0]
const lastCharacter = selectedText[selectedText.length - 1]
return this.matchManager.pairedCharacters[firstCharacter] === lastCharacter
}
unsubscribe () {
this.subscriptions.dispose()
}
getScopedSetting (key) {
return atom.config.get(key, {scope: this.editor.getRootScopeDescriptor()})
}
}
const BACKSLASHES_REGEX = /(\\+)$/
const ESCAPE_SEQUENCE_REGEX = /(\\+)[^\\]$/
// odd number of backslashes
function endsWithEscapeCharacter (string) {
const backslashesMatch = string.match(BACKSLASHES_REGEX)
return backslashesMatch && backslashesMatch[1].length % 2 === 1
}
// even number of backslashes or odd number of backslashes followed by another character
function endsWithEscapeSequence (string) {
const backslashesMatch = string.match(BACKSLASHES_REGEX)
const escapeSequenceMatch = string.match(ESCAPE_SEQUENCE_REGEX)
return (
(escapeSequenceMatch && escapeSequenceMatch[1].length % 2 === 1) ||
(backslashesMatch && backslashesMatch[1].length % 2 === 0)
)
}

View File

@ -0,0 +1,20 @@
const MatchManager = require('./match-manager')
const BracketMatcherView = require('./bracket-matcher-view')
const BracketMatcher = require('./bracket-matcher')
module.exports = {
activate () {
const watchedEditors = new WeakSet()
atom.workspace.observeTextEditors(editor => {
if (watchedEditors.has(editor)) return
const editorElement = atom.views.getView(editor)
const matchManager = new MatchManager(editor, editorElement)
new BracketMatcherView(editor, editorElement, matchManager)
new BracketMatcher(editor, editorElement, matchManager)
watchedEditors.add(editor)
editor.onDidDestroy(() => watchedEditors.delete(editor))
})
}
}

View File

@ -0,0 +1,60 @@
const _ = require('underscore-plus')
const {CompositeDisposable} = require('atom')
module.exports =
class MatchManager {
appendPair (pairList, [itemLeft, itemRight]) {
const newPair = {}
newPair[itemLeft] = itemRight
pairList = _.extend(pairList, newPair)
}
processAutoPairs (autocompletePairs, pairedList, dataFun) {
if (autocompletePairs.length) {
for (let autocompletePair of autocompletePairs) {
const pairArray = autocompletePair.split('')
this.appendPair(pairedList, dataFun(pairArray))
}
}
}
updateConfig () {
this.pairedCharacters = {}
this.pairedCharactersInverse = {}
this.pairRegexes = {}
this.pairsWithExtraNewline = {}
this.processAutoPairs(this.getScopedSetting('bracket-matcher.autocompleteCharacters'), this.pairedCharacters, x => [x[0], x[1]])
this.processAutoPairs(this.getScopedSetting('bracket-matcher.autocompleteCharacters'), this.pairedCharactersInverse, x => [x[1], x[0]])
this.processAutoPairs(this.getScopedSetting('bracket-matcher.pairsWithExtraNewline'), this.pairsWithExtraNewline, x => [x[0], x[1]])
for (let startPair in this.pairedCharacters) {
const endPair = this.pairedCharacters[startPair]
this.pairRegexes[startPair] = new RegExp(`[${_.escapeRegExp(startPair + endPair)}]`, 'g')
}
}
getScopedSetting (key) {
return atom.config.get(key, {scope: this.editor.getRootScopeDescriptor()})
}
constructor (editor, editorElement) {
this.destroy = this.destroy.bind(this)
this.editor = editor
this.subscriptions = new CompositeDisposable()
this.updateConfig()
// Subscribe to config changes
const scope = this.editor.getRootScopeDescriptor()
this.subscriptions.add(
atom.config.observe('bracket-matcher.autocompleteCharacters', {scope}, () => this.updateConfig()),
atom.config.observe('bracket-matcher.pairsWithExtraNewline', {scope}, () => this.updateConfig()),
this.editor.onDidDestroy(this.destroy)
)
this.changeBracketsMode = false
}
destroy () {
this.subscriptions.dispose()
}
}

View File

@ -0,0 +1,11 @@
const {ScopeSelector} = require('second-mate')
const cache = {}
exports.get = function (selector) {
let scopeSelector = cache[selector]
if (!scopeSelector) {
scopeSelector = new ScopeSelector(selector)
cache[selector] = scopeSelector
}
return scopeSelector
}

View File

@ -0,0 +1,18 @@
[
"area",
"base",
"br",
"col",
"command",
"embed",
"hr",
"img",
"input",
"keygen",
"link",
"meta",
"param",
"source",
"track",
"wbr"
]

View File

@ -0,0 +1,258 @@
const {Range} = require('atom')
const _ = require('underscore-plus')
const SelfClosingTags = require('./self-closing-tags')
const TAG_SELECTOR_REGEX = /(\b|\.)(meta\.tag|punctuation\.definition\.tag)/
const COMMENT_SELECTOR_REGEX = /(\b|\.)comment/
// Creates a regex to match opening tag with match[1] and closing tags with match[2]
//
// * tagNameRegexStr - a regex string describing how to match the tagname.
// Should not contain capturing match groups.
//
// Returns a {RegExp}.
const generateTagStartOrEndRegex = function (tagNameRegexStr) {
const notSelfClosingTagEnd = "(?:[^>\\/\"']|\"[^\"]*\"|'[^']*')*>"
return new RegExp(`<(${tagNameRegexStr})${notSelfClosingTagEnd}|<\\/(${tagNameRegexStr})>`)
}
const tagStartOrEndRegex = generateTagStartOrEndRegex('\\w[-\\w]*(?:(?:\\:|\\.)\\w[-\\w]*)*')
// Helper to find the matching start/end tag for the start/end tag under the
// cursor in XML, HTML, etc. editors.
module.exports =
class TagFinder {
constructor (editor) {
// 1. Tag prefix
// 2. Closing tag (optional)
// 3. Tag name
// 4. Attributes (ids, classes, etc. - optional)
// 5. Tag suffix
// 6. Self-closing tag (optional)
this.editor = editor
this.tagPattern = /(<(\/)?)(.+?)(\s+.*?)?((\/)?>|$)/
this.wordRegex = /.*?(>|$)/
}
patternForTagName (tagName) {
tagName = _.escapeRegExp(tagName)
// 1. Start tag
// 2. Tag name
// 3. Attributes (optional)
// 4. Tag suffix
// 5. Self-closing tag (optional)
// 6. End tag
return new RegExp(`(<(${tagName})(\\s+[^>]*?)?((/)?>))|(</${tagName}[^>]*>)`, 'gi')
}
isRangeCommented (range) {
return this.scopesForPositionMatchRegex(range.start, COMMENT_SELECTOR_REGEX)
}
isCursorOnTag () {
return this.scopesForPositionMatchRegex(this.editor.getCursorBufferPosition(), TAG_SELECTOR_REGEX)
}
scopesForPositionMatchRegex (position, regex) {
const {tokenizedBuffer, buffer} = this.editor
const {grammar} = tokenizedBuffer
let column = 0
const line = tokenizedBuffer.tokenizedLineForRow(position.row)
if (line == null) { return false }
const lineLength = buffer.lineLengthForRow(position.row)
const scopeIds = line.openScopes.slice()
for (let i = 0; i < line.tags.length; i++) {
const tag = line.tags[i]
if (tag >= 0) {
const nextColumn = column + tag
if ((nextColumn > position.column) || (nextColumn === lineLength)) { break }
column = nextColumn
} else if ((tag & 1) === 1) {
scopeIds.push(tag)
} else {
scopeIds.pop()
}
}
return scopeIds.some(scopeId => regex.test(grammar.scopeForId(scopeId)))
}
findStartTag (tagName, endPosition, fullRange = false) {
const scanRange = new Range([0, 0], endPosition)
const pattern = this.patternForTagName(tagName)
let startRange = null
let unpairedCount = 0
this.editor.backwardsScanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
if (this.isRangeCommented(range)) return
const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
if (isSelfClosingTag) return
if (isStartTag) {
unpairedCount--
if (unpairedCount < 0) {
stop()
startRange = range
if (!fullRange) {
// Move the start past the initial <
startRange.start = startRange.start.translate([0, 1])
// End right after the tag name
startRange.end = startRange.start.translate([0, tagName.length])
}
}
} else {
unpairedCount++
}
})
return startRange
}
findEndTag (tagName, startPosition, fullRange = false) {
const scanRange = new Range(startPosition, this.editor.buffer.getEndPosition())
const pattern = this.patternForTagName(tagName)
let endRange = null
let unpairedCount = 0
this.editor.scanInBufferRange(pattern, scanRange, ({match, range, stop}) => {
if (this.isRangeCommented(range)) return
const [entireMatch, isStartTag, tagName, attributes, suffix, isSelfClosingTag, isEndTag] = match
if (isSelfClosingTag) return
if (isStartTag) {
unpairedCount++
} else {
unpairedCount--
if (unpairedCount < 0) {
stop()
endRange = range
if (!fullRange) {
// Subtract </ and > from range
endRange = range.translate([0, 2], [0, -1])
}
}
}
})
return endRange
}
findStartEndTags (fullRange = false) {
let ranges = {}
const endPosition = this.editor.getLastCursor().getCurrentWordBufferRange({wordRegex: this.wordRegex}).end
this.editor.backwardsScanInBufferRange(this.tagPattern, [[0, 0], endPosition], ({match, range, stop}) => {
stop()
const [entireMatch, prefix, isClosingTag, tagName, attributes, suffix, isSelfClosingTag] = Array.from(match)
let startRange = range
if (!fullRange) {
if (range.start.row === range.end.row) {
// Move the start past the initial <
startRange.start = startRange.start.translate([0, prefix.length])
// End right after the tag name
startRange.end = startRange.start.translate([0, tagName.length])
} else {
startRange = Range.fromObject([range.start.translate([0, prefix.length]), [range.start.row, Infinity]])
}
}
let endRange
if (isSelfClosingTag) {
endRange = startRange
} else if (isClosingTag) {
endRange = this.findStartTag(tagName, startRange.start, fullRange)
} else {
endRange = this.findEndTag(tagName, startRange.end, fullRange)
}
if (startRange && endRange) ranges = {startRange, endRange}
})
return ranges
}
findMatchingTags () {
return (this.isCursorOnTag() && this.findStartEndTags()) || {}
}
// Parses a fragment of html returning the stack (i.e., an array) of open tags
//
// fragment - the fragment of html to be analysed
// stack - an array to be populated (can be non-empty)
// matchExpr - a RegExp describing how to match opening/closing tags
// the opening/closing descriptions must be captured subexpressions
// so that the code can refer to match[1] to check if an opening
// tag has been found, and to match[2] to check if a closing tag
// has been found
// cond - a condition to be checked at each iteration. If the function
// returns false the processing is immediately interrupted. When
// called the current stack is provided to the function.
//
// Returns an array of strings. Each string is a tag that is still to be closed
// (the most recent non closed tag is at the end of the array).
parseFragment (fragment, stack, matchExpr, cond) {
let match = fragment.match(matchExpr)
while (match && cond(stack)) {
if (SelfClosingTags.indexOf(match[1]) === -1) {
const topElem = stack[stack.length - 1]
if (match[2] && (topElem === match[2])) {
stack.pop()
} else if (match[1]) {
stack.push(match[1])
}
}
fragment = fragment.substr(match.index + match[0].length)
match = fragment.match(matchExpr)
}
return stack
}
// Parses the given fragment of html code returning the last unclosed tag.
//
// fragment - a string containing a fragment of html code.
//
// Returns an array of strings. Each string is a tag that is still to be closed
// (the most recent non closed tag is at the end of the array).
tagsNotClosedInFragment (fragment) {
return this.parseFragment(fragment, [], tagStartOrEndRegex, () => true)
}
// Parses the given fragment of html code and returns true if the given tag
// has a matching closing tag in it. If tag is reopened and reclosed in the
// given fragment then the end point of that pair does not count as a matching
// closing tag.
tagDoesNotCloseInFragment (tags, fragment) {
if (tags.length === 0) { return false }
let stack = tags
const stackLength = stack.length
const tag = tags[tags.length - 1]
const escapedTag = _.escapeRegExp(tag)
stack = this.parseFragment(fragment, stack, generateTagStartOrEndRegex(escapedTag), s =>
s.length >= stackLength || s[s.length - 1] === tag
)
return (stack.length > 0) && (stack[stack.length - 1] === tag)
}
// Parses preFragment and postFragment returning the last open tag in
// preFragment that is not closed in postFragment.
//
// Returns a tag name or null if it can't find it.
closingTagForFragments (preFragment, postFragment) {
const tags = this.tagsNotClosedInFragment(preFragment)
const tag = tags[tags.length - 1]
if (this.tagDoesNotCloseInFragment(tags, postFragment)) {
return tag
} else {
return null
}
}
}

View File

@ -0,0 +1,22 @@
'menu': [
{
'label': 'Packages'
'submenu': [
'label': 'Bracket Matcher'
'submenu': [
{ 'label': 'Go To Matching Bracket', 'command': 'bracket-matcher:go-to-matching-bracket' }
{ 'label': 'Select Inside Brackets', 'command': 'bracket-matcher:select-inside-brackets' }
{ 'label': 'Remove Brackets From Selection', 'command': 'bracket-matcher:remove-brackets-from-selection' }
{ 'label': 'Close Current Tag', 'command': 'bracket-matcher:close-tag' }
{ 'label': 'Remove Matching Brackets', 'command': 'bracket-matcher:remove-matching-brackets' }
{ 'label': 'Select Matching Brackets', 'command': 'bracket-matcher:select-matching-brackets' }
]
]
},
{
'label': 'Selection'
'submenu': [
{ 'label': 'Select Inside Brackets', 'command': 'bracket-matcher:select-inside-brackets' }
]
}
]

View File

@ -0,0 +1,67 @@
{
"name": "bracket-matcher",
"version": "0.92.0",
"main": "./lib/main",
"description": "Highlight the matching bracket for the `(){}[]` character under the cursor. Move the cursor to the matching bracket with `ctrl-m`.",
"repository": "https://github.com/pulsar-edit/bracket-matcher",
"license": "MIT",
"engines": {
"atom": "*"
},
"dependencies": {
"underscore-plus": "1.x"
},
"configSchema": {
"autocompleteCharacters": {
"description": "Autocompleted characters treated as matching pairs, such as `()`, and `{}`.",
"type": "array",
"default": [
"()",
"[]",
"{}",
"\"\"",
"''",
"``",
"“”",
"",
"«»",
""
],
"items": {
"type": "string"
}
},
"pairsWithExtraNewline": {
"description": "Automatically add a newline between the pair when enter is pressed.",
"type": "array",
"default": [
"()",
"[]",
"{}"
],
"items": {
"type": "string"
}
},
"autocompleteBrackets": {
"type": "boolean",
"default": true,
"description": "Autocomplete bracket and quote characters, such as `(` and `)`, and `\"`."
},
"wrapSelectionsInBrackets": {
"type": "boolean",
"default": true,
"description": "Wrap selected text in brackets or quotes when the editor contains selections and the opening bracket or quote is typed."
},
"highlightMatchingLineNumber": {
"type": "boolean",
"default": false,
"description": "Highlight the line number of the matching bracket."
},
"alwaysSkipClosingPairs": {
"type": "boolean",
"default": false,
"description": "Always skip closing pairs in front of the cursor."
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,142 @@
const TagFinder = require('../lib/tag-finder')
const tagFinder = new TagFinder()
describe('closeTag', () => {
describe('TagFinder::parseFragment', () => {
let fragment = ''
beforeEach(() => fragment = '<html><head><body></body>')
it('returns the last not closed elem in fragment, matching a given pattern', () => {
const stack = tagFinder.parseFragment(fragment, [], /<(\w+)|<\/(\w*)/, () => true)
expect(stack[stack.length - 1]).toBe('head')
})
it('stops when cond become true', () => {
const stack = tagFinder.parseFragment(fragment, [], /<(\w+)|<\/(\w*)/, () => false)
expect(stack.length).toBe(0)
})
it('uses the given match expression to match tags', () => {
const stack = tagFinder.parseFragment(fragment, [], /<(body)|(notag)/, () => true)
expect(stack[stack.length - 1]).toBe('body')
})
})
describe('TagFinder::tagsNotClosedInFragment', () => {
it('returns the outermost tag not closed in an HTML fragment', () => {
const fragment = '<html><head></head><body><h1><p></p>'
const tags = tagFinder.tagsNotClosedInFragment(fragment)
expect(tags).toEqual(['html', 'body', 'h1'])
})
it('is not confused by tag attributes', () => {
const fragment = '<html><head></head><body class="c"><h1 class="p"><p></p>'
const tags = tagFinder.tagsNotClosedInFragment(fragment)
expect(tags).toEqual(['html', 'body', 'h1'])
})
it('is not confused by namespace prefixes', () => {
const fragment = '<xhtml:html><xhtml:body><xhtml:h1>'
const tags = tagFinder.tagsNotClosedInFragment(fragment)
expect(tags).toEqual(['xhtml:html', 'xhtml:body', 'xhtml:h1'])
})
})
describe('TagFinder::tagDoesNotCloseInFragment', () => {
it('returns true if the given tag is not closed in the given fragment', () => {
const fragment = '</other1></other2></html>'
expect(tagFinder.tagDoesNotCloseInFragment('body', fragment)).toBe(true)
})
it('returns false if the given tag is closed in the given fragment', () => {
const fragment = '</other1></body></html>'
expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(false)
})
it('returns true even if the given tag is re-opened and re-closed', () => {
const fragment = '<other> </other><body></body><html>'
expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(true)
})
it('returns false even if the given tag is re-opened and re-closed before closing', () => {
const fragment = '<other> </other><body></body></body><html>'
expect(tagFinder.tagDoesNotCloseInFragment(['body'], fragment)).toBe(false)
})
})
describe('TagFinder::closingTagForFragments', () => {
it('returns the last opened in preFragment tag that is not closed in postFragment', () => {
const preFragment = '<html><head></head><body><h1></h1><p>'
const postFragment = '</body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
})
it('correctly handles empty postFragment', () => {
const preFragment = '<html><head></head><body><h1></h1><p>'
const postFragment = ''
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('p')
})
it('correctly handles malformed tags', () => {
const preFragment = '<html><head></head></htm'
const postFragment = ''
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('html')
})
it('returns null if there is no open tag to be closed', () => {
const preFragment = '<html><head></head><body><h1></h1><p>'
const postFragment = '</p></body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe(null)
})
it('correctly closes tags containing hyphens', () => {
const preFragment = '<html><head></head><body><h1></h1><my-element>'
const postFragment = '</body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('my-element')
})
it('correctly closes tags containing attributes', () => {
const preFragment = '<html><head></head><body class="foo bar"><div>'
const postFragment = '</body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('div')
})
it('correctly closes tags containing an XML namespace', () => {
const preFragment = '<html><head></head><body><custom:tag>'
const postFragment = '</body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('custom:tag')
})
it('correctly closes tags containing multiple XML namespaces', () => {
// This is not exactly valid syntax but it can't hurt to support it
const preFragment = '<html><head></head><body><custom:custom2:tag>'
const postFragment = '</body></html>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('custom:custom2:tag')
})
it('correctly closes tags in the present of JSX tags containing member accesses', () => {
const preFragment = '<Foo><Bar.Baz></Bar.Baz>'
const postFragment = ''
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo')
})
it('correctly closes JSX tags containing member accesses', () => {
const preFragment = '<Foo.Bar><div></div>'
const postFragment = ''
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo.Bar')
})
it('correctly closes JSX tags containing deep member accesses', () => {
const preFragment = '<Foo.Bar.Baz><div></div>'
const postFragment = ''
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('Foo.Bar.Baz')
})
it('correctly closes tags when there are other tags with the same prefix', () => {
const preFragment = '<thead><th>'
const postFragment = '</thead>'
expect(tagFinder.closingTagForFragments(preFragment, postFragment)).toBe('th')
})
})
})

View File

@ -0,0 +1,6 @@
[link-1](http://example.com/)
[another-link-2](http://example.com/)
[yet-another-link-3](http://example.com/)
[final-link-4](http://example.com/)
(different-type)
not anywhere

View File

@ -0,0 +1,15 @@
<html>
<body>
<div>
<div>
<div>
<p><a>
</div>
</div>
</div>
<div>
<div>
<div>
</div>
</div>

View File

@ -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));
};

View File

@ -0,0 +1,17 @@
<html>
<body id="bod">
<div>
<div>
<div>
hello
</div>
</div>
</div>
<div>
<div>
world
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,11 @@
@import "syntax-variables";
.bracket-matcher .region {
border-bottom: 1px dotted lime;
position: absolute;
}
.line-number.bracket-matcher.bracket-matcher {
color: @syntax-gutter-text-color-selected;
background-color: @syntax-gutter-background-color-selected;
}

View File

@ -295,7 +295,7 @@ describe "Clojure grammar", ->
#!/usr/bin/env boot
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/boot
@ -306,7 +306,7 @@ describe "Clojure grammar", ->
#!\t/usr/bin/env --boot=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -325,7 +325,7 @@ describe "Clojure grammar", ->
"-*- font:x;foo : bar ; mode : ClojureScript ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*clojure-*- */
@ -343,7 +343,7 @@ describe "Clojure grammar", ->
// -*-font:mode;mode:clojure--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -370,7 +370,7 @@ describe "Clojure grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=clojure ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=clojure:
@ -388,4 +388,4 @@ describe "Clojure grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=clojure ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -29,7 +29,7 @@ describe "CoffeeScript (Literate) grammar", ->
#!/usr/local/bin/env coffee --literate -w
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#!/usr/local/bin/coffee --no-head -literate -w
@ -37,7 +37,7 @@ describe "CoffeeScript (Literate) grammar", ->
#!/usr/local/bin/env coffee --illiterate -w=l
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -56,7 +56,7 @@ describe "CoffeeScript (Literate) grammar", ->
"-*- font:x;foo : bar ; mode : LiTcOFFEe ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*litcoffee-*- */
@ -74,7 +74,7 @@ describe "CoffeeScript (Literate) grammar", ->
// -*-font:mode;mode:litcoffee--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -101,7 +101,7 @@ describe "CoffeeScript (Literate) grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=litcoffee ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=litcoffee:
@ -119,4 +119,4 @@ describe "CoffeeScript (Literate) grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=litcoffee ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -1393,7 +1393,7 @@ describe "CoffeeScript grammar", ->
#!/usr/bin/env coffee
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/coffee
@ -1404,7 +1404,7 @@ describe "CoffeeScript grammar", ->
#!\t/usr/bin/env --coffee=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -1423,7 +1423,7 @@ describe "CoffeeScript grammar", ->
"-*- font:x;foo : bar ; mode : Coffee ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*coffee-*- */
@ -1441,7 +1441,7 @@ describe "CoffeeScript grammar", ->
// -*-font:mode;mode:coffee--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -1468,7 +1468,7 @@ describe "CoffeeScript grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=cOFFEe ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=coffee:
@ -1486,4 +1486,4 @@ describe "CoffeeScript grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=coffee ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -3508,7 +3508,7 @@ describe 'CSS grammar', ->
"-*- font:x;foo : bar ; mode : cSS ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*css-*- */
@ -3526,7 +3526,7 @@ describe 'CSS grammar', ->
// -*-font:mode;mode:css--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -3553,7 +3553,7 @@ describe 'CSS grammar', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=css ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=css:
@ -3571,7 +3571,7 @@ describe 'CSS grammar', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=CSS ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
describe "Missing supported properties regressions", ->
it "recognises place-items property as supported", ->

View File

@ -18,45 +18,45 @@ describe 'Go settings', ->
it 'matches lines correctly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.go'])
expect(increaseIndentRegex.testSync(' case true:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' default:')).toBeTruthy()
expect(increaseIndentRegex.testSync('func something() {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' if true {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' else {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' switch {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' switch true {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' select {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' select true {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' for v := range val {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' type something struct {')).toBeTruthy()
expect(increaseIndentRegex.testSync(' fmt.Printf("some%s",')).toBeTruthy()
expect(increaseIndentRegex.testSync(' aSlice := []string{}{')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' case true:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' default:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('func something() {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' if true {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' else {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' switch {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' switch true {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' select {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' select true {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' for v := range val {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' for i := 0; i < 10; i++ {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' type something struct {')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' fmt.Printf("some%s",')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' aSlice := []string{}{')).toBeTruthy()
it 'matches lines correctly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.go'])
expect(decreaseIndentRegex.testSync(' case true:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' default:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' }')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' },')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' )')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' ),')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' case true:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' default:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' }')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' },')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' )')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' ),')).toBeTruthy()
it 'matches lines correctly using the decreaseNextIndentPattern', ->
decreaseNextIndentRegex = languageMode.decreaseNextIndentRegexForScopeDescriptor(['source.go'])
expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"))')).toBeTruthy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println("something")),')).toBeTruthy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"), "x"),')).toBeTruthy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println(fmt.Sprint("something"))),')).toBeTruthy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println(fmt.Sprint("something"), "x")),')).toBeTruthy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"))')).toBeTruthy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something")),')).toBeTruthy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"), "x"),')).toBeTruthy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println(fmt.Sprint("something"))),')).toBeTruthy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println(fmt.Sprint("something"), "x")),')).toBeTruthy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println("something")')).toBeFalsy()
expect(decreaseNextIndentRegex.testSync(' fmt.Println("something"),')).toBeFalsy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something")')).toBeFalsy()
expect(decreaseNextIndentRegex.findNextMatchSync(' fmt.Println("something"),')).toBeFalsy()
# a line with many (), testing for catastrophic backtracking.
# see https://github.com/atom/language-go/issues/78
longLine = 'first.second().third().fourth().fifth().sixth().seventh().eighth().ninth().tenth()'
expect(decreaseNextIndentRegex.testSync(longLine)).toBeFalsy()
expect(decreaseNextIndentRegex.findNextMatchSync(longLine)).toBeFalsy()

View File

@ -601,8 +601,8 @@ describe 'TextMate HTML grammar', ->
describe 'firstLineMatch', ->
it 'recognises HTML5 doctypes', ->
expect(grammar.firstLineRegex.scanner.findNextMatchSync('<!DOCTYPE html>')).not.toBeNull()
expect(grammar.firstLineRegex.scanner.findNextMatchSync('<!doctype HTML>')).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync('<!DOCTYPE html>')).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync('<!doctype HTML>')).not.toBeNull()
it 'recognises Emacs modelines', ->
valid = '''
@ -621,7 +621,7 @@ describe 'TextMate HTML grammar', ->
"-*- font:x;foo : bar ; mode : HtML ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
/* --*html-*- */
@ -640,7 +640,7 @@ describe 'TextMate HTML grammar', ->
// -*-font:mode;mode:html--*-
'''
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Vim modelines', ->
valid = '''
@ -667,7 +667,7 @@ describe 'TextMate HTML grammar', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=html ts=4
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
ex: se filetype=html:
@ -685,7 +685,7 @@ describe 'TextMate HTML grammar', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=HTML ts=4
'''
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
describe 'tags', ->
it 'tokenizes style tags as such', ->

View File

@ -2400,7 +2400,7 @@ describe "JavaScript grammar", ->
#!/usr/bin/env node
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/node
@ -2411,7 +2411,7 @@ describe "JavaScript grammar", ->
#!\t/usr/bin/env --node=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -2430,7 +2430,7 @@ describe "JavaScript grammar", ->
"-*- font:x;foo : bar ; mode : jS ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*js-*- */
@ -2448,7 +2448,7 @@ describe "JavaScript grammar", ->
// -*-font:mode;mode:js--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -2475,7 +2475,7 @@ describe "JavaScript grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=javascript ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=javascript:
@ -2493,4 +2493,4 @@ describe "JavaScript grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=javascript ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -721,9 +721,6 @@
'match': '^\\s*(package)\\s+([^\\s;]+)'
'name': 'meta.class.perl'
}
{
'include: "#sub'
}
{
'captures':
'1':

View File

@ -183,7 +183,7 @@ describe "Perl 6 grammar", ->
#!/usr/bin/env perl6
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#! pearl6
@ -200,11 +200,11 @@ describe "Perl 6 grammar", ->
#!\t/usr/bin/env --perl6=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises the Perl6 pragma", ->
line = "use v6;"
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
it "recognises Emacs modelines", ->
modelines = """
@ -222,7 +222,7 @@ describe "Perl 6 grammar", ->
"-*- font:x;foo : bar ; mode : pErL6 ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in modelines.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*perl6-*- */
@ -241,7 +241,7 @@ describe "Perl 6 grammar", ->
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -268,7 +268,7 @@ describe "Perl 6 grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=perl6 ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=perl6:
@ -286,7 +286,7 @@ describe "Perl 6 grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=perl6 ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
# Local variables:
# mode: CoffeeScript

View File

@ -1439,7 +1439,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#!/usr/bin/env perl
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
#! pearl
@ -1456,7 +1456,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
#!\t/usr/bin/env --perl=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -1474,7 +1474,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
"-*- font:x;foo : bar ; mode : pErL ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*perl-*- */
@ -1491,7 +1491,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
// -*-font:mode;mode:perl--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -1518,7 +1518,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# vim:noexpandtab titlestring=hi\|there\\\\ ft=perl ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=perl:
@ -1536,7 +1536,7 @@ Assigned to: @<<<<<<<<<<<<<<<<<<<<<< ^<<<<<<<<<<<<<<<<<<<<<<<<<<<<
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=perl ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
# Local variables:
# mode: CoffeeScript

View File

@ -56,7 +56,7 @@
'body': 'elseif (${1:condition}) {\n\t${0:// code...}\n}'
'for …':
'prefix': 'for'
'body': 'for ($${1:i}=${2:0}; $${1:i} < $3; $${1:i}++) {\n\t${0:// code...}\n}'
'body': 'for ($${1:i} = ${2:0}; $${1:i} < $3; $${1:i}++) {\n\t${0:// code...}\n}'
'foreach …':
'prefix': 'foreach'
'body': 'foreach ($${1:variable} as $${2:key} ${3:=> $${4:value}}) {\n\t${0:// code...}\n}'

View File

@ -161,11 +161,11 @@ describe 'PHP in HTML', ->
<?php namespace foo;
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
# Do not allow matching XML declaration until the grammar scoring system takes into account
# the length of the first line match so that longer matches get the priority over shorter matches.
expect(grammar.firstLineRegex.scanner.findNextMatchSync('<?xml version="1.0" encoding="UTF-8"?>')).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync('<?xml version="1.0" encoding="UTF-8"?>')).toBeNull()
it 'recognises interpreter directives', ->
valid = '''
@ -184,7 +184,7 @@ describe 'PHP in HTML', ->
#!/usr/bin/env php
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
\x20#!/usr/sbin/php
@ -196,7 +196,7 @@ describe 'PHP in HTML', ->
#!\t/usr/bin/env --php=bar
'''
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Emacs modelines', ->
valid = '''
@ -215,7 +215,7 @@ describe 'PHP in HTML', ->
"-*- font:x;foo : bar ; mode : php ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
/* --*php-*- */
@ -233,7 +233,7 @@ describe 'PHP in HTML', ->
// -*-font:mode;mode:php--*-
'''
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'recognises Vim modelines', ->
valid = '''
@ -260,7 +260,7 @@ describe 'PHP in HTML', ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=phtml ts=4
'''
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = '''
ex: se filetype=php:
@ -278,7 +278,7 @@ describe 'PHP in HTML', ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=php ts=4
'''
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it 'should tokenize <?php use Some\\Name ?>', ->
lines = grammar.tokenizeLines '''

View File

@ -18,66 +18,66 @@ describe 'Python settings', ->
it 'matches lines correctly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.python'])
expect(increaseIndentRegex.testSync('for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.testSync(' for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.testSync('async for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.testSync(' async for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.testSync('class TheClass(Object):')).toBeTruthy()
expect(increaseIndentRegex.testSync(' class TheClass(Object):')).toBeTruthy()
expect(increaseIndentRegex.testSync('def f(x):')).toBeTruthy()
expect(increaseIndentRegex.testSync(' def f(x):')).toBeTruthy()
expect(increaseIndentRegex.testSync('async def f(x):')).toBeTruthy()
expect(increaseIndentRegex.testSync(' async def f(x):')).toBeTruthy()
expect(increaseIndentRegex.testSync('if this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' if this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.testSync('elif this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' elif this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.testSync('else:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' else:')).toBeTruthy()
expect(increaseIndentRegex.testSync('except Exception:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' except Exception:')).toBeTruthy()
expect(increaseIndentRegex.testSync('except Exception as e:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' except Exception as e:')).toBeTruthy()
expect(increaseIndentRegex.testSync('finally:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' finally:')).toBeTruthy()
expect(increaseIndentRegex.testSync('with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.testSync('async with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' async with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.testSync('while True:')).toBeTruthy()
expect(increaseIndentRegex.testSync(' while True:')).toBeTruthy()
expect(increaseIndentRegex.testSync('\t\t while True:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('async for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' async for i in range(n):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('class TheClass(Object):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' class TheClass(Object):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('def f(x):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' def f(x):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('async def f(x):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' async def f(x):')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('if this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' if this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('elif this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' elif this_var == that_var:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('else:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' else:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('except Exception:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' except Exception:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('except Exception as e:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' except Exception as e:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('finally:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' finally:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('async with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' async with open("filename") as f:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('while True:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync(' while True:')).toBeTruthy()
expect(increaseIndentRegex.findNextMatchSync('\t\t while True:')).toBeTruthy()
it 'does not match lines incorrectly using the increaseIndentPattern', ->
increaseIndentRegex = languageMode.increaseIndentRegexForScopeDescriptor(['source.python'])
expect(increaseIndentRegex.testSync('for i in range(n)')).toBeFalsy()
expect(increaseIndentRegex.testSync('class TheClass(Object)')).toBeFalsy()
expect(increaseIndentRegex.testSync('def f(x)')).toBeFalsy()
expect(increaseIndentRegex.testSync('if this_var == that_var')).toBeFalsy()
expect(increaseIndentRegex.testSync('"for i in range(n):"')).toBeFalsy()
expect(increaseIndentRegex.findNextMatchSync('for i in range(n)')).toBeFalsy()
expect(increaseIndentRegex.findNextMatchSync('class TheClass(Object)')).toBeFalsy()
expect(increaseIndentRegex.findNextMatchSync('def f(x)')).toBeFalsy()
expect(increaseIndentRegex.findNextMatchSync('if this_var == that_var')).toBeFalsy()
expect(increaseIndentRegex.findNextMatchSync('"for i in range(n):"')).toBeFalsy()
it 'matches lines correctly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.python'])
expect(decreaseIndentRegex.testSync('elif this_var == that_var:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' elif this_var == that_var:')).toBeTruthy()
expect(decreaseIndentRegex.testSync('else:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' else:')).toBeTruthy()
expect(decreaseIndentRegex.testSync('except Exception:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' except Exception:')).toBeTruthy()
expect(decreaseIndentRegex.testSync('except Exception as e:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' except Exception as e:')).toBeTruthy()
expect(decreaseIndentRegex.testSync('finally:')).toBeTruthy()
expect(decreaseIndentRegex.testSync(' finally:')).toBeTruthy()
expect(decreaseIndentRegex.testSync('\t\t finally:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('elif this_var == that_var:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' elif this_var == that_var:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('else:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' else:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('except Exception:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' except Exception:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('except Exception as e:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' except Exception as e:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('finally:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync(' finally:')).toBeTruthy()
expect(decreaseIndentRegex.findNextMatchSync('\t\t finally:')).toBeTruthy()
it 'does not match lines incorrectly using the decreaseIndentPattern', ->
decreaseIndentRegex = languageMode.decreaseIndentRegexForScopeDescriptor(['source.python'])
# NOTE! This first one is different from most other rote tests here.
expect(decreaseIndentRegex.testSync('else: expression()')).toBeFalsy()
expect(decreaseIndentRegex.testSync('elif this_var == that_var')).toBeFalsy()
expect(decreaseIndentRegex.testSync(' elif this_var == that_var')).toBeFalsy()
expect(decreaseIndentRegex.testSync('else')).toBeFalsy()
expect(decreaseIndentRegex.testSync(' "finally:"')).toBeFalsy()
expect(decreaseIndentRegex.findNextMatchSync('else: expression()')).toBeFalsy()
expect(decreaseIndentRegex.findNextMatchSync('elif this_var == that_var')).toBeFalsy()
expect(decreaseIndentRegex.findNextMatchSync(' elif this_var == that_var')).toBeFalsy()
expect(decreaseIndentRegex.findNextMatchSync('else')).toBeFalsy()
expect(decreaseIndentRegex.findNextMatchSync(' "finally:"')).toBeFalsy()

View File

@ -14,8 +14,8 @@ describe "Python grammar", ->
grammar = atom.grammars.grammarForScopeName("source.python")
it "recognises shebang on firstline", ->
expect(grammar.firstLineRegex.scanner.findNextMatchSync("#!/usr/bin/env python")).not.toBeNull()
expect(grammar.firstLineRegex.scanner.findNextMatchSync("#! /usr/bin/env python")).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync("#!/usr/bin/env python")).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync("#! /usr/bin/env python")).not.toBeNull()
it "parses the grammar", ->
expect(grammar).toBeDefined()

View File

@ -964,7 +964,7 @@ describe "TextMate Ruby grammar", ->
#!/usr/bin/env ruby
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/ruby
@ -975,7 +975,7 @@ describe "TextMate Ruby grammar", ->
#!\t/usr/bin/env --ruby=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -994,7 +994,7 @@ describe "TextMate Ruby grammar", ->
"-*- font:x;foo : bar ; mode : RUBY ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*ruby-*- */
@ -1012,7 +1012,7 @@ describe "TextMate Ruby grammar", ->
// -*-font:mode;mode:ruby--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -1039,7 +1039,7 @@ describe "TextMate Ruby grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=ruby ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=ruby:
@ -1057,4 +1057,4 @@ describe "TextMate Ruby grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=ruby ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -361,7 +361,7 @@ describe "Shell script grammar", ->
#!/usr/bin/env bash
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
\x20#!/usr/sbin/bash
@ -373,7 +373,7 @@ describe "Shell script grammar", ->
#!\t/usr/bin/env --bash=bar
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Emacs modelines", ->
valid = """
@ -392,7 +392,7 @@ describe "Shell script grammar", ->
"-*- font:x;foo : bar ; mode : sH ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*sh-*- */
@ -410,7 +410,7 @@ describe "Shell script grammar", ->
// -*-font:mode;mode:sh--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -437,7 +437,7 @@ describe "Shell script grammar", ->
# vim:noexpandtab titlestring=hi\|there\\\\ ft=sh ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=sh:
@ -455,4 +455,4 @@ describe "Shell script grammar", ->
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=sh ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -122,7 +122,7 @@ attrName="attrValue">
"-*- font:x;foo : bar ; mode : xMl ; bar : foo ; foooooo:baaaaar;fo:ba-*-";
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
/* --*XML-*- */
@ -140,7 +140,7 @@ attrName="attrValue">
// -*-font:mode;mode:xml--*-
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises Vim modelines", ->
valid = """
@ -167,7 +167,7 @@ attrName="attrValue">
# vim:noexpandtab titlestring=hi\|there\\\\ ft=xml ts=4
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
ex: se filetype=xml:
@ -185,7 +185,7 @@ attrName="attrValue">
# vim:noexpandtab titlestring=hi\\|there\\\\\\ ft=xml ts=4
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()
it "recognises a valid XML declaration", ->
valid = """
@ -196,7 +196,7 @@ attrName="attrValue">
<?xml version="1.0" encoding='UTF-8' standalone='no' ?>
"""
for line in valid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).not.toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).not.toBeNull()
invalid = """
<?XML version="1.0"?>
@ -209,4 +209,4 @@ attrName="attrValue">
<?xml version="1.0">
"""
for line in invalid.split /\n/
expect(grammar.firstLineRegex.scanner.findNextMatchSync(line)).toBeNull()
expect(grammar.firstLineRegex.findNextMatchSync(line)).toBeNull()

View File

@ -0,0 +1,3 @@
{
"title": "Welcome"
}

View File

@ -2,4 +2,6 @@
import WelcomePackage from './welcome-package';
export const t = atom.i18n.getT("welcome");
export default new WelcomePackage();

View File

@ -3,6 +3,7 @@
import etch from 'etch';
import path from 'path';
import { t } from "./main";
export default class WelcomeView {
constructor(props) {
@ -126,7 +127,7 @@ export default class WelcomeView {
}
getTitle() {
return 'Welcome';
return t("title");
}
isEqual(other) {

View File

@ -99,7 +99,7 @@ if [ $OS == 'Mac' ]; then
PULSAR_PATH="$HOME/Applications"
else
# We haven't found an Pulsar.app, use spotlight to search for Pulsar
PULSAR_PATH="$(mdfind "kMDItemCFBundleIdentifier == 'com.github.pulsar'" | grep -v ShipIt | head -1 | xargs -0 dirname)"
PULSAR_PATH="$(mdfind "kMDItemCFBundleIdentifier == 'dev.pulsar-edit.pulsar'" | grep -v ShipIt | head -1 | xargs -0 dirname)"
# Exit if Pulsar can't be found
if [ ! -x "$PULSAR_PATH/$ATOM_APP_NAME" ]; then

View File

@ -7,6 +7,6 @@ Section: devel
Priority: optional
Architecture: <%= arch %>
Installed-Size: <%= installedSize %>
Maintainer: GitHub <atom@github.com>
Maintainer: Pulsar-Edit <admin@pulsar-edit.dev>
Description: <%= description %>
Pulsar is a free and open source text editor that is modern, approachable, and hackable to the core.

View File

@ -2,10 +2,10 @@
Name=<%= appName %>
Comment=<%= description %>
GenericName=Text Editor
Exec=env ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=false <%= installDir %>/bin/<%= appFileName %> %F
Exec=env ATOM_DISABLE_SHELLING_OUT_FOR_ENVIRONMENT=false <%= installDir %>/bin/<%= appFileName %> --no-sandbox %F
Icon=<%= iconPath %>
Type=Application
StartupNotify=true
Categories=GTK;Utility;TextEditor;Development;
MimeType=application/javascript;application/json;application/x-httpd-eruby;application/x-httpd-php;application/x-httpd-php3;application/x-httpd-php4;application/x-httpd-php5;application/x-ruby;application/x-bash;application/x-csh;application/x-sh;application/x-zsh;application/x-shellscript;application/x-sql;application/x-tcl;application/xhtml+xml;application/xml;application/xml-dtd;application/xslt+xml;text/coffeescript;text/css;text/html;text/plain;text/xml;text/xml-dtd;text/x-bash;text/x-c++;text/x-c++hdr;text/x-c++src;text/x-c;text/x-chdr;text/x-csh;text/x-csrc;text/x-dsrc;text/x-diff;text/x-go;text/x-java;text/x-java-source;text/x-makefile;text/x-markdown;text/x-objc;text/x-perl;text/x-php;text/x-python;text/x-ruby;text/x-sh;text/x-zsh;text/yaml;inode/directory
StartupWMClass=atom
StartupWMClass=pulsar

View File

@ -4,9 +4,9 @@
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<vendor>Pulsar</vendor>
<action id="atom.pkexec.dd">
<description gettext-domain="atom">Admin privileges required</description>
<message gettext-domain="atom">Please enter your password to save this file</message>
<action id="pulsar.pkexec.dd">
<description gettext-domain="pulsar">Admin privileges required</description>
<message gettext-domain="pulsar">Please enter your password to save this file</message>
<annotate key="org.freedesktop.policykit.exec.path">/bin/dd</annotate>
<annotate key="org.freedesktop.policykit.exec.allow_gui">true</annotate>
<defaults>

View File

@ -3,7 +3,7 @@ Version: <%= version %>
Release: 0.1%{?dist}
Summary: <%= description %>
License: MIT
URL: https://atom.io/
URL: https://pulsar-edit.dev
AutoReqProv: no # Avoid libchromiumcontent.so missing dependency
Prefix: <%= installDir %>
@ -25,7 +25,7 @@ Requires: alsa-lib, git-core, (glib2 or kde-cli-tools or xdg-utils), lsb-core-no
mkdir -p "%{buildroot}/<%= installDir %>/share/<%= appFileName %>/"
cp -r "<%= appName %>"/* "%{buildroot}/<%= installDir %>/share/<%= appFileName %>/"
mkdir -p "%{buildroot}/<%= installDir %>/bin/"
ln -sf "../share/<%= appFileName %>/resources/app/apm/node_modules/.bin/apm" "%{buildroot}/<%= installDir %>/bin/<%= apmFileName %>"
ln -sf "../share/<%= appFileName %>/resources/app/ppm/node_modules/.bin/apm" "%{buildroot}/<%= installDir %>/bin/<%= apmFileName %>"
cp pulsar.sh "%{buildroot}/<%= installDir %>/bin/<%= appFileName %>"
chmod 755 "%{buildroot}/<%= installDir %>/bin/<%= appFileName %>"
mkdir -p "%{buildroot}/<%= installDir %>/share/applications/"

View File

@ -9,7 +9,7 @@
<key>CFBundleExecutable</key>
<string>Pulsar Helper</string>
<key>CFBundleIdentifier</key>
<string>com.github.pulsar.helper</string>
<string>dev.pulsar-edit.pulsar.helper</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View File

@ -7,7 +7,7 @@
<key>CFBundleIconFile</key>
<string>pulsar.icns</string>
<key>CFBundleIdentifier</key>
<string>com.github.pulsar</string>
<string>dev.pulsar-edit.pulsar</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>

View File

@ -55,6 +55,7 @@ let options = {
"package.json",
"dot-atom/**/*",
"exports/**/*",
"i18n/**/*",
"resources/**/*",
"src/**/*",
"static/**/*",

View File

@ -5,13 +5,14 @@ const temp = require('temp').track();
const TextBuffer = require('text-buffer');
const GrammarRegistry = require('../src/grammar-registry');
const TreeSitterGrammar = require('../src/tree-sitter-grammar');
const FirstMate = require('first-mate');
const { OnigRegExp } = require('oniguruma');
const SecondMate = require('second-mate');
const { OnigScanner } = SecondMate;
describe('GrammarRegistry', () => {
let grammarRegistry;
beforeEach(() => {
beforeEach(async () => {
await SecondMate.ready
grammarRegistry = new GrammarRegistry({ config: atom.config });
expect(subscriptionCount(grammarRegistry)).toBe(1);
});
@ -102,7 +103,7 @@ describe('GrammarRegistry', () => {
);
const grammar = grammarRegistry.grammarForId('source.js');
expect(grammar instanceof FirstMate.Grammar).toBe(true);
expect(grammar instanceof SecondMate.Grammar).toBe(true);
expect(grammar.scopeName).toBe('source.js');
grammarRegistry.removeGrammar(grammar);
@ -127,7 +128,7 @@ describe('GrammarRegistry', () => {
grammarRegistry.removeGrammar(grammar);
expect(
grammarRegistry.grammarForId('source.js') instanceof FirstMate.Grammar
grammarRegistry.grammarForId('source.js') instanceof SecondMate.Grammar
).toBe(true);
});
});
@ -560,7 +561,7 @@ describe('GrammarRegistry', () => {
const grammar = grammarRegistry.selectGrammar('test.js');
expect(grammar.scopeName).toBe('source.js');
expect(grammar instanceof FirstMate.Grammar).toBe(true);
expect(grammar instanceof SecondMate.Grammar).toBe(true);
});
it('favors a tree-sitter grammar over a text-mate grammar when `core.useTreeSitterParsers` is true', () => {
@ -766,7 +767,7 @@ describe('GrammarRegistry', () => {
grammarRegistry.addGrammar(grammar1);
const grammar2 = {
name: 'foo++',
contentRegex: new OnigRegExp('.*bar'),
contentRegex: new OnigScanner(['.*bar']),
fileTypes: ['foo']
};
grammarRegistry.addGrammar(grammar2);

View File

@ -46,6 +46,7 @@ const TextEditorRegistry = require('./text-editor-registry');
const AutoUpdateManager = require('./auto-update-manager');
const StartupTime = require('./startup-time');
const getReleaseChannel = require('./get-release-channel');
const I18n = require("./i18n");
const packagejson = require("../package.json");
const stat = util.promisify(fs.stat);
@ -125,6 +126,11 @@ class AtomEnvironment {
/** @type {StyleManager} */
this.styles = new StyleManager();
this.i18n = new I18n({
notificationManager: this.notifications,
config: this.config
});
/** @type {PackageManager} */
this.packages = new PackageManager({
config: this.config,
@ -135,7 +141,8 @@ class AtomEnvironment {
grammarRegistry: this.grammars,
deserializerManager: this.deserializers,
viewRegistry: this.views,
uriHandlerRegistry: this.uriHandlerRegistry
uriHandlerRegistry: this.uriHandlerRegistry,
i18n: this.i18n
});
/** @type {ThemeManager} */
@ -149,6 +156,7 @@ class AtomEnvironment {
/** @type {MenuManager} */
this.menu = new MenuManager({
i18n: this.i18n,
keymapManager: this.keymaps,
packageManager: this.packages
});
@ -274,6 +282,19 @@ class AtomEnvironment {
this.project.replace(projectSpecification);
}
this.packages.initialize({
devMode,
configDirPath: this.configDirPath,
resourcePath,
safeMode
});
this.i18n.initialize({
configDirPath: this.configDirPath,
packages: this.packages,
resourcePath
});
this.menu.initialize({ resourcePath });
this.contextMenu.initialize({ resourcePath, devMode });
@ -287,12 +308,6 @@ class AtomEnvironment {
this.commands.attach(this.window);
this.styles.initialize({ configDirPath: this.configDirPath });
this.packages.initialize({
devMode,
configDirPath: this.configDirPath,
resourcePath,
safeMode
});
this.themes.initialize({
configDirPath: this.configDirPath,
resourcePath,

View File

@ -1,7 +1,7 @@
const _ = require('underscore-plus');
const Grim = require('grim');
const CSON = require('season');
const FirstMate = require('first-mate');
const SecondMate = require('second-mate');
const { Disposable, CompositeDisposable } = require('event-kit');
const TextMateLanguageMode = require('./text-mate-language-mode');
const TreeSitterLanguageMode = require('./tree-sitter-language-mode');
@ -20,7 +20,7 @@ module.exports = class GrammarRegistry {
constructor({ config } = {}) {
this.config = config;
this.subscriptions = new CompositeDisposable();
this.textmateRegistry = new FirstMate.GrammarRegistry({
this.textmateRegistry = new SecondMate.GrammarRegistry({
maxTokensPerLine: 100,
maxLineLength: 1000
});
@ -264,7 +264,7 @@ module.exports = class GrammarRegistry {
if (grammar.contentRegex) {
const contentMatch = isTreeSitter
? grammar.contentRegex.test(contents)
: grammar.contentRegex.testSync(contents);
: grammar.contentRegex.findNextMatchSync(contents);
if (contentMatch) {
score += 0.05;
} else {
@ -339,8 +339,8 @@ module.exports = class GrammarRegistry {
.split('\n')
.slice(0, numberOfNewlinesInRegex + 1)
.join('\n');
if (grammar.firstLineRegex.testSync) {
return grammar.firstLineRegex.testSync(prefix);
if (grammar.firstLineRegex.findNextMatchSync) {
return grammar.firstLineRegex.findNextMatchSync(prefix);
} else {
return grammar.firstLineRegex.test(prefix);
}

154
src/i18n-helpers.js Normal file
View File

@ -0,0 +1,154 @@
const _ = require('underscore-plus');
const fs = require("fs");
const path = require("path");
const { parse } = require("@formatjs/icu-messageformat-parser");
class I18nCacheHelper {
constructor({ configDirPath, i18n }) {
/**
* cachedASTs[ns][lang] = string objs
* (same shape as registeredStrings but with ASTs instead of strings)
*/
this.cachedASTs = {};
/** @type {string} */
this.configDirPath = configDirPath;
this.i18n = i18n;
this.loadCaches();
this.debouncedCleanAndSave = _.debounce(() => {
this.cleanCaches(this.i18n.registeredStrings);
this.saveCaches();
}, 5_000);
}
fetchAST(ns, _path, str, lang) {
let path = [ns, lang, ..._path];
let ast = optionalTravelDownObjectPath(
this.cachedASTs,
path
);
if (ast && "_AST" in ast) return ast._AST;
ast = parse(str, {
// requiresOtherClause
});
let lastBit = path.pop();
let cachePath = travelDownOrMakePath(this.cachedASTs, path);
cachePath[lastBit] = { _AST: ast };
this.debouncedCleanAndSave();
return ast;
}
/**
* go through `this.cachedASTs`, find stuff that doesn't exist in `registeredStrings`,
* then yeet them
*/
cleanCaches(registeredStrings, cachedASTs) {
if (!cachedASTs) cachedASTs = this.cachedASTs;
Object.entries(cachedASTs).forEach(([k, cachedValue]) => {
let registeredValue = registeredStrings[k];
// path doesn't exist
if (!registeredValue) {
delete cachedASTs[k];
return;
}
// path is an object
if (typeof registeredValue === "object") {
// cached is not AST (plain obj) (good)
if (
typeof cachedValue === "object"
&& !("_AST" in cachedValue)
) {
this.cleanCaches(registeredValue, cachedValue);
return;
}
// cached is AST (bad)
delete cachedASTs[k];
return;
}
// path is a string
if (typeof registeredValue === "string") {
// cached is AST (good)
if ("_AST" in cachedValue) return;
// cached is not AST (bad)
delete cachedASTs[k];
return;
}
});
}
saveCaches() {
let cachedir = path.join(
this.configDirPath,
"compile-cache",
"i18n"
);
fs.mkdirSync(cachedir, { recursive: true });
let cachefile = path.join(cachedir, "strings.json");
fs.writeFileSync(cachefile, JSON.stringify(this.cachedASTs));
}
loadCaches() {
let cachefile = path.join(
this.configDirPath,
"compile-cache",
"i18n",
"strings.json"
);
if (fs.existsSync(cachefile)) {
this.cachedASTs = JSON.parse(fs.readFileSync(cachefile, "utf-8"));
}
}
}
function walkStrings(strings, cb, accum = []) {
Object.entries(strings).forEach(([k, v]) => {
let path = [...accum, k];
if (typeof v === "string") cb(path, v, true);
else if (typeof v === "object") {
cb(path, null, false);
walkStrings(v, cb, path);
}
});
}
function travelDownObjectPath(obj, path) {
for (const pathFragment of path) {
obj = obj[pathFragment];
}
return obj;
}
function optionalTravelDownObjectPath(obj, path) {
for (const pathFragment of path) {
obj = obj[pathFragment];
if (!obj) return undefined;
}
return obj;
}
function travelDownOrMakePath(obj, path) {
for (const pathFragment of path) {
if (!obj[pathFragment]) obj[pathFragment] = {};
obj = obj[pathFragment];
}
return obj;
}
module.exports = {
I18nCacheHelper,
walkStrings,
travelDownObjectPath,
optionalTravelDownObjectPath,
travelDownOrMakePath
};

253
src/i18n.js Normal file
View File

@ -0,0 +1,253 @@
const { splitKeyPath } = require("key-path-helpers");
const fs = require("fs-plus");
const path = require("path");
const { default: IntlMessageFormat } = require("intl-messageformat");
const {
I18nCacheHelper,
walkStrings,
travelDownObjectPath,
optionalTravelDownObjectPath,
travelDownOrMakePath
} = require("./i18n-helpers");
class I18n {
constructor({ notificationManager, config }) {
this.notificationManager = notificationManager;
this.config = config;
this.initialized = false;
/** registeredStrings[ns][lang] = string objs */
this.registeredStrings = { core: {} };
this.cachedFormatters = {};
}
initialize({ configDirPath, packages, resourcePath }) {
/** @type {string} */
this.configDirPath = configDirPath;
this.packages = packages;
/** @type {string} */
this.resourcePath = resourcePath;
/** @type {I18nCacheHelper} */
this.cacheHelper = new I18nCacheHelper({ configDirPath, i18n: this });
const ext = ".json";
const extlen = ext.length;
const dirpath = path.join(resourcePath, "i18n");
const dircontents = fs.readdirSync(dirpath);
let languageTypes = dircontents.filter(p => p.endsWith(ext))
.map(p => p.substring(0, p.length - extlen))
.map(p => ({
value: p,
description: `${new Intl.DisplayNames(p, { type: "language" }).of(p)} (${p})`
}));
this.config.setSchema("core.languageSettings", {
type: "object",
description: "These settings currently require a full restart of Pulsar to take effect.",
properties: {
primaryLanguage: {
type: "string",
order: 1,
default: "en",
enum: languageTypes
},
fallbackLanguages: {
type: "array",
order: 2,
description: "List of fallback languages, if something can't be found in the primary language. Note; `en` is always the last fallback language, to ensure that things at least show up.",
default: [],
items: {
// Array enum is meh, if you pause for the briefest moment and you
// didn't stop at a valid enum value, the entry you just typed gets yeeted
type: "string"
}
}
}
});
this.updateConfigs();
// Preload languages (getting an obj would place it in the cache)
// TODO reassess preloading the "en" fallback when we have more progress on translations
this.getCoreLanguage(this.primaryLanguage);
this.getCoreLanguage("en");
this.packages.onDidActivatePackage(pkg => {
this.getPkgLanguage(pkg.name, this.primaryLanguage);
this.getPkgLanguage(pkg.name, "en");
});
this.packages.onDidDeactivatePackage(pkg => {
if (pkg.name in this.registeredStrings) {
delete this.registeredStrings[pkg.name];
}
});
this.t = (key, opts) => {
const path = splitKeyPath(key);
const languagesToTry = [
this.primaryLanguage,
...this.fallbackLanguages,
"en"
];
for (const lang of languagesToTry) {
const str = this.tSingleLanguage(lang, path, opts);
if (typeof str === "string") return str;
}
// key fallback
let string_opts = opts
? `: { ${
Object.entries(opts)
.map(o => `"${o[0]}": "${o[1]}"`)
.join(", ")
} }`
: "";
return `${key}${string_opts}`;
}
this.initialized = true;
}
updateConfigs() {
this.primaryLanguage = this.config.get("core.languageSettings.primaryLanguage");
this.fallbackLanguages = this.config.get("core.languageSettings.fallbackLanguages");
}
registerStrings(packageId, strings) {
if (!(typeof this.registeredStrings[packageId] === "object")) this.registeredStrings[packageId] = {};
walkStrings(strings, (path, string, isString) => {
let last = path.pop();
let obj = travelDownObjectPath(this.registeredStrings[packageId], path);
if (isString) {
obj[last] = string;
} else if (!obj[last]) {
obj[last] = {};
}
});
}
getT(ns) {
if (!ns) return this.t;
return (key, formats) => this.t(`${ns}.${key}`, formats);
}
/**
* attempts to translate for a single language, given a preparsed path array.
* @return undefined if the language or string cannot be found,
* and throws an error if the path isn't right.
*/
tSingleLanguage(lang, _path, opts) {
let path = [..._path];
const ns = path.shift();
if (!ns) throw new Error(`key path seems invalid: [${_path.map(p => `"${p}"`).join(", ")}]`);
const languageObj = this.getLanguageObj(ns, lang);
if (languageObj === undefined) return undefined;
const str = optionalTravelDownObjectPath(languageObj, path);
if (str !== undefined) {
return this.format(ns, path, str, lang, opts);
} else {
return undefined;
}
}
/**
* gets a language object from a specified namespace
* @return undefined if it can't be found
*/
getLanguageObj(ns, lang) {
return ns === "core"
? this.getCoreLanguage(lang)
: this.getPkgLanguage(ns, lang);
}
/**
* gets a language for `core`
* @return undefined if it can't be found
*/
getCoreLanguage(lang) {
const loaded = this.registeredStrings.core[lang]
if (loaded !== undefined) return loaded;
const fetched = this.fetchCoreLanguageFile(lang);
if (fetched === undefined) return undefined;
this.registeredStrings.core[lang] = fetched;
return fetched;
}
/**
* gets a language for a specific namespace
* @return undefined if it can't be found
*/
getPkgLanguage(ns, lang) {
const loaded = this.registeredStrings[ns]?.[lang];
if (loaded !== undefined) return loaded;
const fetched = this.fetchPkgLanguageFile(ns, lang);
if (fetched === undefined) return fetched;
if (typeof this.registeredStrings[ns] !== "object" || this.registeredStrings[ns] === null) {
this.registeredStrings[ns] = {};
}
this.registeredStrings[ns][lang] = fetched;
}
/**
* fetches a core language from the disk
* @return undefined if it can't be found
*/
fetchCoreLanguageFile(lang) {
let filepath = path.join(this.resourcePath, "i18n", `${lang}.json`);
let contents = JSON.parse(fs.readFileSync(filepath));
return contents;
}
/**
* fetches a language for a specific namespace
* @return undefined if it can't be found
*/
fetchPkgLanguageFile(ns, lang) {
// TODO this could probably be optimised
let packages = this.packages.getAvailablePackages();
let foundPackage = packages.find(p => p.name === ns);
const i18nDir = path.join(foundPackage.path, "i18n");
const langfile = path.join(i18nDir, `${lang}.json`);
if (!(fs.isDirectorySync(i18nDir) && fs.existsSync(langfile))) return;
let contents = JSON.parse(fs.readFileSync(langfile));
return contents;
}
/**
* formats a string with opts,
* and caches the message formatter for the provided path.
*/
format(ns, _path, str, lang, opts) {
let path = [ns, lang, ..._path];
let cachedFormatter = optionalTravelDownObjectPath(this.cachedFormatters, path);
if (cachedFormatter !== undefined) return cachedFormatter.format(opts);
let ast = this.cacheHelper.fetchAST(ns, _path, str, lang);
let formatter = new IntlMessageFormat(ast, lang);
let last = path.pop();
let cachePath = travelDownOrMakePath(this.cachedFormatters, path);
cachePath[last] = formatter;
return formatter.format(opts);
}
}
module.exports = I18n;

View File

@ -106,9 +106,10 @@ module.exports = class ApplicationMenu {
// Replaces VERSION with the current version.
substituteVersion(template) {
let item = this.flattenMenuTemplate(template).find(
({ label }) => label === 'VERSION'
({ label }) => label?.includes("VERSION")
);
if (item) item.label = `Version ${this.version}`;
// TODO maybe this can be done with the i18n API instead of custom replace?
if (item) item.label = item.label.replace("VERSION", this.version);
}
// Sets the proper visible state the update menu items

View File

@ -12,8 +12,8 @@ function addItemToMenu(item, menu) {
}
}
function merge(menu, item, itemSpecificity = Infinity) {
item = cloneMenuItem(item);
function merge(menu, item, t, itemSpecificity = Infinity) {
item = cloneAndLocaliseMenuItem(item, t);
ItemSpecificities.set(item, itemSpecificity);
const matchingItemIndex = findMatchingItemIndex(menu, item);
@ -25,7 +25,7 @@ function merge(menu, item, itemSpecificity = Infinity) {
const matchingItem = menu[matchingItemIndex];
if (item.submenu != null) {
for (let submenuItem of item.submenu) {
merge(matchingItem.submenu, submenuItem, itemSpecificity);
merge(matchingItem.submenu, submenuItem, t, itemSpecificity);
}
} else if (
itemSpecificity &&
@ -35,8 +35,9 @@ function merge(menu, item, itemSpecificity = Infinity) {
}
}
function unmerge(menu, item) {
item = cloneMenuItem(item);
function unmerge(menu, item, t) {
item = cloneAndLocaliseMenuItem(item, t);
const matchingItemIndex = findMatchingItemIndex(menu, item);
if (matchingItemIndex === -1) {
return;
@ -74,11 +75,12 @@ function normalizeLabel(label) {
return process.platform === 'darwin' ? label : label.replace(/&/g, '');
}
function cloneMenuItem(item) {
function cloneAndLocaliseMenuItem(item, t) {
item = _.pick(
item,
'type',
'label',
'localisedLabel',
'id',
'enabled',
'visible',
@ -92,11 +94,18 @@ function cloneMenuItem(item) {
'beforeGroupContaining',
'afterGroupContaining'
);
if (item.localisedLabel) {
if (typeof item.localisedLabel === "string") {
item.label = t(item.localisedLabel);
} else {
item.label = t(item.localisedLabel.key, item.localisedLabel.opts);
}
}
if (item.id === null || item.id === undefined) {
item.id = normalizeLabel(item.label);
}
if (item.submenu != null) {
item.submenu = item.submenu.map(submenuItem => cloneMenuItem(submenuItem));
item.submenu = item.submenu.map(submenuItem => cloneAndLocaliseMenuItem(submenuItem, t));
}
return item;
}
@ -133,6 +142,6 @@ module.exports = {
merge,
unmerge,
normalizeLabel,
cloneMenuItem,
cloneAndLocaliseMenuItem,
acceleratorForKeystroke
};

View File

@ -60,8 +60,8 @@ if (buildMetadata) {
//
// See {::add} for more info about adding menu's directly.
module.exports = MenuManager = class MenuManager {
constructor({resourcePath, keymapManager, packageManager}) {
this.resourcePath = resourcePath;
constructor({ i18n, keymapManager, packageManager }) {
this.i18n = i18n;
this.keymapManager = keymapManager;
this.packageManager = packageManager;
this.initialized = false;
@ -101,9 +101,8 @@ module.exports = MenuManager = class MenuManager {
// added menu items.
add(items) {
items = _.deepClone(items);
for (let i = 0; i < items.length; i++) {
const item = items[i];
if (item.label == null) {
for (const item of items) {
if (item.label == null && item.localisedLabel == null) {
continue; // TODO: Should we emit a warning here?
}
this.merge(this.template, item);
@ -220,11 +219,11 @@ module.exports = MenuManager = class MenuManager {
// Merges an item in a submenu aware way such that new items are always
// appended to the bottom of existing menus where possible.
merge(menu, item) {
MenuHelpers.merge(menu, item);
MenuHelpers.merge(menu, item, this.i18n.t);
}
unmerge(menu, item) {
MenuHelpers.unmerge(menu, item);
MenuHelpers.unmerge(menu, item, this.i18n.t);
}
sendToBrowserProcess(template, keystrokesByCommand) {
@ -242,7 +241,8 @@ module.exports = MenuManager = class MenuManager {
}
sortPackagesMenu() {
const packagesMenu = _.find(this.template, ({id}) => MenuHelpers.normalizeLabel(id) === 'Packages');
let packagesLabel = this.i18n.t("core.menu.packages.self");
const packagesMenu = _.find(this.template, ({id}) => MenuHelpers.normalizeLabel(id) === packagesLabel);
if (!(packagesMenu && packagesMenu.submenu != null)) {
return;
}
@ -255,5 +255,4 @@ module.exports = MenuManager = class MenuManager {
});
return this.update();
}
};

View File

@ -38,7 +38,8 @@ module.exports = class PackageManager {
grammarRegistry: this.grammarRegistry,
deserializerManager: this.deserializerManager,
viewRegistry: this.viewRegistry,
uriHandlerRegistry: this.uriHandlerRegistry
uriHandlerRegistry: this.uriHandlerRegistry,
i18n: this.i18n
} = params);
this.emitter = new Emitter();
@ -587,7 +588,8 @@ module.exports = class PackageManager {
menuManager: this.menuManager,
contextMenuManager: this.contextMenuManager,
deserializerManager: this.deserializerManager,
viewRegistry: this.viewRegistry
viewRegistry: this.viewRegistry,
i18n: this.i18n
};
pack = metadata.theme ? new ThemePackage(options) : new Package(options);
@ -692,7 +694,8 @@ module.exports = class PackageManager {
menuManager: this.menuManager,
contextMenuManager: this.contextMenuManager,
deserializerManager: this.deserializerManager,
viewRegistry: this.viewRegistry
viewRegistry: this.viewRegistry,
i18n: this.i18n
};
const pack = metadata.theme

View File

@ -30,6 +30,7 @@ module.exports = class Package {
this.contextMenuManager = params.contextMenuManager;
this.deserializerManager = params.deserializerManager;
this.viewRegistry = params.viewRegistry;
this.i18n = params.i18n;
this.emitter = new Emitter();
this.mainModule = null;
@ -240,7 +241,10 @@ module.exports = class Package {
}
if (typeof this.mainModule.activate === 'function') {
this.mainModule.activate(
this.packageManager.getPackageState(this.name) || {}
this.packageManager.getPackageState(this.name) || {},
{
t: this.i18n.getT(this.name)
}
);
}
this.mainActivated = true;

View File

@ -13,7 +13,7 @@ const NullGrammar = require('./null-grammar');
const TextMateLanguageMode = require('./text-mate-language-mode');
const ScopeDescriptor = require('./scope-descriptor');
const TextMateScopeSelector = require('first-mate').ScopeSelector;
const TextMateScopeSelector = require('second-mate').ScopeSelector;
const GutterContainer = require('./gutter-container');
let TextEditorComponent = null;
let TextEditorElement = null;

View File

@ -5,7 +5,7 @@ const TokenizedLine = require('./tokenized-line');
const TokenIterator = require('./token-iterator');
const ScopeDescriptor = require('./scope-descriptor');
const NullGrammar = require('./null-grammar');
const { OnigRegExp } = require('oniguruma');
const { OnigScanner } = require('second-mate');
const {
toFirstMateScopeId,
fromFirstMateScopeId
@ -144,7 +144,7 @@ class TextMateLanguageMode {
);
if (!decreaseIndentRegex) return;
if (!decreaseIndentRegex.testSync(line)) return;
if (!decreaseIndentRegex.findNextMatchSync(line)) return;
const precedingRow = this.buffer.previousNonBlankRow(bufferRow);
if (precedingRow == null) return;
@ -156,14 +156,14 @@ class TextMateLanguageMode {
scopeDescriptor
);
if (increaseIndentRegex) {
if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1;
if (!increaseIndentRegex.findNextMatchSync(precedingLine)) desiredIndentLevel -= 1;
}
const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(
scopeDescriptor
);
if (decreaseNextIndentRegex) {
if (decreaseNextIndentRegex.testSync(precedingLine))
if (decreaseNextIndentRegex.findNextMatchSync(precedingLine))
desiredIndentLevel -= 1;
}
@ -203,17 +203,17 @@ class TextMateLanguageMode {
if (!increaseIndentRegex) return desiredIndentLevel;
if (!this.isRowCommented(precedingRow)) {
if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine))
if (increaseIndentRegex && increaseIndentRegex.findNextMatchSync(precedingLine))
desiredIndentLevel += 1;
if (
decreaseNextIndentRegex &&
decreaseNextIndentRegex.testSync(precedingLine)
decreaseNextIndentRegex.findNextMatchSync(precedingLine)
)
desiredIndentLevel -= 1;
}
if (!this.buffer.isRowBlank(precedingRow)) {
if (decreaseIndentRegex && decreaseIndentRegex.testSync(line))
if (decreaseIndentRegex && decreaseIndentRegex.findNextMatchSync(line))
desiredIndentLevel -= 1;
}
@ -812,7 +812,7 @@ class TextMateLanguageMode {
if (indentation < startIndentLevel) {
break;
} else if (indentation === startIndentLevel) {
if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow;
if (foldEndRegex && foldEndRegex.findNextMatchSync(line)) foldEndRow = nextRow;
break;
}
foldEndRow = nextRow;
@ -848,7 +848,7 @@ class TextMateLanguageMode {
regexForPattern(pattern) {
if (pattern) {
if (!this.regexesByPattern[pattern]) {
this.regexesByPattern[pattern] = new OnigRegExp(pattern);
this.regexesByPattern[pattern] = new OnigScanner([pattern]);
}
return this.regexesByPattern[pattern];
}

View File

@ -19,10 +19,11 @@
}
StartupTime.addMarker('window:start', startWindowTime);
window.onload = function() {
window.onload = async function() {
try {
StartupTime.addMarker('window:onload:start');
const startTime = Date.now();
await require('second-mate').ready
process.on('unhandledRejection', function(error, promise) {
console.error(

106
yarn.lock
View File

@ -1438,6 +1438,45 @@
minimatch "^3.1.2"
strip-json-comments "^3.1.1"
"@formatjs/ecma402-abstract@1.14.3":
version "1.14.3"
resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.14.3.tgz#6428f243538a11126180d121ce8d4b2f17465738"
integrity sha512-SlsbRC/RX+/zg4AApWIFNDdkLtFbkq3LNoZWXZCE/nHVKqoIJyaoQyge/I0Y38vLxowUn9KTtXgusLD91+orbg==
dependencies:
"@formatjs/intl-localematcher" "0.2.32"
tslib "^2.4.0"
"@formatjs/fast-memoize@2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@formatjs/fast-memoize/-/fast-memoize-2.0.1.tgz#f15aaa73caad5562899c69bdcad8db82adcd3b0b"
integrity sha512-M2GgV+qJn5WJQAYewz7q2Cdl6fobQa69S1AzSM2y0P68ZDbK5cWrJIcPCO395Of1ksftGZoOt4LYCO/j9BKBSA==
dependencies:
tslib "^2.4.0"
"@formatjs/icu-messageformat-parser@2.3.0", "@formatjs/icu-messageformat-parser@^2.3.0":
version "2.3.0"
resolved "https://registry.yarnpkg.com/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.3.0.tgz#8e8fd577c3e39454ef14bba4963f2e1d5f2cc46c"
integrity sha512-xqtlqYAbfJDF4b6e4O828LBNOWXrFcuYadqAbYORlDRwhyJ2bH+xpUBPldZbzRGUN2mxlZ4Ykhm7jvERtmI8NQ==
dependencies:
"@formatjs/ecma402-abstract" "1.14.3"
"@formatjs/icu-skeleton-parser" "1.3.18"
tslib "^2.4.0"
"@formatjs/icu-skeleton-parser@1.3.18":
version "1.3.18"
resolved "https://registry.yarnpkg.com/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.18.tgz#7aed3d60e718c8ad6b0e64820be44daa1e29eeeb"
integrity sha512-ND1ZkZfmLPcHjAH1sVpkpQxA+QYfOX3py3SjKWMUVGDow18gZ0WPqz3F+pJLYQMpS2LnnQ5zYR2jPVYTbRwMpg==
dependencies:
"@formatjs/ecma402-abstract" "1.14.3"
tslib "^2.4.0"
"@formatjs/intl-localematcher@0.2.32":
version "0.2.32"
resolved "https://registry.yarnpkg.com/@formatjs/intl-localematcher/-/intl-localematcher-0.2.32.tgz#00d4d307cd7d514b298e15a11a369b86c8933ec1"
integrity sha512-k/MEBstff4sttohyEpXxCmC3MqbUn9VvHGlZ8fauLzkbwXmVrEeyzS+4uhrvAk9DWU9/7otYWxyDox4nT/KVLQ==
dependencies:
tslib "^2.4.0"
"@gar/promisify@^1.0.1":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
@ -2659,11 +2698,9 @@ braces@~3.0.2:
dependencies:
fill-range "^7.0.1"
"bracket-matcher@https://github.com/pulsar-edit/bracket-matcher.git#c877977":
"bracket-matcher@file:packages/bracket-matcher":
version "0.92.0"
resolved "https://github.com/pulsar-edit/bracket-matcher.git#c877977ac7e9b7fe43c2100a1880c7ffc119280b"
dependencies:
first-mate "^7.4.1"
underscore-plus "1.x"
browser-stdout@1.3.1:
@ -3789,10 +3826,9 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
document-register-element@^1.14.10:
"document-register-element@https://github.com/pulsar-edit/document-register-element.git#1f5868f":
version "1.14.10"
resolved "https://registry.yarnpkg.com/document-register-element/-/document-register-element-1.14.10.tgz#a7bd025e6b73bd827fec2d8e90aba755e99387f5"
integrity sha512-w5UA37hEIrs+9pruo2yR5UD13c4UHDlkqqjt4qurnp7QsBI9b1IOi8WXUim+aCqKBsENX3Z/cso7XMOuwJH1Yw==
resolved "https://github.com/pulsar-edit/document-register-element.git#1f5868fba7b6c3a337c777a0396c43a080e1fad3"
dom-serializer@^2.0.0:
version "2.0.0"
@ -3991,7 +4027,7 @@ electron@12.2.3:
"@types/node" "^14.6.2"
extract-zip "^1.0.3"
emissary@^1, emissary@^1.0.0, emissary@^1.1.0, emissary@^1.2.0, emissary@^1.3.2:
emissary@^1.0.0, emissary@^1.1.0, emissary@^1.2.0, emissary@^1.3.2, emissary@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/emissary/-/emissary-1.3.3.tgz#a618d92d682b232d31111dc3625a5df661799606"
integrity sha512-pD6FWNBSlEOzSJDCTcSGVLgNnGw5fnCvvGMdQ/TN43efeXZ/QTq8+hZoK3OOEXPRNjMmSJmeOnEJh+bWT5O8rQ==
@ -4390,7 +4426,7 @@ etch@^0.12.2, etch@^0.12.6:
resolved "https://registry.yarnpkg.com/etch/-/etch-0.12.8.tgz#c24bc9bd3a6148f62204ce8643d2e899b9ecb9de"
integrity sha512-dFLRe4wLroVtwzyy1vGlE3BSDZHiL0kZME5XgNGzZIULcYTvVno8vbiIleAesoKJmwWaxDTzG+4eppg2zk14JQ==
event-kit@2.5.3, event-kit@^2.0.0, event-kit@^2.1.0, event-kit@^2.2.0, event-kit@^2.4.0, event-kit@^2.5.3:
event-kit@2.5.3, event-kit@^2.0.0, event-kit@^2.1.0, event-kit@^2.4.0, event-kit@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/event-kit/-/event-kit-2.5.3.tgz#d47e4bc116ec0aacd00263791fa1a55eb5e79ba1"
integrity sha512-b7Qi1JNzY4BfAYfnIRanLk0DOD1gdkWHT4GISIn8Q2tAf3LpU8SP2CMwWaq40imYoKWbtN4ZhbSRxvsnikooZQ==
@ -4605,19 +4641,6 @@ find-up@^5.0.0:
locate-path "^6.0.0"
path-exists "^4.0.0"
first-mate@7.4.3, first-mate@^7.4.1:
version "7.4.3"
resolved "https://registry.yarnpkg.com/first-mate/-/first-mate-7.4.3.tgz#058b9b6d2f43e38a5f0952669338cff2c46ae2dd"
integrity sha512-PtZUpaPmcV5KV4Rw5TfwczEnExN+X1o3Q/G82E4iRJ0tW91fm3Yi7pa5t4cBH8r3D6EyoBKvfpG2jKE+TZ0/nw==
dependencies:
emissary "^1"
event-kit "^2.2.0"
fs-plus "^3.0.0"
grim "^2.0.1"
oniguruma "^7.2.3"
season "^6.0.2"
underscore-plus "^1"
flat-cache@^3.0.4:
version "3.0.4"
resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11"
@ -5194,7 +5217,7 @@ graphql@14.5.8:
dependencies:
iterall "^1.2.2"
grim@2.0.3, grim@^2.0.1, grim@^2.0.2:
grim@2.0.3, grim@^2.0.1, grim@^2.0.2, grim@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/grim/-/grim-2.0.3.tgz#66e575efc4577981d959da0764926b4aaded4b0d"
integrity sha512-FM20Ump11qYLK9k9DbL8yzVpy+YBieya1JG15OeH8s+KbHq8kL4SdwRtURwIUHniSxb24EoBUpwKfFjGNVi4/Q==
@ -5534,6 +5557,16 @@ internal-slot@^1.0.3:
has "^1.0.3"
side-channel "^1.0.4"
intl-messageformat@^10.3.3:
version "10.3.3"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-10.3.3.tgz#576798d31c9f8d90f9beadaa5a3878b8d30177a2"
integrity sha512-un/f07/g2e/3Q8e1ghDKET+el22Bi49M7O/rHxd597R+oLpPOMykSv5s51cABVfu3FZW+fea4hrzf2MHu1W4hw==
dependencies:
"@formatjs/ecma402-abstract" "1.14.3"
"@formatjs/fast-memoize" "2.0.1"
"@formatjs/icu-messageformat-parser" "2.3.0"
tslib "^2.4.0"
invert-kv@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
@ -7422,13 +7455,6 @@ onetime@^5.1.0:
dependencies:
mimic-fn "^2.1.0"
oniguruma@^7.2.3:
version "7.2.3"
resolved "https://registry.yarnpkg.com/oniguruma/-/oniguruma-7.2.3.tgz#e0b0b415302de8cdd6564e57a1a822ac0ab57012"
integrity sha512-PZZcE0yfg8Q1IvaJImh21RUTHl8ep0zwwyoE912KqlWVrsGByjjj29sdACcD1BFyX2bLkfuOJeP+POzAGVWtbA==
dependencies:
nan "^2.14.0"
"open-on-github@file:packages/open-on-github":
version "1.3.2"
@ -8503,6 +8529,18 @@ season@^6.0.2:
fs-plus "^3.0.0"
yargs "^3.23.0"
"second-mate@https://github.com/pulsar-edit/second-mate.git#14aa7bd":
version "8.0.0"
resolved "https://github.com/pulsar-edit/second-mate.git#14aa7bd94b90c47aa99f000394301b9573b8898b"
dependencies:
emissary "^1.3.3"
event-kit "^2.5.3"
fs-plus "^3.0.0"
grim "^2.0.3"
season "^6.0.2"
underscore-plus "^1"
vscode-oniguruma "^1.7.0"
selector-kit@^0.1:
version "0.1.0"
resolved "https://registry.yarnpkg.com/selector-kit/-/selector-kit-0.1.0.tgz#304338fceccea35ec28ffaddb792ab7715633e6f"
@ -9517,6 +9555,11 @@ truncate-utf8-bytes@^1.0.0:
dependencies:
utf8-byte-length "^1.0.1"
tslib@^2.4.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf"
integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
@ -9810,6 +9853,11 @@ verror@^1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
vscode-oniguruma@^1.7.0:
version "1.7.0"
resolved "https://registry.yarnpkg.com/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz#439bfad8fe71abd7798338d1cd3dc53a8beea94b"
integrity sha512-L9WMGRfrjOhgHSdOYgCt/yRMsXzLDJSL7BPrOZt73gU0iWO4mpqzqQzOz5srxqTvMBaR0XZTSrVWo4j55Rc6cA==
vscode-ripgrep@1.9.0:
version "1.9.0"
resolved "https://registry.yarnpkg.com/vscode-ripgrep/-/vscode-ripgrep-1.9.0.tgz#d6cdea4d290f3c2919472cdcfe2440d5fb1f99db"