Merge remote-tracking branch 'origin/master' into fix-macos-arm64

This commit is contained in:
Maurício Szabo 2023-04-06 13:41:48 -03:00
commit 917dec4503
45 changed files with 40403 additions and 1992 deletions

View File

@ -10,6 +10,7 @@ linux_task:
- apt-get update
- export DEBIAN_FRONTEND="noninteractive"
- apt-get install -y
ffmpeg
rpm
build-essential
git
@ -27,7 +28,7 @@ linux_task:
- git submodule update
- sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
install_script:
- yarn install || yarn install
- yarn install --ignore-engines || yarn install --ignore-engines
build_script:
- yarn build
- yarn run build:apm
@ -36,7 +37,11 @@ linux_task:
binary_artifacts:
path: ./binaries/*
test_script:
- Xvfb :99 & DISPLAY=:99 PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
- rm -R node_modules/electron; yarn install --check-files
- ./binaries/*AppImage --appimage-extract
- export BINARY_NAME='squashfs-root/pulsar'
- mkdir -p ./tests/videos
- Xvfb -screen 0 1024x768x24+32 :99 & nohup ffmpeg -video_size 1024x768 -f x11grab -i :99.0 ./tests/videos/out.mpg & DISPLAY=:99 PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
always:
videos_artifacts:
path: ./tests/videos/**
@ -81,17 +86,19 @@ arm_linux_task:
- git submodule update
- sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
install_script:
- yarn install || yarn install
- yarn install --ignore-engines || yarn install --ignore-engines
build_script:
- yarn build
- yarn run build:apm
- rm -Rf node-modules/electron && yarn install --check-files
build_binary_script:
- source /etc/profile.d/rvm.sh
- yarn dist || yarn dist
binary_artifacts:
path: ./binaries/*
test_script:
- rm -R node_modules/electron; yarn install --check-files
- ./binaries/*AppImage --appimage-extract
- export BINARY_NAME='squashfs-root/pulsar'
- Xvfb :99 & DISPLAY=:99 PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
always:
videos_artifacts:
@ -121,7 +128,7 @@ silicon_mac_task:
- sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
install_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- yarn install || yarn install
- yarn install --ignore-engines || yarn install --ignore-engines
build_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- yarn build
@ -133,6 +140,9 @@ silicon_mac_task:
path: ./binaries/*
test_script:
- export PATH="/opt/homebrew/bin:/opt/homebrew/opt/node@16/bin:$PATH"
- rm -R node_modules/electron; yarn install --check-files
- hdiutil mount binaries/Pulsar*dmg
- export BINARY_NAME=`ls /Volumes/Pulsar*/Pulsar.app/Contents/MacOS/Pulsar`
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list
always:
videos_artifacts:
@ -166,7 +176,7 @@ intel_mac_task:
- sed -i -e "s/[0-9]*-dev/`date -u +%Y%m%d%H`/g" package.json
install_script:
- export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
- arch -x86_64 npx yarn install || arch -x86_64 npx yarn install
- arch -x86_64 npx yarn install --ignore-engines || arch -x86_64 npx yarn install --ignore-engines
build_script:
- export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
- arch -x86_64 npx yarn build
@ -178,6 +188,9 @@ intel_mac_task:
path: ./binaries/*
test_script:
- export PATH="/usr/local/opt/node@16/bin:/usr/local/bin:$PATH"
- rm -R node_modules/electron; yarn install --check-files
- hdiutil mount binaries/Pulsar*dmg
- export BINARY_NAME=`ls /Volumes/Pulsar*/Pulsar.app/Contents/MacOS/Pulsar`
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml arch -x86_64 npx playwright test --reporter=junit,list
always:
videos_artifacts:
@ -189,20 +202,23 @@ intel_mac_task:
windows_task:
alias: windows
timeout_in: 90m
windows_container:
image: cirrusci/windowsservercore:visualstudio2022-2022.06.23
env:
CIRRUS_SHELL: bash
PATH: C:\Python310\Scripts\;C:\Python310\;%PATH%;C:\Program Files\nodejs\;C:\Program Files\Git\cmd;C:\Users\User\AppData\Local\Microsoft\WindowsApps;C:\Users\User\AppData\Roaming\npm;C:\Program Files\Microsoft Visual Studio\2022\Community\Msbuild\Current\Bin\
prepare_script:
- choco install nodejs --version=14.15.0 -y
- choco install nodejs --version=16.16.0 -y
- choco install python --version=3.10.3 -y
- choco install git visualstudio2019-workload-vctools -y
- git submodule init
- git submodule update
- npm config set python 'C:\Python310\python.exe'
install_script:
- npx yarn install --ignore-engines || sleep 1 && npx yarn install --ignore-engines || echo "There is a reason for so many tries"
- npx yarn install --ignore-engines
|| rm -R node_modules && npx yarn install --ignore-engines
|| rm -R node_modules && npx yarn install --ignore-engines
build_script:
- npx yarn build:apm
- npx yarn build || npx yarn build || npx yarn build
@ -211,3 +227,14 @@ windows_task:
- npx yarn dist || npx yarn dist || npx yarn dist
binary_artifacts:
path: .\binaries\*
test_script:
- mkdir extracted; tar -xf binaries/*zip -C ./extracted/
- export BINARY_NAME=./extracted/Pulsar.exe
- PLAYWRIGHT_JUNIT_OUTPUT_NAME=report.xml npx playwright test --reporter=junit,list || echo "Yeah, tests failed, Windows is like this"
always:
videos_artifacts:
path: ./tests/videos/**
junit_artifacts:
path: report.xml
type: text/xml
format: junit

View File

@ -6,6 +6,7 @@
## [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

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

@ -14,6 +14,11 @@ async function openAtom(profilePath, videoName) {
env: env,
timeout: 50000
}
if(env.BINARY_NAME) {
config.executablePath = env.BINARY_NAME
config.args = ["--no-sandbox"]
}
if(process.env.CI) {
config.recordVideo = {
dir: path.join('tests', 'videos', videoName)

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

@ -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",
@ -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

@ -51,8 +51,12 @@ export default class PackageSnippetsView {
<input id='toggleSnippets' className='input-checkbox' type='checkbox' ref='snippetToggle' />
<div className='setting-title'>Enable</div>
</label>
<div className='setting-description'>
{'Disable this if you want to prevent this packages snippets from appearing as suggestions or if you want to customize them in your snippets file.'}
<div className='setting-description' ref='snippetSettingDescription'>
<p>Disable this if you want to prevent this packages snippets from appearing as suggestions or if you want to customize them in your snippets file.</p>
<p>To <strong>disable</strong> most snippets and <strong>enable</strong> just a few, use the <kbd>Copy</kbd> button on any snippet you want to enable, then paste the result into your own snippets file.</p>
<p>To <strong>enable</strong> most snippets and <strong>disable</strong> just a few, use the <kbd>Copy</kbd> button on any snippet you want to disable, paste the result into your own snippets file, and change the body to <code>null</code>.</p>
</div>
</div>
@ -60,6 +64,7 @@ export default class PackageSnippetsView {
<thead>
<tr>
<th>Trigger</th>
<th ref="headingCommand">Command</th>
<th>Name</th>
<th>Scope</th>
<th>Body</th>
@ -73,13 +78,12 @@ export default class PackageSnippetsView {
getSnippetProperties () {
const packageProperties = {}
for (const {name, properties, selectorString} of this.snippetsProvider.getSnippets()) {
for (const {name, properties} of this.snippetsProvider.getSnippets()) {
if (name && name.indexOf && name.indexOf(this.packagePath) === 0) {
const object = properties.snippets != null ? properties.snippets : {}
for (let key in object) {
const snippet = object[key]
if (snippet != null) {
snippet.selectorString = selectorString
if (packageProperties[key] == null) {
packageProperties[key] = snippet
}
@ -116,13 +120,15 @@ export default class PackageSnippetsView {
this.getSnippets((snippets) => {
this.refs.snippets.innerHTML = ''
let anyWithCommand = snippets.some(s => ('command' in s))
if (snippetsDisabled) {
this.refs.snippets.classList.add('text-subtle')
} else {
this.refs.snippets.classList.remove('text-subtle')
}
for (let {body, bodyText, name, prefix, selectorString} of snippets) {
for (let {body, bodyText, command, name, packageName, prefix, selector} of snippets) {
if (name == null) {
name = ''
}
@ -135,8 +141,13 @@ export default class PackageSnippetsView {
body = bodyText || ''
}
if (selectorString == null) {
selectorString = ''
if (selector == null) {
selector = ''
}
let commandName = ''
if (packageName && command) {
commandName = `${packageName}:${command}`
}
const row = document.createElement('tr')
@ -146,13 +157,18 @@ export default class PackageSnippetsView {
prefixTd.textContent = prefix
row.appendChild(prefixTd)
const commandTd = document.createElement('td')
commandTd.textContent = commandName
row.appendChild(commandTd)
commandTd.style.display = anyWithCommand ? '' : 'none'
const nameTd = document.createElement('td')
nameTd.textContent = name
row.appendChild(nameTd)
const scopeTd = document.createElement('td')
scopeTd.classList.add('snippet-scope-name')
scopeTd.textContent = selectorString
scopeTd.textContent = selector
row.appendChild(scopeTd)
const bodyTd = document.createElement('td')
@ -160,7 +176,7 @@ export default class PackageSnippetsView {
row.appendChild(bodyTd)
this.refs.snippets.appendChild(row)
this.createButtonsForSnippetRow(bodyTd, {body, prefix, scope: selectorString, name})
this.createButtonsForSnippetRow(bodyTd, {body, prefix, scope: selector, name, command})
}
if (this.refs.snippets.children.length > 0) {
@ -168,10 +184,14 @@ export default class PackageSnippetsView {
} else {
this.element.style.display = 'none'
}
// The “Command” column should only be shown if at least one snippet is
// mapped to a command name.
this.refs.headingCommand.style.display = anyWithCommand ? '' : 'none'
})
}
createButtonsForSnippetRow (td, {scope, body, name, prefix}) {
createButtonsForSnippetRow (td, {scope, body, name, prefix, command}) {
let buttonContainer = document.createElement('div')
buttonContainer.classList.add('btn-group', 'btn-group-xs')
@ -198,7 +218,7 @@ export default class PackageSnippetsView {
copyButton.addEventListener('click', (event) => {
event.preventDefault()
return this.writeSnippetToClipboard({scope, body, name, prefix})
return this.writeSnippetToClipboard({scope, body, name, prefix, command})
})
buttonContainer.appendChild(viewButton)
@ -207,24 +227,39 @@ export default class PackageSnippetsView {
td.appendChild(buttonContainer)
}
writeSnippetToClipboard ({scope, body, name, prefix}) {
writeSnippetToClipboard ({scope, body, name, prefix, command}) {
let content
const extension = path.extname(this.snippetsProvider.getUserSnippetsPath())
body = body.replace(/\n/g, '\\n').replace(/\t/g, '\\t')
// Either `prefix` or `command` will be present, or else both. Only copy
// the values that are present.
let triggers = []
if (extension === '.cson') {
if (prefix) {
triggers.push(` 'prefix': '${prefix}'`)
}
if (command) {
triggers.push(` 'command': '${command}'`)
}
body = body.replace(/'/g, `\\'`)
content = `
'${scope}':
'${name}':
'prefix': '${prefix}'
${triggers.join('\n')}
'body': '${body}'
`
} else {
if (prefix) {
triggers.push(` "prefix": "${prefix}"`)
}
if (command) {
triggers.push(` "command": "${command}"`)
}
body = body.replace(/"/g, `\\"`)
content = `
"${scope}": {
"${name}": {
"prefix": "${prefix}",
${triggers.join(',\n')}
"body": "${body}"
}
}

View File

@ -0,0 +1,14 @@
module.exports = {
env: {
es2021: true,
jasmine: true,
node: true
},
globals: {
waitsForPromise: true
},
rules: {
"node/no-missing-require": "off",
"semi": ["error", "always"]
}
};

View File

@ -2,6 +2,7 @@
".source.b": {
"BAR": {
"prefix": "b",
"command": "sample-command",
"body": "bar?\nline two"
}
}

View File

@ -1,5 +1,5 @@
{
".source.a": {
".source.a, .source.aa": {
"FOO": {
"prefix": "f",
"body": "foo!"

View File

@ -1,338 +0,0 @@
path = require 'path'
PackageDetailView = require '../lib/package-detail-view'
PackageManager = require '../lib/package-manager'
SettingsView = require '../lib/settings-view'
PackageKeymapView = require '../lib/package-keymap-view'
PackageSnippetsView = require '../lib/package-snippets-view'
_ = require 'underscore-plus'
SnippetsProvider =
getSnippets: -> atom.config.scopedSettingsStore.propertySets
describe "InstalledPackageView", ->
beforeEach ->
spyOn(PackageManager.prototype, 'loadCompatiblePackageVersion').andCallFake ->
it "displays the grammars registered by the package", ->
settingsPanels = null
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
runs ->
pack = atom.packages.getActivePackage('language-test')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
settingsPanels = view.element.querySelectorAll('.package-grammars .settings-panel')
waitsFor ->
children = Array.from(settingsPanels).map((s) -> s.children.length)
childrenCount = children.reduce(((a, b) -> a + b), 0)
childrenCount is 2
expect(settingsPanels[0].querySelector('.grammar-scope').textContent).toBe 'Scope: source.a'
expect(settingsPanels[0].querySelector('.grammar-filetypes').textContent).toBe 'File Types: .a, .aa, a'
expect(settingsPanels[1].querySelector('.grammar-scope').textContent).toBe 'Scope: source.b'
expect(settingsPanels[1].querySelector('.grammar-filetypes').textContent).toBe 'File Types: '
expect(settingsPanels[2]).toBeUndefined()
it "displays the snippets registered by the package", ->
snippetsTable = null
snippetsModule = null
# Relies on behavior not present in the snippets package before 1.33.
# TODO: These tests should always run once 1.33 is released.
shouldRunScopeTest = parseFloat(atom.getVersion()) >= 1.33
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
waitsForPromise ->
atom.packages.activatePackage('snippets').then (p) ->
snippetsModule = p.mainModule
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
SnippetsProvider =
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
runs ->
pack = atom.packages.getActivePackage('language-test')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
snippetsTable = view.element.querySelector('.package-snippets-table tbody')
waitsFor 'snippets table children to contain 2 items', ->
snippetsTable.children.length >= 2
runs ->
expect(snippetsTable.querySelector('tr:nth-child(1) td:nth-child(1)').textContent).toBe 'b'
expect(snippetsTable.querySelector('tr:nth-child(1) td:nth-child(2)').textContent).toBe 'BAR'
expect(snippetsTable.querySelector('tr:nth-child(1) td.snippet-scope-name').textContent).toBe '.b.source' if shouldRunScopeTest
expect(snippetsTable.querySelector('tr:nth-child(2) td:nth-child(1)').textContent).toBe 'f'
expect(snippetsTable.querySelector('tr:nth-child(2) td:nth-child(2)').textContent).toBe 'FOO'
expect(snippetsTable.querySelector('tr:nth-child(2) td.snippet-scope-name').textContent).toBe '.a.source' if shouldRunScopeTest
describe "when a snippet body is viewed", ->
it "shows a tooltip", ->
tooltipCalls = []
view = null
snippetsTable = null
snippetsModule = null
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
waitsForPromise ->
atom.packages.activatePackage('snippets').then (p) ->
snippetsModule = p.mainModule
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
SnippetsProvider =
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
runs ->
pack = atom.packages.getActivePackage('language-test')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
snippetsTable = view.element.querySelector('.package-snippets-table tbody')
waitsFor 'snippets table children to contain 2 items', ->
snippetsTable.children.length >= 2
runs ->
expect(view.element.ownerDocument.querySelector('.snippet-body-tooltip')).not.toExist()
view.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-view-btn').click()
expect(view.element.ownerDocument.querySelector('.snippet-body-tooltip')).toExist()
# Relies on behavior not present in the snippets package before 1.33.
# TODO: These tests should always run once 1.33 is released.
if parseFloat(atom.getVersion()) >= 1.33
describe "when a snippet is copied", ->
[pack, card] = []
snippetsTable = null
snippetsModule = null
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
waitsForPromise ->
atom.packages.activatePackage('snippets').then (p) ->
snippetsModule = p.mainModule
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
SnippetsProvider =
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
getUserSnippetsPath: snippetsModule.getUserSnippetsPath()
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
runs ->
pack = atom.packages.getActivePackage('language-test')
card = new PackageSnippetsView(pack, SnippetsProvider)
snippetsTable = card.element.querySelector('.package-snippets-table tbody')
waitsFor 'snippets table children to contain 2 items', ->
snippetsTable.children.length >= 2
describe "when the snippets file ends in .cson", ->
it "writes a CSON snippet to the clipboard", ->
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.cson')
card.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-copy-btn').click()
expect(atom.clipboard.read()).toBe """
\n'.b.source':
'BAR':
'prefix': 'b'
'body': 'bar?\\nline two'\n
"""
describe "when the snippets file ends in .json", ->
it "writes a JSON snippet to the clipboard", ->
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.json')
card.element.querySelector('.package-snippets-table tbody tr:nth-child(1) td.snippet-body .btn:nth-child(2)').click()
expect(atom.clipboard.read()).toBe """
\n ".b.source": {
"BAR": {
"prefix": "b",
"body": "bar?\\nline two"
}
}\n
"""
describe "when the snippets toggle is clicked", ->
it "sets the packagesWithSnippetsDisabled config to include the package name", ->
[pack, card] = []
snippetsModule = []
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
waitsForPromise ->
atom.packages.activatePackage('snippets').then (p) ->
snippetsModule = p.mainModule
return unless snippetsModule.provideSnippets().getUnparsedSnippets?
SnippetsProvider =
getSnippets: -> snippetsModule.provideSnippets().getUnparsedSnippets()
waitsFor 'snippets to load', -> snippetsModule.provideSnippets().bundledSnippetsLoaded()
runs ->
pack = atom.packages.getActivePackage('language-test')
card = new PackageSnippetsView(pack, SnippetsProvider)
jasmine.attachToDOM(card.element)
card.refs.snippetToggle.click()
expect(card.refs.snippetToggle.checked).toBe false
expect(_.include(atom.config.get('core.packagesWithSnippetsDisabled') ? [], 'language-test')).toBe true
waitsFor 'snippets table to update', ->
card.refs.snippets.classList.contains('text-subtle')
runs ->
card.refs.snippetToggle.click()
expect(card.refs.snippetToggle.checked).toBe true
expect(_.include(atom.config.get('core.packagesWithSnippetsDisabled') ? [], 'language-test')).toBe false
waitsFor 'snippets table to update', ->
not card.refs.snippets.classList.contains('text-subtle')
it "does not display keybindings from other platforms", ->
keybindingsTable = null
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
runs ->
pack = atom.packages.getActivePackage('language-test')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
keybindingsTable = view.element.querySelector('.package-keymap-table tbody')
expect(keybindingsTable.children.length).toBe 1
describe "when the keybindings toggle is clicked", ->
it "sets the packagesWithKeymapsDisabled config to include the package name", ->
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
runs ->
pack = atom.packages.getActivePackage('language-test')
card = new PackageKeymapView(pack)
jasmine.attachToDOM(card.element)
card.refs.keybindingToggle.click()
expect(card.refs.keybindingToggle.checked).toBe false
expect(_.include(atom.config.get('core.packagesWithKeymapsDisabled') ? [], 'language-test')).toBe true
if atom.keymaps.build?
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody.text-subtle tr')
expect(keybindingRows.length).toBe 1
card.refs.keybindingToggle.click()
expect(card.refs.keybindingToggle.checked).toBe true
expect(_.include(atom.config.get('core.packagesWithKeymapsDisabled') ? [], 'language-test')).toBe false
if atom.keymaps.build?
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody tr')
expect(keybindingRows.length).toBe 1
describe "when a keybinding is copied", ->
[pack, card] = []
beforeEach ->
waitsForPromise ->
atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'))
runs ->
pack = atom.packages.getActivePackage('language-test')
card = new PackageKeymapView(pack)
describe "when the keybinding file ends in .cson", ->
it "writes a CSON snippet to the clipboard", ->
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.cson'
card.element.querySelector('.copy-icon').click()
expect(atom.clipboard.read()).toBe """
'test':
'cmd-g': 'language-test:run'
"""
describe "when the keybinding file ends in .json", ->
it "writes a JSON snippet to the clipboard", ->
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn 'keymap.json'
card.element.querySelector('.copy-icon').click()
expect(atom.clipboard.read()).toBe """
"test": {
"cmd-g": "language-test:run"
}
"""
describe "when the package is active", ->
it "displays the correct enablement state", ->
packageCard = null
waitsForPromise ->
atom.packages.activatePackage('status-bar')
runs ->
expect(atom.packages.isPackageActive('status-bar')).toBe(true)
pack = atom.packages.getLoadedPackage('status-bar')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
packageCard = view.element.querySelector('.package-card')
runs ->
# Trigger observeDisabledPackages() here
# because it is not default in specs
atom.packages.observeDisabledPackages()
atom.packages.disablePackage('status-bar')
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true)
expect(packageCard.classList.contains('disabled')).toBe(true)
describe "when the package is not active", ->
it "displays the correct enablement state", ->
atom.packages.loadPackage('status-bar')
expect(atom.packages.isPackageActive('status-bar')).toBe(false)
pack = atom.packages.getLoadedPackage('status-bar')
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
packageCard = view.element.querySelector('.package-card')
# Trigger observeDisabledPackages() here
# because it is not default in specs
atom.packages.observeDisabledPackages()
atom.packages.disablePackage('status-bar')
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true)
expect(packageCard.classList.contains('disabled')).toBe(true)
it "still loads the config schema for the package", ->
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'))
waitsFor ->
atom.packages.isPackageLoaded('package-with-config') is true
runs ->
expect(atom.config.get('package-with-config.setting')).toBe undefined
pack = atom.packages.getLoadedPackage('package-with-config')
new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
expect(atom.config.get('package-with-config.setting')).toBe 'something'
describe "when the package was not installed from atom.io", ->
normalizePackageDataReadmeError = 'ERROR: No README data found!'
it "still displays the Readme", ->
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-readme'))
waitsFor ->
atom.packages.isPackageLoaded('package-with-readme') is true
runs ->
pack = atom.packages.getLoadedPackage('package-with-readme')
expect(pack.metadata.readme).toBe normalizePackageDataReadmeError
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider)
expect(view.refs.sections.querySelector('.package-readme').textContent).not.toBe normalizePackageDataReadmeError
expect(view.refs.sections.querySelector('.package-readme').textContent.trim()).toContain 'I am a Readme!'

View File

@ -0,0 +1,517 @@
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const path = require('path');
const PackageDetailView = require('../lib/package-detail-view');
const PackageManager = require('../lib/package-manager');
const SettingsView = require('../lib/settings-view');
const PackageKeymapView = require('../lib/package-keymap-view');
const PackageSnippetsView = require('../lib/package-snippets-view');
const _ = require('underscore-plus');
let SnippetsProvider = {
getSnippets() {
return atom.config.scopedSettingsStore.propertySets;
}
};
describe("InstalledPackageView", function() {
beforeEach(() => {
spyOn(PackageManager.prototype, 'loadCompatiblePackageVersion')
.andCallFake(() => {});
});
it("displays the grammars registered by the package", () => {
let settingsPanels = null;
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
runs(() => {
const pack = atom.packages.getActivePackage('language-test');
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
settingsPanels = view.element.querySelectorAll('.package-grammars .settings-panel');
waitsFor(() => {
const children = Array.from(settingsPanels).map(s => s.children.length);
const childrenCount = children.reduce(((a, b) => a + b), 0);
return childrenCount === 2;
});
expect(
settingsPanels[0].querySelector('.grammar-scope').textContent
).toBe('Scope: source.a');
expect(
settingsPanels[0].querySelector('.grammar-filetypes'
).textContent).toBe('File Types: .a, .aa, a');
expect(
settingsPanels[1].querySelector('.grammar-scope').textContent
).toBe('Scope: source.b');
expect(
settingsPanels[1].querySelector('.grammar-filetypes').textContent
).toBe('File Types: ');
expect(settingsPanels[2]).toBeUndefined();
});
});
it("displays the snippets registered by the package", () => {
let snippetsTable = null;
let snippetsModule = null;
// Relies on behavior not present in the snippets package before 1.103.
const shouldRunScopeTest = parseFloat(atom.getVersion()) >= 1.103;
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
waitsForPromise(() => {
return atom.packages.activatePackage('snippets').then((p) => {
snippetsModule = p.mainModule;
if (snippetsModule.provideSnippets().getUnparsedSnippets == null) {
return;
}
SnippetsProvider = {
getSnippets() {
return snippetsModule.provideSnippets().getUnparsedSnippets();
}
};
});
});
waitsFor('snippets to load', () => {
return snippetsModule.provideSnippets().bundledSnippetsLoaded();
});
runs(() => {
const pack = atom.packages.getActivePackage('language-test');
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
snippetsTable = view.element.querySelector('.package-snippets-table tbody');
});
waitsFor('snippets table children to contain 2 items', () => {
return snippetsTable.children.length >= 2;
});
runs(() => {
expect(
snippetsTable.querySelector('tr:nth-child(1) td:nth-child(1)').textContent
).toBe('b');
if (shouldRunScopeTest) {
expect(
snippetsTable.querySelector('tr:nth-child(1) td:nth-child(2)').textContent
).toBe('language-test:sample-command');
}
expect(
snippetsTable.querySelector('tr:nth-child(1) td:nth-child(3)'
).textContent).toBe('BAR');
if (shouldRunScopeTest) {
expect(
snippetsTable.querySelector('tr:nth-child(1) td.snippet-scope-name').textContent
).toBe('.source.b');
}
expect(
snippetsTable.querySelector('tr:nth-child(2) td:nth-child(1)').textContent
).toBe('f');
if (shouldRunScopeTest) {
expect(
snippetsTable.querySelector('tr:nth-child(1) td:nth-child(2)').textContent
).toBe('');
}
expect(
snippetsTable.querySelector('tr:nth-child(2) td:nth-child(3)').textContent
).toBe('FOO');
if (shouldRunScopeTest) {
expect(
snippetsTable.querySelector('tr:nth-child(2) td.snippet-scope-name').textContent
).toBe('.source.a, .source.aa');
}
});
});
describe("when a snippet body is viewed", () =>
it("shows a tooltip", () => {
let view = null;
let snippetsTable = null;
let snippetsModule = null;
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
waitsForPromise(() =>
atom.packages.activatePackage('snippets').then((p) => {
snippetsModule = p.mainModule;
if (snippetsModule.provideSnippets().getUnparsedSnippets == null) {
return;
}
SnippetsProvider = {
getSnippets() {
return snippetsModule.provideSnippets().getUnparsedSnippets();
}
};
})
);
waitsFor('snippets to load', () => {
return snippetsModule.provideSnippets().bundledSnippetsLoaded();
});
runs(() => {
const pack = atom.packages.getActivePackage('language-test');
view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
snippetsTable = view.element.querySelector('.package-snippets-table tbody');
});
waitsFor('snippets table children to contain 2 items', () => snippetsTable.children.length >= 2);
runs(() => {
expect(
view.element.ownerDocument.querySelector('.snippet-body-tooltip')
).not.toExist();
view.element.querySelector(
'.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-view-btn'
).click();
expect(
view.element.ownerDocument.querySelector('.snippet-body-tooltip')
).toExist();
});
})
);
// Relies on behavior not present in the snippets package before 1.33.
// TODO: These tests should always run once 1.33 is released.
if (parseFloat(atom.getVersion()) >= 1.33) {
describe("when a snippet is copied", () => {
let pack, card;
let snippetsTable = null;
let snippetsModule = null;
beforeEach(() => {
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
waitsForPromise(() => {
return atom.packages.activatePackage('snippets').then((p) => {
snippetsModule = p.mainModule;
if (snippetsModule.provideSnippets().getUnparsedSnippets == null) {
return;
}
SnippetsProvider = {
getSnippets() {
return snippetsModule.provideSnippets().getUnparsedSnippets();
},
getUserSnippetsPath: () => snippetsModule.getUserSnippetsPath()
};
});
});
waitsFor('snippets to load', () => {
return snippetsModule.provideSnippets().bundledSnippetsLoaded();
});
runs(() => {
pack = atom.packages.getActivePackage('language-test');
card = new PackageSnippetsView(pack, SnippetsProvider);
snippetsTable = card.element.querySelector('.package-snippets-table tbody');
});
waitsFor('snippets table children to contain 2 items', () => snippetsTable.children.length >= 2);
});
describe("when the snippets file ends in .cson", () =>
it("writes a CSON snippet to the clipboard", () => {
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.cson');
card.element.querySelector(
'.package-snippets-table tbody tr:nth-child(1) td.snippet-body .snippet-copy-btn'
).click();
expect(atom.clipboard.read()).toBe(`\
\n'.b.source':
'BAR':
'prefix': 'b'
'body': 'bar?\\nline two'\n\
`
);
})
);
describe("when the snippets file ends in .json", () =>
it("writes a JSON snippet to the clipboard", () => {
spyOn(SnippetsProvider, 'getUserSnippetsPath').andReturn('snippets.json');
card.element.querySelector(
'.package-snippets-table tbody tr:nth-child(1) td.snippet-body .btn:nth-child(2)'
).click();
expect(atom.clipboard.read()).toBe(`\
\n ".b.source": {
"BAR": {
"prefix": "b",
"body": "bar?\\nline two"
}
}\n\
`
);
})
);
});
}
describe("when the snippets toggle is clicked", () =>
it("sets the packagesWithSnippetsDisabled config to include the package name", function() {
let pack, card;
let snippetsModule = null;
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
waitsForPromise(() =>
atom.packages.activatePackage('snippets').then((p) => {
snippetsModule = p.mainModule;
if (snippetsModule.provideSnippets().getUnparsedSnippets == null) {
return;
}
return SnippetsProvider = {
getSnippets() {
return snippetsModule.provideSnippets().getUnparsedSnippets();
}
};
})
);
waitsFor('snippets to load', () => {
return snippetsModule.provideSnippets().bundledSnippetsLoaded();
});
runs(() => {
pack = atom.packages.getActivePackage('language-test');
card = new PackageSnippetsView(pack, SnippetsProvider);
jasmine.attachToDOM(card.element);
card.refs.snippetToggle.click();
expect(card.refs.snippetToggle.checked).toBe(false);
let disabledSnippetsPackages = atom.config.get('core.packagesWithSnippetsDisabled') || [];
expect(
_.include(disabledSnippetsPackages, 'language-test')
).toBe(true);
});
waitsFor('snippets table to update', () => {
return card.refs.snippets.classList.contains('text-subtle');
});
runs(() => {
card.refs.snippetToggle.click();
expect(card.refs.snippetToggle.checked).toBe(true);
let disabledSnippetsPackages = atom.config.get('core.packagesWithSnippetsDisabled') || [];
expect(
_.include(disabledSnippetsPackages, 'language-test')
).toBe(false);
});
waitsFor('snippets table to update', () => {
return !card.refs.snippets.classList.contains('text-subtle');
});
})
);
it("does not display keybindings from other platforms", () => {
let keybindingsTable = null;
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
runs(() => {
const pack = atom.packages.getActivePackage('language-test');
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
keybindingsTable = view.element.querySelector('.package-keymap-table tbody');
expect(keybindingsTable.children.length).toBe(1);
});
});
describe("when the keybindings toggle is clicked", () =>
it("sets the packagesWithKeymapsDisabled config to include the package name", () => {
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
runs(() => {
let keybindingRows;
const pack = atom.packages.getActivePackage('language-test');
const card = new PackageKeymapView(pack);
jasmine.attachToDOM(card.element);
card.refs.keybindingToggle.click();
expect(card.refs.keybindingToggle.checked).toBe(false);
let disabledKeymapsPackages = atom.config.get('core.packagesWithKeymapsDisabled') || [];
expect(
_.include(disabledKeymapsPackages, 'language-test')
).toBe(true);
if (atom.keymaps.build) {
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody.text-subtle tr');
expect(keybindingRows.length).toBe(1);
}
card.refs.keybindingToggle.click();
expect(card.refs.keybindingToggle.checked).toBe(true);
disabledKeymapsPackages = atom.config.get('core.packagesWithKeymapsDisabled') || [];
expect(
_.include(disabledKeymapsPackages, 'language-test')
).toBe(false);
if (atom.keymaps.build) {
keybindingRows = card.element.querySelectorAll('.package-keymap-table tbody tr');
expect(keybindingRows.length).toBe(1);
}
});
})
);
describe("when a keybinding is copied", () => {
let [pack, card] = Array.from([]);
beforeEach(() => {
waitsForPromise(() => {
return atom.packages.activatePackage(path.join(__dirname, 'fixtures', 'language-test'));
});
runs(() => {
pack = atom.packages.getActivePackage('language-test');
card = new PackageKeymapView(pack);
});
});
describe("when the keybinding file ends in .cson", () =>
it("writes a CSON snippet to the clipboard", () => {
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn('keymap.cson');
card.element.querySelector('.copy-icon').click();
expect(atom.clipboard.read()).toBe(`\
'test':
'cmd-g': 'language-test:run'\
`
);
})
);
describe("when the keybinding file ends in .json", () => {
it("writes a JSON snippet to the clipboard", () => {
spyOn(atom.keymaps, 'getUserKeymapPath').andReturn('keymap.json');
card.element.querySelector('.copy-icon').click();
expect(atom.clipboard.read()).toBe(`\
"test": {
"cmd-g": "language-test:run"
}\
`
);
});
});
});
describe("when the package is active", () =>
it("displays the correct enablement state", () => {
let packageCard = null;
waitsForPromise(() => {
return atom.packages.activatePackage('status-bar');
});
runs(() => {
expect(atom.packages.isPackageActive('status-bar')).toBe(true);
const pack = atom.packages.getLoadedPackage('status-bar');
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
packageCard = view.element.querySelector('.package-card');
});
runs(() => {
// Trigger observeDisabledPackages() here
// because it is not default in specs
atom.packages.observeDisabledPackages();
atom.packages.disablePackage('status-bar');
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true);
expect(packageCard.classList.contains('disabled')).toBe(true);
});
})
);
describe("when the package is not active", () => {
it("displays the correct enablement state", () => {
atom.packages.loadPackage('status-bar');
expect(atom.packages.isPackageActive('status-bar')).toBe(false);
const pack = atom.packages.getLoadedPackage('status-bar');
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
const packageCard = view.element.querySelector('.package-card');
// Trigger observeDisabledPackages() here
// because it is not default in specs
atom.packages.observeDisabledPackages();
atom.packages.disablePackage('status-bar');
expect(atom.packages.isPackageDisabled('status-bar')).toBe(true);
expect(packageCard.classList.contains('disabled')).toBe(true);
});
it("still loads the config schema for the package", () => {
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-config'));
waitsFor(() => atom.packages.isPackageLoaded('package-with-config') === true);
runs(() => {
expect(atom.config.get('package-with-config.setting')).toBe(undefined);
const pack = atom.packages.getLoadedPackage('package-with-config');
new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
expect(atom.config.get('package-with-config.setting')).toBe('something');
});
});
});
describe("when the package was not installed from atom.io", () => {
const normalizePackageDataReadmeError = 'ERROR: No README data found!';
it("still displays the Readme", () => {
atom.packages.loadPackage(path.join(__dirname, 'fixtures', 'package-with-readme'));
waitsFor(() => {
return atom.packages.isPackageLoaded('package-with-readme') === true;
});
runs(() => {
const pack = atom.packages.getLoadedPackage('package-with-readme');
expect(pack.metadata.readme).toBe(normalizePackageDataReadmeError);
const view = new PackageDetailView(pack, new SettingsView(), new PackageManager(), SnippetsProvider);
expect(
view.refs.sections.querySelector('.package-readme').textContent
).not.toBe(normalizePackageDataReadmeError);
expect(
view.refs.sections.querySelector('.package-readme').textContent.trim()
).toContain('I am a Readme!');
});
});
});
});

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

@ -37,8 +37,6 @@ async function modifyMainPackageJson(file, extraMetadata, isRemovePackageScripts
/// END Monkey-Patch
const builder = require("electron-builder")
const Platform = builder.Platform
const pngIcon = 'resources/app-icons/beta.png'
const icoIcon = 'resources/app-icons/beta.ico'
@ -55,6 +53,7 @@ let options = {
"package.json",
"dot-atom/**/*",
"exports/**/*",
"i18n/**/*",
"resources/**/*",
"src/**/*",
"static/**/*",
@ -219,7 +218,7 @@ let options = {
],
"target": [
{ "target": "nsis" },
{ "target": "portable" },
{ target: "zip" },
],
},
// Windows NSIS Configuration
@ -262,7 +261,6 @@ async function main() {
let options = whatToBuild()
options.extraMetadata = generateMetadata(JSON.parse(package))
builder.build({
//targets: Platform.LINUX.createTarget(),
config: options
}).then((result) => {
console.log("Built binaries")

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,

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

@ -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"
@ -5518,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"
@ -9506,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"