Merge branch 'clojure-highlight-with-new-improvements' into feature/electron-20

This commit is contained in:
Maurício Szabo 2023-03-12 14:28:31 -03:00
commit cf2a81a971
616 changed files with 55755 additions and 8960 deletions

View File

@ -4,7 +4,11 @@ module.exports = {
commonjs: true,
es2021: true,
},
extends: ["eslint:recommended", "plugin:node/recommended"],
extends: [
"eslint:recommended",
"plugin:node/recommended",
// "plugin:jsdoc/recommended"
],
overrides: [],
parserOptions: {
ecmaVersion: "latest"
@ -17,6 +21,9 @@ module.exports = {
}
]
},
plugins: [
"jsdoc"
],
globals: {
atom: "writeable"
}

38
.github/workflows/documentation.yml vendored Normal file
View File

@ -0,0 +1,38 @@
name: Documentation
on:
push:
branches: [ "master" ]
jobs:
documentation:
runs-on: ubuntu-latest
permissions:
contents: write
strategy:
matrix:
node-version: [16.x]
steps:
- name: Checkout the Latest Code
uses: actions/checkout@v3
- name: Setup NodeJS - ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
- name: Install Dependencies
run: yarn install
- name: Generate Public JSDocs
run: yarn run js-docs
- name: Generate Private JSDocs
run: yarn run private-js-docs
- name: Commit All Changes
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: GH Action Documentation

View File

@ -1,10 +1,10 @@
name: Editor tests
on:
- push
- pull_request
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ATOM_JASMINE_REPORTER: list
jobs:
tests:
name: tests
@ -13,7 +13,8 @@ jobs:
!startsWith(github.event.pull_request.title, '[skip-editor-ci]')
strategy:
matrix:
os: [ubuntu-20.04, macos-latest, windows-2019]
# os: [ubuntu-20.04, macos-latest, windows-2019]
os: [ubuntu-20.04, macos-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
@ -34,4 +35,4 @@ jobs:
- name: Run tests
uses: GabrielBB/xvfb-action@v1
with:
run: yarn start --test spec
run: node script/run-tests.js spec

View File

@ -1,9 +1,9 @@
name: Package tests for Pulsar on Linux
on:
- push
- pull_request
env:
APM_PATH: ./apm/node_modules/atom-package-manager/bin/apm
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ATOM_JASMINE_REPORTER: list
jobs:
setup:
name: Build Editor
@ -12,12 +12,6 @@ jobs:
!startsWith(github.event.pull_request.title, '[skip-package-ci]')
runs-on: ubuntu-20.04
steps:
- name: Install windows-build-tools
if: ${{ matrix.os == 'windows-latest' }}
run: |
npm i --global windows-build-tools@4.0.0
npm config set msvs_version 2019
- name: Checkout the latest code
uses: actions/checkout@v2
@ -35,60 +29,146 @@ jobs:
- name: build dependencies
run: yarn build:apm
- name: Cache node_modules
id: cache-node
uses: actions/cache@v3
with:
path: node_modules
key: linux-modules-${{ hashFiles('package.json') }}
- name: build the editor
run: (yarn dist deb || yarn dist deb) && mv binaries/*deb pulsar.deb
- name: Cache apm
id: cache-apm
- name: Cache pulsar
id: cache-pulsar
uses: actions/cache@v3
with:
path: apm
key: linux-apm-${{ hashFiles('apm/package.json') }}
path: pulsar.deb
key: pulsar-$env:GITHUB_SHA
test:
name: Test Packages
name: Package
needs: setup
runs-on: ubuntu-20.04
strategy:
fail-fast: false
max-parallel: 8
matrix:
include:
- filter: autocomplete|language
descr: syntax-related
- filter: dark|light
descr: themes
- filter: git-diff|go-to-line|grammar-selector|line-ending-selector|autoflow|autosave|symbols-view|spell-check|bracket-matcher|whitespace|wrap-guide|snippets|encoding-selector
descr: editor related
- filter: deprecation-cop|dev-live-reload|incompatible-packages|package-generator|exception-reporting|metrics|update-package-dependencies|styleguide
descr: development helpers
- filter: welcome|about|background-tips|tabs|tree-view|archive-view|markdown-preview|status-bar|settings-view|notifications
descr: UI elements
- filter: image-view|bookmarks|keybinding-resolver|link|timecop
descr: others
- package: "atom-dark-syntax"
- package: "atom-dark-ui"
- package: "atom-light-syntax"
- package: "atom-light-ui"
- package: "base16-tomorrow-dark-theme"
- package: "base16-tomorrow-light-theme"
- package: "one-dark-ui"
- package: "one-light-ui"
- package: "one-dark-syntax"
- package: "one-light-syntax"
- package: "solarized-dark-syntax"
- package: "solarized-light-syntax"
- package: "about"
- package: "archive-view"
- package: "autocomplete-atom-api"
- package: "autocomplete-css"
- package: "autocomplete-html"
- package: "autocomplete-plus"
- package: "autocomplete-snippets"
- package: "autoflow"
- package: "autosave"
- package: "background-tips"
- package: "bookmarks"
- package: "bracket-matcher"
- package: "command-palette"
- package: "dalek"
- package: "deprecation-cop"
- package: "dev-live-reload"
- package: "encoding-selector"
- package: "exception-reporting"
- package: "find-and-replace"
- package: "fuzzy-finder"
- package: "github"
- package: "git-diff"
- package: "go-to-line"
- package: "grammar-selector"
- package: "image-view"
- package: "incompatible-packages"
- package: "keybinding-resolver"
- package: "line-ending-selector"
- package: "link"
- package: "markdown-preview"
- package: "notifications"
- package: "open-on-github"
- package: "package-generator"
- package: "settings-view"
- package: "snippets"
- package: "spell-check"
- package: "status-bar"
- package: "styleguide"
- package: "symbols-view"
- package: "tabs"
- package: "timecop"
- package: "tree-view"
- package: "update-package-dependencies"
- package: "welcome"
- package: "whitespace"
- package: "wrap-guide"
- package: "language-c"
- package: "language-clojure"
- package: "language-coffee-script"
- package: "language-csharp"
- package: "language-css"
- package: "language-gfm"
- package: "language-git"
- package: "language-go"
- package: "language-html"
- package: "language-hyperlink"
- package: "language-java"
- package: "language-javascript"
- package: "language-json"
- package: "language-less"
- package: "language-make"
- package: "language-mustache"
- package: "language-objective-c"
- package: "language-perl"
- package: "language-php"
- package: "language-property-list"
- package: "language-python"
- package: "language-ruby"
- package: "language-ruby-on-rails"
- package: "language-rust-bundled"
- package: "language-sass"
- package: "language-shellscript"
- package: "language-source"
- package: "language-sql"
- package: "language-text"
- package: "language-todo"
- package: "language-toml"
- package: "language-typescript"
- package: "language-xml"
- package: "language-yaml"
steps:
- name: Checkout the latest code
uses: actions/checkout@v2
- name: Restore node_modules from Cache
id: restore-node
- name: Setup node
uses: actions/setup-node@v2-beta
with:
node-version: 16
- name: Install Dependencies
run: yarn install || yarn install
- name: Build Dependencies
run: yarn build || yarn build
- name: Restore pulsar from Cache
id: restore-pulsar
uses: actions/cache@v3
with:
path: node_modules
key: linux-modules-${{ hashFiles('package.json') }}
path: pulsar.deb
key: pulsar-$env:GITHUB_SHA
- name: Restore apm from Cache
id: restore-apm
uses: actions/cache@v3
with:
path: apm
key: linux-apm-${{ hashFiles('apm/package.json') }}
- name: Install Pulsar
run: sudo dpkg -i pulsar.deb && sudo apt-get -f install -y
- name: Run Package Tests for ${{ matrix.descr }}
uses: GabrielBB/xvfb-action@v1
with:
run: node -e "require('./script/run-package-tests')(/${{ matrix.filter }}/)"
- name: Install xvfb
run: sudo apt-get install -y xvfb
- name: Run ${{ matrix.package }} Tests
run: Xvfb :1 & cd node_modules/${{ matrix.package }} && if test -d spec; then DISPLAY=:1 pulsar --test spec; fi
# run: node -e "require('./script/run-package-tests')(/${{ matrix.package }}/)"

View File

@ -1,15 +1,304 @@
# Pulsar next version
- Bump github package to 0.36.13 (add Silicon Apple support)
# Changelog
- Format inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Format defined in [Pulsar Change Log](PENDING_APPROVAL)
- Project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
## [Unreleased]
## 1.102.0
- Fixed a bug where `pulsar` on Windows could never trigger
- Fixed `github` package shelling out to `git` on macOS
- Fixed minor bugs found during fixes to tests
- Improved our testing infastructure to aide in finding and fixing further bugs
- Updated many dependencies of Pulsar and its core packages
- New Pulsar Icon on macOS
- Selected text is styled by default
- Restored `right-clicked` CSS class on tags
- Fixed syntax highlighting on C++
- Updated JavaScript snippets to modern ES6 syntax
- PPM no longer assumes `master` for git branches
### Pulsar
- Added: implement signing and notarizing for macOS, PR #4 lol [@Meadowsys](https://github.com/pulsar-edit/pulsar/pull/387)
- Fixed: Pin `python` brew installation to `3.10` during MacOS Intel Cirrus Build [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/384)
- Update: Bump `ppm` to `a46537c0b7f0eaaef5404ef88003951fdc988c65` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/383)
- Added: Add new macOS icon [@mdibella-dev](https://github.com/pulsar-edit/pulsar/pull/372)
- Fixed: type $ as # [@Meadowsys](https://github.com/pulsar-edit/pulsar/pull/378)
- Update: deps: Update github to v0.36.14-pretranspiled-take-2 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/375)
- Added: add style to selected text by default [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/238)
- Added: Set Max Concurrent Package Tests [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/376)
- Fixed: c++ fixes [@icecream17](https://github.com/pulsar-edit/pulsar/pull/369)
- Update: deps: Update github to v0.36.14-pretranspiled [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/373)
- Update `coffeescript` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/361)
- Updated: Misc Dependency Updates [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/362)
- Added: Bundle `autocomplete-plus` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/358)
- Fixed: Add LICENSE.md to extra resources (resourcesPath) [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/354)
- Fixed: Get Windows `pulsar` Working [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/340)
- Fixed: Restore `right-clicked` class on a right-clicked tab [@savetheclocktower](https://github.com/pulsar-edit/pulsar/pull/368)
- Updated: ppm: Update submodule to commit 4645ba2905747897b0 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/371)
- Added: Machine decaf tabs spec [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/367)
- Added: Manually Decaf `tabs` package Specs [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/357)
- Fixed: Uncomment and fix a settings-view package test [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/366)
- Added: Decaf Changes from Manual and Machine Decaf to Main [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/356)
- Added: Manual decafe tabs [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/352)
- Added: Organize failing tests [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/307)
- Fixed: autocomplete-snippets: Fix repo URL [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/341)
- Updated: update apm message to pulsar -p [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/337)
- Fixed: Replace incorrect spellings of 'macOS' with the correct one [@mdibella-dev](https://github.com/pulsar-edit/pulsar/pull/336)
- Changed: use `let` and `const` in js snippets [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/326)
- Fixed: Fix URI to correct address [@mdibella-dev](https://github.com/pulsar-edit/pulsar/pull/335)
- Updated: update copyright year (2023) [@icecream17](https://github.com/pulsar-edit/pulsar/pull/332)
### ppm
- Fixed: fix: Don't assume `master` when checking git packages for upgrades [@savetheclocktower](https://github.com/pulsar-edit/ppm/pull/56)
- Fixed: meta: Normalize package.json and lockfile line endings [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/54)
- Update: spec: Fixtures Node v10.20.1 --> Electron v12.2.3 [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/52)
- Fixed: Fix .com links, pulsar rebranding and rebranding readme [@Daeraxa](https://github.com/pulsar-edit/ppm/pull/48)
### github
- Fixed: lib: Rebrand getAtomAppName() function (fix shelling out to `git` on macOS) [@DeeDeeG](https://github.com/pulsar-edit/github/pull/13)
- Fixed: meta: Revert "main" to "./lib/index", no dist (fix package on `master` branch) [@DeeDeeG](https://github.com/pulsar-edit/github/pull/12)
## 1.101.0-beta
- Fixed a bug where macOS menus like "Open" don't do anything
- Fixed a bug where macOS wouldn't open files by dragging them onto the dock.
- Fixed a bug where devtools won't open (https://github.com/pulsar-edit/pulsar/issues/260)
- Fixed a bug where the editor refused to open with the message "GPU process
isn't usable. Goodbye" (https://github.com/pulsar-edit/pulsar/issues/233)
- Fixed logo artifacts on Linux
- Fixed Windows Taskbar Icon being 'Cut in Half'
- Fixed commands like `--version`, `--package` or `--help` did not show outputs
- Fixed additional flags not being sent to `--package`
- Small improvement on the binary size
- Fixed "install command line tools" on Mac and Windows
- Cached queries for featured packages (featured packages will load faster, and
fewer errors on the settings-view regarding package info)
- Added warning when `settings-view` is disabled, describing how to re-enable it
### Pulsar
- Added: script: Clean up `pulsar` and `ppm` on uninstall [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/297)
- Added: increase search query delay [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/289)
- Fixed: update `packages/README.md` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/317)
- Fixed: Fix Windows Icon being cut in half [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/318)
- Removed: remove unused json [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/309)
- Added: add ignored `package-lock.json` to packages [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/308)
- Rebrand: Rebrand AppUserModelID - Ensure Pulsar is separated as its own App Icon on Windows [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/315)
- Removed: remove fs-plus from image-view package [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/305)
- Added: Additional Bundling of Core Packages [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/314)
- Fixed: Resolve some `about` package tests (6 Resolved Tests) [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/310)
- Fixed: Fix Package Test Cache Issue [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/302)
- Fixed: Resolve all Tests within `language-html` (Resolves 2 Failing Tests) [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/300)
- Fixed: Resolve all Tests within `language-javascript` (Resolves 24 Failing Tests) [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/299)
- Fixed: Resolve 40 Failing `image-view` Tests [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/293)
- Added: Added changelog entries that we missed [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/292)
- Removed: meta: Delete preinstall script from package.json [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/296)
- Added: Improve macOS Builds [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/280)
- Fixed: Fix `archive-view` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/294)
- Added: Improved Windows Builds [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/279)
- Added: More Bundles [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/290)
- Fixed: Fix macOS open without window [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/291)
- Removed: delete workflow from language-java [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/285)
- Removed: Remove handlers for opening things on Mac [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/288)
- Rebrand: Rebranding and relinking to new site [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/282)
- Added: script: symlink ppm in post-install.sh (for .deb and .rpm packages) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/273)
- Added: Add --no-sandbox to start script [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/276)
- Added: exclude directories from build [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/265)
- Added: add warning when settings-view is disabled [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/243)
- Fixed: Fix typo [@snowcatridge10](https://github.com/pulsar-edit/pulsar/pull/267)
- Fixed: Fix install on packaged code [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/269)
- Fixed: Fix Logo weirdness [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/271)
- Fixed: Fix installing shell commands to path (macOS) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/263)
- Fixed: 🍎 Fix wrong app name resolution in pulsar.sh on Mac [@soupertonic](https://github.com/pulsar-edit/pulsar/pull/252)
- Fixed: Postinstall error with rm usr/bin/pulsar [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/228)
- Added: Made changes to the main.js file. [@CatPerson136](https://github.com/pulsar-edit/pulsar/pull/232)
- Added: Add `--no-sandbox` to Linux Launch [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/262)
- Removed: removed unused files [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/219)
- Rebrand: rebrand package publish domain [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/245)
- Removed: remove metrics code from welcome package [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/244)
- Fixed: Deep cache for settings view [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/250)
- Fixed: fix syntax error in `packages/README.md` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/248)
- Removed: remove package.json dependencies [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/169)
- Added: `underscore-plus` to dependencies [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/218)
### ppm
- Added: Convert body params to query params [@Spiker985](https://github.com/pulsar-edit/ppm/pull/47)
- Fixed: src: Update Electron header download URL [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/43)
## v1.100.0-beta
- Bump to Electron 12 and Node 14
- Added a rebranded API
- Added a rebranding API
- Removed experimental file watchers on the editor
- Ability to install packages from git repositories
- New backend
- Better error messages when we can't install a package
- New Pulsar Package Repository Backend
- Better error messages when installing a package fails
- Config watching fixes
- Bump tree-sitter to 0.20.1 and all grammars to their recent versions
- Native support for Apple Silicon
- Change Log Window added to Welcome Package
- Native support for Apple Silicon and ARM Linux
- Removed Benchmark Startup Mode
- Removed all telemetry from Core Editor
- New Pulsar Website
- New Test Runner to Improve Testing
- Added Apple Silicon support to `github` Package v0.36.13
# Atom v1.6.0
See https://atom.io/releases
### Pulsar
- Added: Incorporate settings-view to core [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/220)
- Added: Bundle `autocomplete-css` && `autocomplete-html` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/212)
- Added: add or update `packages/*/package-lock.json` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/209)
- Fixed: Organize our Exclusions/Inclusions [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/208)
- Added: Bundle `package-generator` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/207)
- Fixed: meta: Don't exclude 'loophole' or 'pegjs' packages [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/206)
- Fixed: Fix `dugite` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/201)
- Bumped: ppm: Update ppm submodule (new Electron headers download URL) [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/198)
- Removed: Revert "Merge pull request #184 from pulsar-edit/bump-autocomplete-plus" [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/196)
- Bumped: Bump GitHub package [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/186)
- Fixed: CI (Windows): Use npm (not yarn) to install ppm [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/185)
- Bumped: Bumped `autocomplete-plus` [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/184)
- Added: Adding test runner missing files [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/183)
- Fixed: fix about package test [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/180)
- Added: Add tar.gz target to electron-builder [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/178)
- Fixed: Cleanup/standardize pulsar.sh [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/175)
- Fixed: Update LICENSE.md [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/171)
- Removed: remove old scripts [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/168)
- Fixed: Fix Codacy Ignore [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/167)
- Added: New ChangeLog Format [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/166)
- Fixed: shorten task description if too long [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/163)
- Fixed: Improve Package Tests [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/161)
- Removed: Metric docs from `welcome` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/159)
- Fixed: PostInstall of `ppm` [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/153)
- Fixed: Unmerged Menus ignoring separators [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/151)
- Removed: `mkdirp` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/150)
- Fixed: `--package` exiting incorrectly [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/149)
- Bumped: `ppm` submodule [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/144)
- Fixed: undefined `nsole` [@jonian](https://github.com/pulsar-edit/pulsar/pull/142)
- Fixed: Git tab in Binaries [@benonymus](https://github.com/pulsar-edit/pulsar/pull/140)
- Fixed: `yarn.lock` versions [@jonian](https://github.com/pulsar-edit/pulsar/pull/139)
- Added: `dist` & `binaries` to `gitignore` [@jonain](https://github.com/pulsar-edit/pulsar/pull/138)
- Bumped: `ppm` submodule to allow Git Package Install [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/131)
- Bumped: `settings-view` 0.261.9 -> 0.261.10 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/130)
- Removed: Unused code fragments from build scripts [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/128)
- Added: Ability to run `ppm` from `pulsar` CLI [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/125)
- Fixed: base16 URL to use WayBack Machine [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/121)
- Removed: `fs-plus` from `exception-reporting` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/118)
- Removed: Benchmark Startup Mode Part 2 [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/115)
- Removed: Unused scripts [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/114)
- Bumped: `background-tips` 0.28.0 -> 0.28.1 [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/111)
- Removed: Tooling bloat [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/110)
- Bumped: `snippets` NA -> 1.6.1 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/107)
- Removed: Benchmark Startup mode [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/105)
- Added: Binaries for Intel Mac & ARM Linux [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/101)
- Added: `yarn dist` accepts arguments [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/97)
- Fixed: Load core packages `README.md` [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/96)
- Fixed: Unlock terminal on Linux [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/82)
- Added: Aliases to workflow for link generation [@kaosine](https://github.com/pulsar-edit/pulsar/pull/78)
- Fixed: Hooked `NSFW` directly [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/77)
- Bumped: `settings-view` 0.261.8 -> 0.261.9 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/72)
- Bumped: `.nvmrc` 12.18 -> 16 [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/71)
- Bumped: `ppm` submodule for new backend [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/68)
- Removed: Experimental and internal watchers [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/67)
- Fixed: Improvements for windows binaries [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/66)
- Fixed: Improvements for binary building [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/63)
- Bumped: `async` 3.2.0 -> 3.2.4 [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/59)
- Removed: Mystery/Ghost Submodule [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/51)
- Removed: Telemetry and Remote Crash Reports [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/40)
- Added: Bundled `language-c` into the editor [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/33)
- Bumped: `electron` 11.5.0 -> 12.2.3 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/28)
- Fixed: `yarn install` due to syntax error [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/16)
- Added: Bundled most language grammars into the editor [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/14)
- Bumped: `autocomplete-html` 0.8.8 -> 0.8.9 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/14)
- Bumped: `tree-sitter` NA -> 0.20.0 [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/14)
- Added: Branding Config on Global Atom API [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/7)
- Added: `yarn` as method to build editor. [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/6)
- Bumped: `fs-admin` 0.15.0 -> 0.19.0 [@kaosine](https://github.com/pulsar-edit/pulsar/pull/4)
- Bumped: `text-buffer` 13.18.5 -> 13.18.6 [@kaosine](https://github.com/pulsar-edit/pulsar/pull/4)
- Decaffeinate: Numerous efforts from many contributors to decaffeinate the editor:
* [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/112)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/45)
* [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/29)
* [@fabianfiorotto](https://github.com/pulsar-edit/pulsar/pull/13)
- Rebrand: Numerous efforts from many contributors to rebrand the editor:
* [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/190)
* [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/173)
* [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/172)
* [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/156)
* [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/145)
* [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/136)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/126)
* [@ElectronicsArchiver](https://github.com/pulsar-edit/pulsar/pull/123)
* [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/122)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/120)
* [@Sertonix](https://github.com/pulsar-edit/pulsar/pull/103)
* [@Daeraxa](https://github.com/pulsar-edit/pulsar/pull/83)
* [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/81)
* [@kaosine](https://github.com/pulsar-edit/pulsar/pull/65)
* [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/58)
* [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/54)
* [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/22)
* [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/17)
* [@softcode589](https://github.com/pulsar-edit/pulsar/pull/11)
* [@LandarXT](https://github.com/pulsar-edit/pulsar/pull/10)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/8)
- Tests: Numerous efforts from many contributors to improve our tests:
* [@icecream17](https://github.com/pulsar-edit/pulsar/pull/152)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/141)
* [@DeeDeeG](https://github.com/pulsar-edit/pulsar/pull/116)
* [@Spiker985](https://github.com/pulsar-edit/pulsar/pull/109)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/70)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/50)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/48)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/46)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/42)
* [@confused-Techie](https://github.com/pulsar-edit/pulsar/pull/41)
* [@fabianfiorotto](https://github.com/pulsar-edit/pulsar/pull/36)
* [@fabianfiorotto](https://github.com/pulsar-edit/pulsar/pull/35)
* [@mauricioszabo](https://github.com/pulsar-edit/pulsar/pull/18)
### ppm
- Fixed: ppm PostInstall [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/41)
- Added: Better `help` command display [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/40)
- Fixed: Empty Featured Packages [@jonian](https://github.com/pulsar-edit/ppm/pull/38)
- Fixed: Use ppm as basename in `getResourcePath` [@jonain](https://github.com/pulsar-edit/ppm/pull/36)
- Fixed: Installation from Git [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/34)
- Added: Ability to define tag to install [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/13)
- Added: Our new Pulsar Package Repository Backend [@confused-Techie](https://github.com/pulsar-edit/ppm/pull/5)
- Bumped: `electron` to 12 [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/2)
- Rebrand: Numerous efforts from many contributors to rebrand ppm:
* [@Sertonix](https://github.com/pulsar-edit/ppm/pull/12)
* [@softcode589](https://github.com/pulsar-edit/ppm/pull/7)
* [@mauricioszabo](https://github.com/pulsar-edit/ppm/pull/6)
- Tests: Numerous efforts from many contributors to improve our tests:
* [@DeeDeeG](https://github.com/pulsar-edit/ppm/pull/39)
### autocomplete-html
- Fixed: Finding the proper Node version [@mauricioszabo](https://github.com/pulsar-edit/autocomplete-html/pull/1)
### settings-view
- Added: Remember Scroll Position [@jonian](https://github.com/pulsar-edit/settings-view/pull/12)
- Removed: Support for deprecated packages [@Sertonix](https://github.com/pulsar-edit/settings-view/pull/6)
- Added: Better errors when search fails [@mauricioszabo](https://github.com/pulsar-edit/settings-view/pull/2)
- Rebrand: Numerous efforts from many contributors to rebrand settings-view:
* [@mauricioszabo](https://github.com/pulsar-edit/settings-view/pull/7)
* [@softcode589](https://github.com/pulsar-edit/settings-view/pull/3)
* [@mauricioszabo](https://github.com/pulsar-edit/settings-view/pull/1)
- Tests: Numerous efforts from many contributors to improve our tests:
* [@confused-Techie](https://github.com/pulsar-edit/settings-view/pull/10)
### snippets
- Added: Proper Testing [@confused-Techie](https://github.com/pulsar-edit/snippets/pull/4)
- Removed: `fs-plus` [@Sertonix](https://github.com/pulsar-edit/snippets/pull/2)
- Fixed: Fix open Snippets URI [@Sertonix](https://github.com/pulsar-edit/snippets/pull/1)
### background-tips
- Bumped: `background-tips` 0.28.0 -> 0.28.1 [@confused-Techie](https://github.com/pulsar-edit/background-tips/pull/4)
- Rebrand: Numerous efforts from many contributors to rebrand background-tips:
* [@Sertonix](https://github.com/pulsar-edit/background-tips/pull/5)
* [@Sertonix](https://github.com/pulsar-edit/background-tips/pull/2)
* [@Sertonix](https://github.com/pulsar-edit/background-tips/pull/1)
## Atom v1.6.0
- See https://atom.io/releases

View File

@ -1,5 +1,5 @@
# VERSION: 0.2
# DESCRIPTION: Image to build Atom
# DESCRIPTION: Image to build Pulsar
FROM ubuntu:20.04

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2022 Pulsar-Edit
Copyright (c) 2022-2023 Pulsar-Edit
Original work copyright (c) 2011-2022 GitHub Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy

View File

@ -64,7 +64,7 @@
<!---------------------------{ Images }--------------------------->
[Preview]: https://user-images.githubusercontent.com/378023/49132478-f4b77680-f31f-11e8-9e10-e8454d8d9b7e.png 'Preview of the editor.'
[Preview]: resources/readme.png 'Preview of the editor.'
<!---------------------------{ Badges }--------------------------->

View File

View File

View File

@ -11,3 +11,7 @@
// console.log(`Saved! ${editor.getPath()}`)
// )
// );
//
// See the Pulsar Launch manual for more information on this file and how to
// customize it.
// https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#the-init-file

View File

@ -18,15 +18,15 @@
# 'ctrl-p': 'core:move-down'
#
# You can find more information about keymaps in these guides:
# * http://flight-manual.atom.io/using-atom/sections/basic-customization/#customizing-keybindings
# * http://flight-manual.atom.io/behind-atom/sections/keymaps-in-depth/
# * https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#customizing-keybindings
# * https://pulsar-edit.dev/docs/launch-manual/sections/behind-pulsar#keymaps-in-depth
#
# If you're having trouble with your keybindings not working, try the
# Keybinding Resolver: `Cmd+.` on macOS and `Ctrl+.` on other platforms. See the
# Debugging Guide for more information:
# * http://flight-manual.atom.io/hacking-atom/sections/debugging/#check-the-keybindings
# * https://pulsar-edit.dev/docs/launch-manual/sections/core-hacking/#check-your-keybindings
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Pulsar Flight Manual:
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#configuring-with-cson
# Pulsar Launch Manual:
# https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#configuring-with-cson

View File

@ -17,5 +17,5 @@
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it in the
# Pulsar Flight Manual:
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
# Pulsar Launch Manual:
# https://pulsar-edit.dev/docs/launch-manual/sections/using-pulsar/#configuring-with-cson

View File

@ -1,10 +1,10 @@
/*
* Your Stylesheet
*
* This stylesheet is loaded when Atom starts up and is reloaded automatically
* This stylesheet is loaded when Pulsar starts up and is reloaded automatically
* when it is changed and saved.
*
* Add your own CSS or Less to fully customize Atom.
* Add your own CSS or Less to fully customize Pulsar.
* If you are unfamiliar with Less, you can read more about it here:
* http://lesscss.org
*/

25
hooks/README.md Normal file
View File

@ -0,0 +1,25 @@
### Contents
| Filename | Description |
| - | - |
| post-checkout | This hook executes after a branch checkout, or branch switch has occurred. |
| post-merge | This hook executes after a branch merge has occurred |
| update_editor.sh | The actual brains of the hooks. Performs a yarn install, yarn build, yarn build:apm, and syncs all submodules. |
### Disclaimer
These hooks are not guaranteed. These were made out of convenience and presented to the org as an optional tool for usage.
### Usage
There are several ways to apply these hooks:
- You can manually copy the files over to the `<pulsar-repo-root>/.git/hooks` folder and validate that they are executable - the effect should be immediate. This is the preferred option for Windows.
- You can use manage_hooks.sh to copy/symlink the hooks you choose. This is the preferred option for Linux/macOS.
- Your mileage may vary on macOS as it has not been tested outright, but should work in theory.
### Instructions
- Open your favorite terminal
- Navigate to `<pulsar-repo-root>/hooks`.
- IMPORTANT: The bash completions will only work within this directory, and are activated when using exactly `./manage-hooks.sh`.
- If you have bash-completions, source the `manage_hooks-completion.bash` file to allow for auto-complete ie `source manage_hooks-completion.bash`.
- Allow the auto-complete responses to guide you.
- Standard commands are `list`, `install` and `remove`
- The `install` and `remove` commands require the hook you wish to install, followed by an optional parameter for `copy` vs `symlink` with symlink being the default.
- A symbolically linked hook allows you to receive updates in the future. If you plan on adjusting your hook(s), you probably want to `copy` the files to the `<pulsar-repo-root>/.git/hooks` directory

View File

@ -0,0 +1,59 @@
#!/bin/bash
#Not gonna lie, this is some magical stuff, but I will try to explain it
#Automatic variables used by complete
#COMPREPLY = An array of autocompletions to pass back to the shell
#COMP_WORDS = The passed-in list of parameters
#COMP_CWORD = The index of the current word as related to the count of
#parameters
#compgen = Generates an array of autocompletions to pass back to the shell
#based on provided parameters
function __completion() {
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
opts="list install remove"
#Determine what "level" of autocomplete we are operating on
#1 is no params
#2 is 1 param, in this instance the command
#3 etc
case ${COMP_CWORD} in
1)
#Generate a Word-list from opts from the current word
COMPREPLY=( $(compgen -W "${opts}" -- ${cur}) )
;;
2)
case ${prev} in
install)
#Generate a list of filenames (and append "all") based on the current
#directory and current word while ignoring select files
COMPREPLY=( $(compgen -W "all $(ls --ignore='*.md' --ignore='*.sh' --ignore='*.bash')" -- ${cur}) )
;;
remove)
#Generate a list of filenames (and append "all") based on the
#.git/hooks directory and current word while ignoring the sample
#files
COMPREPLY=( $(compgen -W "all $(ls ../.git/hooks --ignore='*.sample')" -- ${cur}) )
;;
esac
;;
3)
case "${COMP_WORDS[COMP_CWORD-2]}" in
install)
#When attempting to autocomplete for the third parameter ie copy
#/symbolic AND the command is install
#Generate a list of Words from the provded string
COMPREPLY=( $(compgen -W "copy symbolic" -- ${cur}) )
;;
esac
;;
esac
}
#Use the function defined above, when an autocomplete event for
#./manage_hooks.sh is fired. This is why you must be in the proper directory
complete -F __completion ./manage_hooks.sh

83
hooks/manage_hooks.sh Executable file
View File

@ -0,0 +1,83 @@
#!/bin/bash
USAGEMSG="Usage: manage_hooks <action> <hook> <option>"
if [[ $# -lt 1 ]] || [[ $# -gt 3 ]]; then
echo "$USAGEMSG"
exit 1
fi
action="$1"
case $1 in
list)
ls --color=if-tty -l ../.git/hooks --ignore='*.sample'
;;
install)
#Grab our list of files if all was passed.
#Couldn't get * to function without globbing
if [[ "$2" == "all" ]]; then
hooks=( $(readlink -f $(ls --ignore='*.md' --ignore='*.sh' --ignore='*.bash')) )
else
hooks=( $(readlink -f "$2") )
fi
#Default to symbolic
if [[ -z $3 ]]; then
option="symbolic"
else
option="$3"
fi
case $option in
copy)
#If copying, copy the update_editor file as well
hooks+=( "$(readlink -f "update_editor.sh")" "${hooks[@]}" )
for hook in "${hooks[@]}"; do
#If the file doesnt already exist copy it over
if [[ ! -f "../.git/hooks/$(basename "$hook")" ]]; then
echo "$hook"
cp "$hook" "../.git/hooks"
fi
done
;;
symbolic)
#Switch to the git hook directory, because symlink hooks are required
#to be relative
cd "../.git/hooks"
for hook in "${hooks[@]}"; do
#If the file doesnt already exist, symlink it
if [[ ! -f "$(basename $hook)" ]]; then
echo "$(readlink -f .)/$(basename $hook)"
ln --symbolic --relative "../../hooks/$(basename "$hook")"
fi
done
;;
esac
;;
remove)
#Switch to the git hook directory, so we dont have to deal with
#relative file paths
cd "../.git/hooks"
#Grab the canonical path to the file, readlink always follows symlinks
if [[ "$2" == "all" ]]; then
hooks=( $(realpath --no-symlinks $(ls --ignore='*.sample' --ignore='*.sh')) )
else
hooks=( $(realpath --no-symlinks "$2") )
fi
for hook in "${hooks[@]}"; do
echo "$hook"
rm "$hook"
done
;;
*)
echo "$USAGEMSG"
exit 1
;;
esac

24
hooks/post-checkout Executable file
View File

@ -0,0 +1,24 @@
#!/bin/bash
#This hook launches after a branch change, or branch checkout. It cannot affect
#the outcome of a git switch or git checkout with the exception that the hook
#exit status becomes the exit status of the antecedent command
#See https://git-scm.com/docs/githooks#_post_checkout
if [[ -L "$0" ]]; then
#If our launched script is a link, grab the root
hookRoot="$(dirname $(readlink --canonicalize "$0"))"
else
#Otherwise use the launched script directory
hookRoot="$(dirname "$0")"
fi
if [[ "$1" == "$2" ]]; then
#Previous HEAD and new HEAD are the same, ignore
exit 0
fi
if [[ "$3" == '1' ]]; then
#A branch checkout will always be flagged as 1, and a file checkout as 0
"${hookRoot}/update_editor.sh" $0
fi

15
hooks/post-merge Executable file
View File

@ -0,0 +1,15 @@
#!/bin/bash
#This hook launches after a git pull. It cannot affect a git merge, and will not
#be executed if a merge fails
#See https://git-scm.com/docs/githooks#_post_merge
if [[ -L "$0" ]]; then
#If our launched script is a link, grab the root
hookRoot="$(dirname $(readlink --canonicalize $0))"
else
#Otherwise use the launched script directory
hookRoot="$(dirname "$0")"
fi
"${hookRoot}/update_editor.sh" $0

56
hooks/update_editor.sh Executable file
View File

@ -0,0 +1,56 @@
#!/bin/bash
case $1 in
*post-checkout)
ACTION='Branch change'
;;
*post-merge)
ACTION='Remote pull'
;;
*)
ACTION="Unknown event ($1)"
;;
esac
echo "${ACTION} occurred, rebuilding editor"
export ATOM_ELECTRON_VERSION=$(cat package.json | rg --trim --replace "" '"electronVersion": "' | rg --replace "" '",')
replacement="1.100.$(date +'%Y%m%d%H%k%M')"
filter='("version": ")[0-9\.]+(",)'
regex="s/$filter/\1$replacement\2/"
sed --regexp-extended --in-place "$regex" package.json
echo ' Installing editor packages'
yarn install &> /dev/null
if [[ $? == 0 ]]; then
echo ' Install completed successfully'
else
echo ' Install failed'
exit 1
fi
echo ' Rebuilding modules'
yarn build &> /dev/null
if [[ $? == 0 ]]; then
echo ' Module build completed successfully'
else
echo ' Module build failed'
exit 1
fi
echo ' Rebuilding PPM'
if [[ -d "ppm" ]]; then
yarn build:apm &> /dev/null
if [[ $? == 0 ]]; then
echo ' PPM build completed successfully'
else
echo ' PPM build failed'
exit 1
fi
else
echo ' PPM folder not found'
fi
git submodule sync && git submodule update

View File

@ -2,7 +2,7 @@
"name": "pulsar",
"author": "Pulsar Community <noreply@pulsar-edit.com>",
"productName": "Pulsar",
"version": "1.63.0-dev",
"version": "1.102.0-dev",
"description": "A Community-led Hyper-Hackable Text Editor",
"branding": {
"id": "pulsar",
@ -25,7 +25,7 @@
"@babel/core": "7.18.6",
"@electron/remote": "2.0.8",
"about": "file:packages/about",
"archive-view": "https://codeload.github.com/atom/archive-view/legacy.tar.gz/refs/tags/v0.66.0",
"archive-view": "file:packages/archive-view",
"async": "3.2.4",
"atom-dark-syntax": "file:packages/atom-dark-syntax",
"atom-dark-ui": "file:packages/atom-dark-ui",
@ -36,30 +36,27 @@
"autocomplete-atom-api": "https://codeload.github.com/atom/autocomplete-atom-api/legacy.tar.gz/refs/tags/v0.10.7",
"autocomplete-css": "file:packages/autocomplete-css",
"autocomplete-html": "file:packages/autocomplete-html",
"autocomplete-plus": "https://codeload.github.com/atom/autocomplete-plus/legacy.tar.gz/refs/tags/v2.42.5",
"autocomplete-snippets": "https://codeload.github.com/atom/autocomplete-snippets/legacy.tar.gz/refs/tags/v1.12.1",
"autocomplete-plus": "file:./packages/autocomplete-plus",
"autocomplete-snippets": "file:packages/autocomplete-snippets",
"autoflow": "file:packages/autoflow",
"autosave": "https://codeload.github.com/atom/autosave/legacy.tar.gz/refs/tags/v0.24.6",
"babel-preset-atomic": "^5.0.0",
"background-tips": "https://codeload.github.com/pulsar-edit/background-tips/legacy.tar.gz/refs/tags/v0.28.1",
"background-tips": "file:packages/background-tips",
"base16-tomorrow-dark-theme": "file:packages/base16-tomorrow-dark-theme",
"base16-tomorrow-light-theme": "file:packages/base16-tomorrow-light-theme",
"bookmarks": "https://codeload.github.com/atom/bookmarks/legacy.tar.gz/refs/tags/v0.46.0",
"bookmarks": "file:packages/bookmarks",
"bracket-matcher": "https://github.com/pulsar-edit/bracket-matcher.git#use-napi-oniguruma",
"chai": "4.3.4",
"clear-cut": "^2.0.2",
"coffee-script": "1.12.7",
"coffeescript": "1.12.7",
"color": "3.1.3",
"command-palette": "https://codeload.github.com/atom/command-palette/legacy.tar.gz/refs/tags/v0.43.5",
"command-palette": "file:packages/command-palette",
"dalek": "file:packages/dalek",
"dedent": "^0.7.0",
"deprecation-cop": "file:packages/deprecation-cop",
"dev-live-reload": "file:packages/dev-live-reload",
"devtron": "1.4.0",
"document-register-element": "^1.14.10",
"electron-notarize": "1.0.0",
"electron-osx-sign": "0.5.0",
"encoding-selector": "https://codeload.github.com/atom/encoding-selector/legacy.tar.gz/refs/tags/v0.23.9",
"encoding-selector": "file:packages/encoding-selector",
"etch": "0.14.1",
"event-kit": "^2.5.3",
"exception-reporting": "file:packages/exception-reporting",
@ -74,11 +71,12 @@
"fuzzy-finder": "https://github.com/pulsar-edit/fuzzy-finder.git#simplify-code",
"git-diff": "file:packages/git-diff",
"git-utils": "https://github.com/pulsar-edit/git-utils.git#f4e8c37",
"github": "https://codeload.github.com/pulsar-edit/github/tar.gz/refs/tags/v0.36.14-pretranspiled-take-2",
"glob": "^7.1.1",
"go-to-line": "file:packages/go-to-line",
"grammar-selector": "file:packages/grammar-selector",
"grim": "2.0.3",
"image-view": "https://codeload.github.com/atom/image-view/legacy.tar.gz/refs/tags/v0.64.0",
"image-view": "file:packages/image-view",
"incompatible-packages": "file:packages/incompatible-packages",
"jasmine-json": "~0.0",
"jasmine-reporters": "1.1.0",
@ -138,45 +136,46 @@
"one-light-syntax": "file:packages/one-light-syntax",
"one-light-ui": "file:packages/one-light-ui",
"oniguruma": "https://github.com/Aerijo/napi-oniguruma.git",
"open-on-github": "https://codeload.github.com/atom/open-on-github/legacy.tar.gz/refs/tags/v1.3.2",
"open-on-github": "file:packages/open-on-github",
"package-generator": "file:packages/package-generator",
"pathwatcher": "https://github.com/pulsar-edit/node-pathwatcher.git#567561f76e77333107a528c0aa0ee30863d796e1",
"postcss": "8.2.10",
"postcss-selector-parser": "6.0.4",
"prebuild-install": "6.0.0",
"property-accessors": "^1.1.3",
"resolve": "1.18.1",
"scandal": "https://github.com/pulsar-edit/scandal.git#abfe4c4",
"scoped-property-store": "^0.17.0",
"scrollbar-style": "^4.0.1",
"season": "^6.0.2",
"semver": "7.3.2",
"semver": "7.3.8",
"service-hub": "^0.7.4",
"settings-view": "https://github.com/pulsar-edit/settings-view.git#v0.261.11",
"settings-view": "file:packages/settings-view",
"sinon": "9.2.1",
"snippets": "https://github.com/pulsar-edit/snippets.git#009b767",
"solarized-dark-syntax": "file:packages/solarized-dark-syntax",
"solarized-light-syntax": "file:packages/solarized-light-syntax",
"source-map-support": "0.5.21",
"spell-check": "https://codeload.github.com/atom/spell-check/legacy.tar.gz/refs/tags/v0.77.1",
"status-bar": "https://codeload.github.com/atom/status-bar/legacy.tar.gz/refs/tags/v1.8.17",
"status-bar": "file:packages/status-bar",
"styleguide": "https://codeload.github.com/atom/styleguide/legacy.tar.gz/refs/tags/v0.49.12",
"superstring": "https://github.com/pulsar-edit/superstring-wasm.git#312d1fb",
"symbols-view": "https://codeload.github.com/atom/symbols-view/legacy.tar.gz/refs/tags/v0.118.4",
"tabs": "https://codeload.github.com/atom/tabs/legacy.tar.gz/refs/tags/v0.110.2",
"temp": "0.9.2",
"tabs": "file:packages/tabs",
"temp": "0.9.4",
"text-buffer": "https://github.com/pulsar-edit/text-buffer.git#c0c7cf3e3",
"timecop": "https://codeload.github.com/atom/timecop/legacy.tar.gz/refs/tags/v0.36.2",
"tree-sitter": "https://github.com/pulsar-edit/node-tree-sitter.git#c304335",
"tree-view": "https://github.com/pulsar-edit/tree-view.git#4274c7a",
"typescript-simple": "8.0.6",
"underscore-plus": "^1.7.0",
"update-package-dependencies": "file:./packages/update-package-dependencies",
"vscode-ripgrep": "1.9.0",
"web-tree-sitter": "^0.20.7",
"welcome": "file:packages/welcome",
"whitespace": "https://codeload.github.com/atom/whitespace/legacy.tar.gz/refs/tags/v0.37.8",
"winreg": "^1.2.1",
"wrap-guide": "https://codeload.github.com/atom/wrap-guide/legacy.tar.gz/refs/tags/v0.41.0",
"yargs": "16.1.0"
"yargs": "17.6.2"
},
"packageDependencies": {
"atom-dark-syntax": "file:./packages/atom-dark-syntax",
@ -192,44 +191,45 @@
"solarized-dark-syntax": "file:./packages/solarized-dark-syntax",
"solarized-light-syntax": "file:./packages/solarized-light-syntax",
"about": "file:./packages/about",
"archive-view": "0.66.0",
"archive-view": "file:./packages/archive-view",
"autocomplete-atom-api": "0.10.7",
"autocomplete-css": "file:./packages/autocomplete-css",
"autocomplete-html": "file:./packages/autocomplete-html",
"autocomplete-plus": "2.42.4",
"autocomplete-snippets": "1.12.1",
"autocomplete-plus": "file:./packages/autocomplete-plus",
"autocomplete-snippets": "file:./packages/autocomplete-snippets",
"autoflow": "file:./packages/autoflow",
"autosave": "0.24.6",
"background-tips": "0.28.0",
"bookmarks": "0.46.0",
"background-tips": "file:./packages/background-tips",
"bookmarks": "file:./packages/bookmarks",
"bracket-matcher": "0.92.0",
"command-palette": "0.43.5",
"command-palette": "file:./packages/command-palette",
"dalek": "file:./packages/dalek",
"deprecation-cop": "file:./packages/deprecation-cop",
"dev-live-reload": "file:./packages/dev-live-reload",
"encoding-selector": "0.23.9",
"encoding-selector": "file:./packages/encoding-selector",
"exception-reporting": "file:./packages/exception-reporting",
"find-and-replace": "0.220.1",
"fuzzy-finder": "1.14.3",
"github": "0.36.14",
"git-diff": "file:./packages/git-diff",
"go-to-line": "file:./packages/go-to-line",
"grammar-selector": "file:./packages/grammar-selector",
"image-view": "0.64.0",
"image-view": "file:./packages/image-view",
"incompatible-packages": "file:./packages/incompatible-packages",
"keybinding-resolver": "0.39.1",
"line-ending-selector": "file:./packages/line-ending-selector",
"link": "file:./packages/link",
"markdown-preview": "0.160.2",
"notifications": "0.72.1",
"open-on-github": "1.3.2",
"package-generator": "1.3.0",
"settings-view": "0.261.9",
"open-on-github": "file:./packages/open-on-github",
"package-generator": "file:./packages/package-generator",
"settings-view": "file:./packages/settings-view",
"snippets": "1.6.1",
"spell-check": "0.77.1",
"status-bar": "1.8.17",
"status-bar": "file:./packages/status-bar",
"styleguide": "0.49.12",
"symbols-view": "0.118.4",
"tabs": "0.110.2",
"tabs": "file:./packages/tabs",
"timecop": "0.36.2",
"tree-view": "0.229.1",
"update-package-dependencies": "file:./packages/update-package-dependencies",
@ -273,31 +273,23 @@
},
"private": true,
"scripts": {
"preinstall": "node -e 'process.exit(0)'",
"build": "electron-rebuild",
"build:apm": "cd ppm && yarn install",
"start": "electron --enable-logging . -f",
"dist": "node script/electron-builder.js"
},
"standard-engine": "./script/node_modules/standard",
"standard": {
"env": {
"atomtest": true,
"browser": true,
"jasmine": true,
"node": true
},
"globals": [
"atom"
]
"start": "electron --no-sandbox --enable-logging . -f",
"dist": "node script/electron-builder.js",
"js-docs": "jsdoc2md ./src/**/*.js ./packages/**/*.js > ./docs/Pulsar-API-Documentation.md",
"private-js-docs": "jsdoc2md --private ./src/**/*.js ./packages/**/*.js > ./docs/Source-Code-Documentation.md"
},
"devDependencies": {
"@electron/notarize": "^1.2.3",
"@playwright/test": "1.22.2",
"electron": "19.1.3",
"electron-builder": "23.3.1",
"electron-rebuild": "3.2.7",
"eslint": "^8.27.0",
"eslint": "^8.33.0",
"eslint-plugin-jsdoc": "^39.7.4",
"eslint-plugin-node": "^11.1.0",
"jsdoc-to-markdown": "^8.0.0",
"playwright": "1.22.2",
"playwright-core": "1.22.2",
"random-seed": "0.3.0",

View File

@ -3,7 +3,7 @@
This folder contains core packages that are bundled with Pulsar releases. Not all core packages are kept here; please
see the table below for the location of every core package.
> **NOTE:** There is an ongoing effort to migrate more Atom packages from their individual repositories to this folder.
> **NOTE:** There is an ongoing effort to migrate more Pulsar packages from their individual repositories to this folder.
See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate-core-packages.md) for more details.
| Package | Where to find it | Migration issue |
@ -14,30 +14,30 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **atom-light-syntax** | [`./atom-light-syntax`](./atom-light-syntax) | |
| **atom-light-ui** | [`./atom-light-ui`](./atom-light-ui) | |
| **autocomplete-atom-api** | [`atom/autocomplete-atom-api`][autocomplete-atom-api] | |
| **autocomplete-css** | [`./autocomplete-css`][./autocomplete-css] | |
| **autocomplete-html** | [`./autocomplete-html`][./autocomplete-html] | |
| **autocomplete-css** | [`./autocomplete-css`](./autocomplete-css) | |
| **autocomplete-html** | [`./autocomplete-html`](./autocomplete-html) | |
| **autocomplete-plus** | [`atom/autocomplete-plus`][autocomplete-plus] | |
| **autocomplete-snippets** | [`atom/autocomplete-snippets`][autocomplete-snippets] | |
| **autocomplete-snippets** | [`./autocomplete-snippets`](./autocomplete-snippets) | |
| **autoflow** | [`./autoflow`](./autoflow) | |
| **autosave** | [`atom/autosave`][autosave] | [#17834](https://github.com/atom/atom/issues/17834) |
| **background-tips** | [`atom/background-tips`][background-tips] | [#17835](https://github.com/atom/atom/issues/17835) |
| **autosave** | [`pulsar-edit/autosave`][autosave] | [#17834](https://github.com/atom/atom/issues/17834) |
| **background-tips** | [`./background-tips`](./background-tips) | |
| **base16-tomorrow-dark-theme** | [`./base16-tomorrow-dark-theme`](./base16-tomorrow-dark-theme) | |
| **base16-tomorrow-light-theme** | [`./base16-tomorrow-light-theme`](./base16-tomorrow-light-theme) | |
| **bookmarks** | [`atom/bookmarks`][bookmarks] | [#18273](https://github.com/atom/atom/issues/18273) |
| **bookmarks** | [`./bookmarks`](./bookmarks) | |
| **bracket-matcher** | [`atom/bracket-matcher`][bracket-matcher] | |
| **command-palette** | [`atom/command-palette`][command-palette] | |
| **dalek** | [`./dalek`](./dalek) | [#17838](https://github.com/atom/atom/issues/17838) |
| **command-palette** | [`./command-palette`](./command-palette) | |
| **dalek** | [`./dalek`](./dalek) | |
| **deprecation-cop** | [`./deprecation-cop`](./deprecation-cop) | |
| **dev-live-reload** | [`./dev-live-reload`](./dev-live-reload) | |
| **encoding-selector** | [`atom/encoding-selector`][encoding-selector] | [#17841](https://github.com/atom/atom/issues/17841) |
| **encoding-selector** | [`./encoding-selector`](./encoding-selector) | |
| **exception-reporting** | [`./exception-reporting`](./exception-reporting) | |
| **find-and-replace** | [`atom/find-and-replace`][find-and-replace] | |
| **fuzzy-finder** | [`atom/fuzzy-finder`][fuzzy-finder] | |
| **github** | [`atom/github`][github] | |
| **find-and-replace** | [`pulsar-edit/find-and-replace`][find-and-replace] | |
| **fuzzy-finder** | [`pulsar-edit/fuzzy-finder`][fuzzy-finder] | |
| **github** | [`pulsar-edit/github`][github] | |
| **git-diff** | [`./git-diff`](./git-diff) | |
| **go-to-line** | [`./go-to-line`](./go-to-line) | |
| **grammar-selector** | [`./grammar-selector`](./grammar-selector) | |
| **image-view** | [`atom/image-view`][image-view] | [#18274](https://github.com/atom/atom/issues/18274) |
| **image-view** | [`./image-view`](./image-view) | |
| **incompatible-packages** | [`./incompatible-packages`](./incompatible-packages) | |
| **keybinding-resolver** | [`atom/keybinding-resolver`][keybinding-resolver] | [#18275](https://github.com/atom/atom/issues/18275) |
| **language-c** | [`./language-c`](./language-c) | |
@ -82,50 +82,38 @@ See [RFC 003](https://github.com/atom/atom/blob/master/docs/rfcs/003-consolidate
| **one-dark-ui** | [`./one-dark-ui`](./one-dark-ui) | |
| **one-light-syntax** | [`./one-light-syntax`](./one-light-syntax) | |
| **one-light-ui** | [`./one-light-ui`](./one-light-ui) | |
| **open-on-github** | [`atom/open-on-github`][open-on-github] | [#18278](https://github.com/atom/atom/issues/18278) |
| **package-generator** | [`./package-generator`][./package-generator] | |
| **settings-view** | [`atom/settings-view`][settings-view] | |
| **snippets** | [`atom/snippets`][snippets] | |
| **open-on-github** | [`./open-on-github`](./open-on-github) | |
| **settings-view** | [`./settings-view`](./settings-view) | |
| **package-generator** | [`./package-generator`](./package-generator) | |
| **snippets** | [`pulsar-edit/snippets`][snippets] | |
| **solarized-dark-syntax** | [`./solarized-dark-syntax`](./solarized-dark-syntax) | |
| **solarized-light-syntax** | [`./solarized-light-syntax`](./solarized-light-syntax) | |
| **spell-check** | [`atom/spell-check`][spell-check] | |
| **status-bar** | [`atom/status-bar`][status-bar] | [#18282](https://github.com/atom/atom/issues/18282) |
| **styleguide** | [`atom/styleguide`][styleguide] | [#18283](https://github.com/atom/atom/issues/18283) |
| **symbols-view** | [`atom/symbols-view`][symbols-view] | |
| **tabs** | [`atom/tabs`][tabs] | |
| **timecop** | [`atom/timecop`][timecop] | [#18272](https://github.com/atom/atom/issues/18272) |
| **tree-view** | [`atom/tree-view`][tree-view] | |
| **status-bar** | [`./status-bar`](./status-bar) | |
| **styleguide** | [`pulsar-edit/styleguide`][styleguide] | [#18283](https://github.com/atom/atom/issues/18283) |
| **symbols-view** | [`pulsar-edit/symbols-view`][symbols-view] | |
| **tabs** | [`./tabs`](./tabs) | |
| **timecop** | [`pulsar-edit/timecop`][timecop] | [#18272](https://github.com/atom/atom/issues/18272) |
| **tree-view** | [`pulsar-edit/tree-view`][tree-view] | |
| **update-package-dependencies** | [`./update-package-dependencies`](./update-package-dependencies) | |
| **welcome** | [`./welcome`](./welcome) | |
| **whitespace** | [`atom/whitespace`][whitespace] | |
| **whitespace** | [`./whitespace`](./whitespace) | |
| **wrap-guide** | [`atom/wrap-guide`][wrap-guide] | [#18286](https://github.com/atom/atom/issues/18286) |
[archive-view]: https://github.com/pulsar-edit/archive-view
[autocomplete-atom-api]: https://github.com/pulsar-edit/autocomplete-atom-api
[autocomplete-plus]: https://github.com/pulsar-edit/autocomplete-plus
[autocomplete-snippets]: https://github.com/pulsar-edit/autocomplete-snippets
[autosave]: https://github.com/pulsar-edit/autosave
[background-tips]: https://github.com/pulsar-edit/background-tips
[bookmarks]: https://github.com/pulsar-edit/bookmarks
[bracket-matcher]: https://github.com/pulsar-edit/bracket-matcher
[command-palette]: https://github.com/pulsar-edit/command-palette
[encoding-selector]: https://github.com/pulsar-edit/encoding-selector
[find-and-replace]: https://github.com/pulsar-edit/find-and-replace
[fuzzy-finder]: https://github.com/pulsar-edit/fuzzy-finder
[github]: https://github.com/pulsar-edit/github
[image-view]: https://github.com/pulsar-edit/image-view
[keybinding-resolver]: https://github.com/pulsar-edit/keybinding-resolver
[markdown-preview]: https://github.com/pulsar-edit/markdown-preview
[notifications]: https://github.com/pulsar-edit/notifications
[open-on-github]: https://github.com/pulsar-edit/open-on-github
[settings-view]: https://github.com/pulsar-edit/settings-view
[snippets]: https://github.com/pulsar-edit/snippets
[spell-check]: https://github.com/pulsar-edit/spell-check
[status-bar]: https://github.com/pulsar-edit/status-bar
[styleguide]: https://github.com/pulsar-edit/styleguide
[symbols-view]: https://github.com/pulsar-edit/symbols-view
[tabs]: https://github.com/pulsar-edit/tabs
[timecop]: https://github.com/pulsar-edit/timecop
[tree-view]: https://github.com/pulsar-edit/tree-view
[whitespace]: https://github.com/pulsar-edit/whitespace
[wrap-guide]: https://github.com/pulsar-edit/wrap-guide

View File

@ -1,7 +1,7 @@
const { Disposable } = require('atom');
const etch = require('etch');
const { shell } = require('electron');
const AtomLogo = require('./atom-logo'); //Update to new Pulsar logo (L#83)
const AtomLogo = require('./atom-logo');
const EtchComponent = require('../etch-component');
const UpdateView = require('./update-view');
@ -46,6 +46,7 @@ module.exports = class AboutView extends EtchComponent {
handleTermsOfUseClick(e) {
e.preventDefault();
shell.openExternal('https://atom.io/terms'); //If we use this then this URL will need updating but button disabled (L#182)
// TODO Update to Privacy Policy once `pulsar-edit.github.io` #161 is resolved
}
handleHowToUpdateClick(e) {
@ -80,8 +81,7 @@ module.exports = class AboutView extends EtchComponent {
{ className: 'about-header' },
$.a(
{ className: 'about-atom-io', href: `${atom.branding.urlWeb}`, },
//$(AtomLogo)
'Pulsar' //Remove and reinstate above line when available
$(AtomLogo)
),
$.div(
{ className: 'about-header-info' },
@ -224,4 +224,3 @@ module.exports = class AboutView extends EtchComponent {
return 'info';
}
};

View File

@ -125,7 +125,7 @@ let UpdateManager = class UpdateManager {
getReleaseNotesURLForVersion(appVersion) {
// Dev versions will not have a releases page
if (appVersion.indexOf('dev') > -1) {
return 'https://atom.io/releases';
return 'https://pulsar-edit.dev/download.html';
}
if (!appVersion.startsWith('v')) {

File diff suppressed because it is too large Load Diff

View File

@ -10,8 +10,8 @@
"atom": ">=1.7 <2.0.0"
},
"dependencies": {
"etch": "0.9.0",
"semver": "^5.5.0"
"etch": "^0.14.1",
"semver": "^7.3.8"
},
"consumedServices": {
"status-bar": {

View File

@ -25,7 +25,7 @@ describe('About', () => {
});
describe('when the about:about-atom command is triggered', () => {
it('shows the About Atom view', async () => {
it('shows the About Pulsar view', async () => {
// Attaching the workspaceElement to the DOM is required to allow the
// `toBeVisible()` matchers to work. Anything testing visibility or focus
// requires that the workspaceElement is on the DOM. Tests that attach the
@ -40,7 +40,7 @@ describe('About', () => {
});
});
describe('when the Atom version number is clicked', () => {
describe('when the Pulsar version number is clicked', () => {
it('copies the version number to the clipboard', async () => {
await atom.workspace.open('atom://about');
jasmine.attachToDOM(workspaceElement);

View File

@ -60,7 +60,7 @@ describe('the status bar', () => {
});
});
it('continues to show the squirrel until Atom is updated to the new version', async () => {
it('continues to show the squirrel until Pulsar is updated to the new version', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes');
@ -81,7 +81,7 @@ describe('the status bar', () => {
expect(workspaceElement).not.toContain('.about-release-notes');
});
it('does not show the view if Atom is updated to a newer version than notified', async () => {
it('does not show the view if Pulsar is updated to a newer version than notified', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0');
await atom.packages.deactivatePackage('about');
@ -125,7 +125,7 @@ describe('the status bar', () => {
});
});
it('continues to show the squirrel until Atom is updated to the new version', async () => {
it('continues to show the squirrel until Pulsar is updated to the new version', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0');
expect(workspaceElement).toContain('.about-release-notes');
@ -146,7 +146,7 @@ describe('the status bar', () => {
expect(workspaceElement).not.toContain('.about-release-notes');
});
it('does not show the view if Atom is updated to a newer version than notified', async () => {
it('does not show the view if Pulsar is updated to a newer version than notified', async () => {
MockUpdater.finishDownloadingUpdate('42.0.0');
await atom.packages.deactivatePackage('about');

View File

@ -8,26 +8,27 @@ describe('UpdateManager', () => {
});
describe('::getReleaseNotesURLForVersion', () => {
it('returns atom.io releases when dev version', () => {
it('returns pulsar-edit download page when dev version', () => {
expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-dev-e44b57d')
).toContain('atom.io/releases');
).toContain('pulsar-edit.dev/download');
});
it('returns the page for the release when not a dev version', () => {
expect(updateManager.getReleaseNotesURLForVersion('1.7.0')).toContain(
'atom-ide-community/atom/releases/tag/v1.7.0'
expect(updateManager.getReleaseNotesURLForVersion('1.100.0')).toContain(
'pulsar-edit/pulsar/releases/tag/v1.100.0'
);
expect(updateManager.getReleaseNotesURLForVersion('v1.7.0')).toContain(
'atom-ide-community/atom/releases/tag/v1.7.0'
expect(updateManager.getReleaseNotesURLForVersion('v1.100.0')).toContain(
'pulsar-edit/pulsar/releases/tag/v1.100.0'
);
// TODO: Since we no longer follow release channels, is it useful to continue testing their state?
expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-beta10')
).toContain('atom-ide-community/atom/releases/tag/v1.7.0-beta10');
updateManager.getReleaseNotesURLForVersion('1.100.0-beta10')
).toContain('pulsar-edit/pulsar/releases/tag/v1.100.0-beta10');
expect(
updateManager.getReleaseNotesURLForVersion('1.7.0-nightly10')
updateManager.getReleaseNotesURLForVersion('1.100.0-nightly10')
).toContain(
'atom-ide-community/atom-nightly-releases/releases/tag/v1.7.0-nightly10'
'pulsar-edit/pulsar-nightly-releases/releases/tag/v1.100.0-nightly10'
);
});
});

View File

@ -61,7 +61,7 @@ describe('UpdateView', () => {
let args = shell.openExternal.mostRecentCall.args;
expect(shell.openExternal).toHaveBeenCalled();
expect(args[0]).toContain('installing-atom');
expect(args[0]).toContain('installing-pulsar');
});
});
@ -241,120 +241,120 @@ describe('UpdateView', () => {
).toBe('Check now');
});
describe('when core.automaticallyUpdate is toggled', () => {
beforeEach(async () => {
expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
atom.autoUpdater.checkForUpdate.reset();
});
it('shows the auto update UI', async () => {
expect(
aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true);
expect(
aboutElement.querySelector('.about-default-update-message')
).toBeVisible();
expect(
aboutElement.querySelector('.about-default-update-message')
.textContent
).toBe('Pulsar will check for updates automatically');
atom.config.set('core.automaticallyUpdate', false);
await scheduler.getNextUpdatePromise();
expect(
aboutElement.querySelector('.about-auto-updates input').checked
).toBe(false);
expect(
aboutElement.querySelector('.about-default-update-message')
).toBeVisible();
expect(
aboutElement.querySelector('.about-default-update-message')
.textContent
).toBe('Automatic updates are disabled please check manually');
});
it('updates config and the UI when the checkbox is used to toggle', async () => {
expect(
aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true);
aboutElement.querySelector('.about-auto-updates input').click();
await scheduler.getNextUpdatePromise();
expect(atom.config.get('core.automaticallyUpdate')).toBe(false);
expect(
aboutElement.querySelector('.about-auto-updates input').checked
).toBe(false);
expect(
aboutElement.querySelector('.about-default-update-message')
).toBeVisible();
expect(
aboutElement.querySelector('.about-default-update-message')
.textContent
).toBe('Automatic updates are disabled please check manually');
aboutElement.querySelector('.about-auto-updates input').click();
await scheduler.getNextUpdatePromise();
expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
expect(
aboutElement.querySelector('.about-auto-updates input').checked
).toBe(true);
expect(
aboutElement.querySelector('.about-default-update-message')
).toBeVisible();
expect(
aboutElement.querySelector('.about-default-update-message')
.textContent
).toBe('Pulsar will check for updates automatically');
});
describe('checking for updates', function() {
afterEach(() => {
this.updateView = null;
});
it('checks for update when the about page is shown', () => {
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({
updateManager: updateManager,
availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {}
});
expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled();
});
it('does not check for update when the about page is shown and the update manager is not in the idle state', () => {
atom.autoUpdater.getState.andReturn('downloading');
updateManager.resetState();
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({
updateManager: updateManager,
availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {}
});
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
});
it('does not check for update when the about page is shown and auto updates are turned off', () => {
atom.config.set('core.automaticallyUpdate', false);
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
this.updateView = new UpdateView({
updateManager: updateManager,
availableVersion: '9999.0.0',
viewUpdateReleaseNotes: () => {}
});
expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
});
});
});
// describe('when core.automaticallyUpdate is toggled', () => {
// beforeEach(async () => {
// expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
// atom.autoUpdater.checkForUpdate.reset();
// });
//
// it('shows the auto update UI', async () => {
// expect(
// aboutElement.querySelector('.about-auto-updates input').checked
// ).toBe(true);
// expect(
// aboutElement.querySelector('.about-default-update-message')
// ).toBeVisible();
// expect(
// aboutElement.querySelector('.about-default-update-message')
// .textContent
// ).toBe('Pulsar will check for updates automatically');
//
// atom.config.set('core.automaticallyUpdate', false);
// await scheduler.getNextUpdatePromise();
//
// expect(
// aboutElement.querySelector('.about-auto-updates input').checked
// ).toBe(false);
// expect(
// aboutElement.querySelector('.about-default-update-message')
// ).toBeVisible();
// expect(
// aboutElement.querySelector('.about-default-update-message')
// .textContent
// ).toBe('Automatic updates are disabled please check manually');
// });
//
// it('updates config and the UI when the checkbox is used to toggle', async () => {
// expect(
// aboutElement.querySelector('.about-auto-updates input').checked
// ).toBe(true);
//
// aboutElement.querySelector('.about-auto-updates input').click();
// await scheduler.getNextUpdatePromise();
//
// expect(atom.config.get('core.automaticallyUpdate')).toBe(false);
// expect(
// aboutElement.querySelector('.about-auto-updates input').checked
// ).toBe(false);
// expect(
// aboutElement.querySelector('.about-default-update-message')
// ).toBeVisible();
// expect(
// aboutElement.querySelector('.about-default-update-message')
// .textContent
// ).toBe('Automatic updates are disabled please check manually');
//
// aboutElement.querySelector('.about-auto-updates input').click();
// await scheduler.getNextUpdatePromise();
//
// expect(atom.config.get('core.automaticallyUpdate')).toBe(true);
// expect(
// aboutElement.querySelector('.about-auto-updates input').checked
// ).toBe(true);
// expect(
// aboutElement.querySelector('.about-default-update-message')
// ).toBeVisible();
// expect(
// aboutElement.querySelector('.about-default-update-message')
// .textContent
// ).toBe('Pulsar will check for updates automatically');
// });
//
// describe('checking for updates', function() {
// afterEach(() => {
// this.updateView = null;
// });
//
// it('checks for update when the about page is shown', () => {
// expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
//
// this.updateView = new UpdateView({
// updateManager: updateManager,
// availableVersion: '9999.0.0',
// viewUpdateReleaseNotes: () => {}
// });
//
// expect(atom.autoUpdater.checkForUpdate).toHaveBeenCalled();
// });
//
// it('does not check for update when the about page is shown and the update manager is not in the idle state', () => {
// atom.autoUpdater.getState.andReturn('downloading');
// updateManager.resetState();
// expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
//
// this.updateView = new UpdateView({
// updateManager: updateManager,
// availableVersion: '9999.0.0',
// viewUpdateReleaseNotes: () => {}
// });
//
// expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
// });
//
// it('does not check for update when the about page is shown and auto updates are turned off', () => {
// atom.config.set('core.automaticallyUpdate', false);
// expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
//
// this.updateView = new UpdateView({
// updateManager: updateManager,
// availableVersion: '9999.0.0',
// viewUpdateReleaseNotes: () => {}
// });
//
// expect(atom.autoUpdater.checkForUpdate).not.toHaveBeenCalled();
// });
// });
// });
});
});

View File

@ -0,0 +1,20 @@
# Archive view package
Adds support for browsing archive files in Pulsar with the following extensions:
* `.egg`
* `.epub`
* `.jar`
* `.love`
* `.nupkg`
* `.tar`
* `.tar.gz`
* `.tgz`
* `.war`
* `.whl`
* `.xpi`
* `.zip`
Select a file to extract it to a temp file and open it in a new editor.
![](./resources/preview.png)

View File

@ -0,0 +1,3 @@
'.archive-editor':
'k': 'core:move-up'
'j': 'core:move-down'

View File

@ -0,0 +1,208 @@
/** @babel */
/** @jsx etch.dom */
import fs from 'fs'
import humanize from 'humanize-plus'
import archive from 'ls-archive'
import {CompositeDisposable, Disposable, Emitter, File} from 'atom'
import etch from 'etch'
import FileView from './file-view'
import DirectoryView from './directory-view'
export default class ArchiveEditorView {
constructor (archivePath) {
this.disposables = new CompositeDisposable()
this.emitter = new Emitter()
this.path = archivePath
this.file = new File(this.path)
this.entries = []
etch.initialize(this)
this.refresh()
this.disposables.add(this.file.onDidChange(() => this.refresh()))
this.disposables.add(this.file.onDidRename(() => this.refresh()))
this.disposables.add(this.file.onDidDelete(() => this.destroy()))
const focusHandler = () => this.focusSelectedFile()
this.element.addEventListener('focus', focusHandler)
this.disposables.add(new Disposable(() => this.element.removeEventListener('focus', focusHandler)))
}
update () {}
render () {
return (
<div className='archive-editor' tabIndex='-1'>
<div className='archive-container'>
<div ref='loadingMessage' className='padded icon icon-hourglass text-info'>{`Loading archive\u2026`}</div>
<div ref='errorMessage' className='padded icon icon-alert text-error' />
<div className='inset-panel'>
<div ref='summary' className='panel-heading' />
<ol ref='tree' className='archive-tree padded list-tree has-collapsable-children' />
</div>
</div>
</div>
)
}
copy () {
return new ArchiveEditorView(this.path)
}
destroy () {
while (this.entries.length > 0) {
this.entries.pop().destroy()
}
this.disposables.dispose()
this.emitter.emit('did-destroy')
etch.destroy(this)
}
onDidDestroy (callback) {
return this.emitter.on('did-destroy', callback)
}
onDidChangeTitle (callback) {
return this.emitter.on('did-change-title', callback)
}
serialize () {
return {
deserializer: this.constructor.name,
path: this.path
}
}
getPath () {
return this.file.getPath()
}
getTitle () {
return this.path ? this.file.getBaseName() : 'untitled'
}
getURI () {
return this.path
}
refresh () {
this.refs.summary.style.display = 'none'
this.refs.tree.style.display = 'none'
this.refs.loadingMessage.style.display = ''
this.refs.errorMessage.style.display = 'none'
if (this.path !== this.getPath()) {
this.path = this.getPath()
this.emitter.emit('did-change-title')
}
const originalPath = this.path
archive.list(this.path, {tree: true}, (error, entries) => {
if (originalPath !== this.path) {
return
}
if (error != null) {
let message = 'Reading the archive file failed'
if (error.message) {
message += `: ${error.message}`
}
this.refs.errorMessage.style.display = ''
this.refs.errorMessage.textContent = message
} else {
this.createTreeEntries(entries)
this.updateSummary()
}
// We hide the loading message _after_ creating the archive tree
// to avoid forced reflows.
this.refs.loadingMessage.style.display = 'none'
})
}
createTreeEntries (entries) {
while (this.entries.length > 0) {
this.entries.pop().destroy()
}
let index = 0
for (const entry of entries) {
if (entry.isDirectory()) {
const entryView = new DirectoryView(this, index, this.path, entry)
this.entries.push(entryView)
} else {
const entryView = new FileView(this, index, this.path, entry)
this.entries.push(entryView)
}
index++
}
this.selectFileAfterIndex(-1)
// Wait until selecting (focusing) the first file before appending the entries
// to avoid a double-forced reflow when focusing.
for (const entry of this.entries) {
this.refs.tree.appendChild(entry.element)
}
this.refs.tree.style.display = ''
}
updateSummary () {
const fileCount = this.entries.filter((entry) => entry instanceof FileView).length
const fileLabel = fileCount === 1 ? '1 file' : `${humanize.intComma(fileCount)} files`
const directoryCount = this.entries.filter((entry) => entry instanceof DirectoryView).length
const directoryLabel = directoryCount === 1 ? '1 folder' : `${humanize.intComma(directoryCount)} folders`
this.refs.summary.style.display = ''
let fileSize
try {
fileSize = fs.statSync(this.path)?.size;
} catch (e) {}
if (fileSize == null) fileSize = -1
this.refs.summary.textContent = `${humanize.fileSize(fileSize)} with ${fileLabel} and ${directoryLabel}`
}
focusSelectedFile () {
const selectedFile = this.refs.tree.querySelector('.selected')
if (selectedFile) {
selectedFile.focus()
}
}
selectFileBeforeIndex (index) {
for (let i = index - 1; i >= 0; i--) {
const previousEntry = this.entries[i]
if (previousEntry instanceof FileView) {
previousEntry.select()
break
} else {
if (previousEntry.selectLastFile()) {
break
}
}
}
}
selectFileAfterIndex (index) {
for (let i = index + 1; i < this.entries.length; i++) {
const nextEntry = this.entries[i]
if (nextEntry instanceof FileView) {
nextEntry.select()
break
} else {
if (nextEntry.selectFirstFile()) {
break
}
}
}
}
focus () {
this.focusSelectedFile()
}
}

View File

@ -0,0 +1,74 @@
const fs = require('fs')
const path = require('path')
const {Disposable} = require('atom')
const getIconServices = require('./get-icon-services')
const ArchiveEditorView = require('./archive-editor-view')
module.exports = {
activate () {
this.disposable = atom.workspace.addOpener((filePath = '') => {
// Check that filePath exists before opening, in case a remote URI was given
if (!isPathSupported(filePath)) return;
let isFile = false
try {
isFile = fs.statSync(filePath)?.isFile()
} catch (e) {}
if (isFile) {
return new ArchiveEditorView(filePath)
}
})
},
deactivate () {
this.disposable.dispose()
for (const item of atom.workspace.getPaneItems()) {
if (item instanceof ArchiveEditorView) {
item.destroy()
}
}
},
consumeElementIcons (service) {
getIconServices().setElementIcons(service)
return new Disposable(() => getIconServices().resetElementIcons())
},
consumeFileIcons (service) {
getIconServices().setFileIcons(service)
return new Disposable(() => getIconServices().resetFileIcons())
},
deserialize (params = {}) {
let isFile = false
try {
isFile = fs.statSync(params.path)?.isFile()
} catch (e) {}
if (isFile) {
return new ArchiveEditorView(params.path)
} else {
console.warn(`Can't build ArchiveEditorView for path "${params.path}"; file no longer exists`)
}
}
}
function isPathSupported (filePath) {
switch (path.extname(filePath)) {
case '.egg':
case '.epub':
case '.jar':
case '.love':
case '.nupkg':
case '.tar':
case '.tgz':
case '.war':
case '.whl':
case '.xpi':
case '.zip':
return true
case '.gz':
return path.extname(path.basename(filePath, '.gz')) === '.tar'
default:
return false
}
}

View File

@ -0,0 +1,36 @@
const fs = require('fs')
const path = require('path')
class DefaultFileIcons {
iconClassForPath (filePath) {
const extension = path.extname(filePath).toLowerCase()
const base = path.basename(filePath, extension).toLowerCase();
let isSymbolicLinkSync = false
try {
fs.lstatSync(filePath)?.isSymbolicLink();
} catch (e) {}
if (isSymbolicLinkSync) return 'icon-file-symlink-file'
if (base === 'readme' && ['','.markdown','.md','.mdown','.mkd','.mkdown','.rmd','.ron'].includes(extension)) {
return 'icon-book'
}
if (['.bz2','.egg','.epub','.gem','.gz','.jar','.lz','.lzma','.lzo','.rar','.tar','.tgz','.war','.whl','.xpi','.xz','.z','.zip'].includes(extension)) {
return 'icon-file-zip'
}
if (['.gif','.ico','.jpeg','.jpg','.png','.tif','.tiff','.webp'].includes(extension)) {
return 'icon-file-media'
}
if (extension === ".pdf") return 'icon-file-pdf'
if (['.ds_store','.a','.exe','.o','.pyc','.pyo','.so','.woff'].includes(extension)) {
return 'icon-file-binary'
}
return 'icon-file-text'
}
}
module.exports = new DefaultFileIcons()

View File

@ -0,0 +1,132 @@
/** @babel */
import {CompositeDisposable, Disposable} from 'atom'
import FileView from './file-view'
import getIconServices from './get-icon-services'
export default class DirectoryView {
constructor (parentView, indexInParentView, archivePath, entry) {
this.disposables = new CompositeDisposable()
this.entries = []
this.parentView = parentView
this.indexInParentView = indexInParentView
this.element = document.createElement('li')
this.element.classList.add('list-nested-item', 'entry')
const listItem = document.createElement('span')
listItem.classList.add('list-item')
const clickHandler = (event) => {
event.stopPropagation()
event.preventDefault()
this.element.classList.toggle('collapsed')
}
listItem.addEventListener('click', clickHandler)
this.disposables.add(new Disposable(() => { listItem.removeEventListener('click', clickHandler) }))
const entrySpan = document.createElement('span')
entrySpan.textContent = entry.getName()
listItem.appendChild(entrySpan)
this.element.appendChild(listItem)
this.entry = entry
this.entrySpan = entrySpan
getIconServices().updateDirectoryIcon(this)
this.entriesTree = document.createElement('ol')
this.entriesTree.classList.add('list-tree')
let index = 0
for (const child of entry.children) {
if (child.isDirectory()) {
const entryView = new DirectoryView(this, index, archivePath, child)
this.entries.push(entryView)
this.entriesTree.appendChild(entryView.element)
} else {
const entryView = new FileView(this, index, archivePath, child)
this.entries.push(entryView)
this.entriesTree.appendChild(entryView.element)
}
index++
}
this.element.appendChild(this.entriesTree)
}
destroy () {
if (this.iconDisposable) {
this.iconDisposable.dispose()
this.iconDisposable = null
}
while (this.entries.length > 0) {
this.entries.pop().destroy()
}
this.disposables.dispose()
this.element.remove()
}
selectFileBeforeIndex (index) {
for (let i = index - 1; i >= 0; i--) {
const previousEntry = this.entries[i]
if (previousEntry instanceof FileView) {
previousEntry.select()
return
} else {
if (previousEntry.selectLastFile()) {
return
}
}
}
this.parentView.selectFileBeforeIndex(this.indexInParentView)
}
selectFileAfterIndex (index) {
for (let i = index + 1; i < this.entries.length; i++) {
const nextEntry = this.entries[i]
if (nextEntry instanceof FileView) {
nextEntry.select()
return
} else {
if (nextEntry.selectFirstFile()) {
return
}
}
}
this.parentView.selectFileAfterIndex(this.indexInParentView)
}
selectFirstFile () {
for (const entry of this.entries) {
if (entry instanceof FileView) {
entry.select()
return true
} else {
if (entry.selectFirstFile()) {
return true
}
}
}
return false
}
selectLastFile () {
for (var i = this.entries.length - 1; i >= 0; i--) {
const entry = this.entries[i]
if (entry instanceof FileView) {
entry.select()
return true
} else {
if (entry.selectLastFile()) {
return true
}
}
}
return false
}
}

View File

@ -0,0 +1,112 @@
/** @babel */
import {CompositeDisposable, Disposable} from 'atom'
import path from 'path'
import fs from 'fs'
import temp from 'temp'
import archive from 'ls-archive'
import getIconServices from './get-icon-services'
export default class FileView {
constructor (parentView, indexInParentView, archivePath, entry) {
this.disposables = new CompositeDisposable()
this.parentView = parentView
this.indexInParentView = indexInParentView
this.archivePath = archivePath
this.entry = entry
this.element = document.createElement('li')
this.element.classList.add('list-item', 'entry')
this.element.tabIndex = -1
this.name = document.createElement('span')
getIconServices().updateFileIcon(this)
this.name.textContent = this.entry.getName()
this.element.appendChild(this.name)
const clickHandler = () => {
this.select()
this.openFile()
}
this.element.addEventListener('click', clickHandler)
this.disposables.add(new Disposable(() => { this.element.removeEventListener('click', clickHandler) }))
this.disposables.add(atom.commands.add(this.element, {
'core:confirm': () => {
if (this.isSelected()) {
this.openFile()
}
},
'core:move-down': () => {
if (this.isSelected()) {
this.parentView.selectFileAfterIndex(this.indexInParentView)
}
},
'core:move-up': () => {
if (this.isSelected()) {
this.parentView.selectFileBeforeIndex(this.indexInParentView)
}
}
}))
}
destroy () {
this.disposables.dispose()
this.element.remove()
}
isSelected () {
return this.element.classList.contains('selected')
}
logError (message, error) {
console.error(message, error.stack != null ? error.stack : error)
}
openFile () {
archive.readFile(this.archivePath, this.entry.getPath(), (error, contents) => {
if (error != null) {
this.logError(`Error reading: ${this.entry.getPath()} from ${this.archivePath}`, error)
} else {
temp.mkdir('atom-', (error, tempDirPath) => {
if (error != null) {
this.logError(`Error creating temp directory: ${tempDirPath}`, error)
} else {
const tempArchiveDirPath = path.join(tempDirPath, path.basename(this.archivePath))
fs.mkdir(tempArchiveDirPath, {recursive:true}, error => {
if (error != null) {
this.logError(`Error creating archive directory ${tempArchiveDirPath}`, error)
} else {
const tempFilePath = path.join(tempArchiveDirPath, this.entry.getName())
fs.writeFile(tempFilePath, contents, error => {
if (error != null) {
this.logError(`Error writing to ${tempFilePath}`, error)
} else {
atom.workspace.open(tempFilePath)
}
})
}
})
}
})
}
})
}
select () {
this.element.focus()
const archiveEditorElement = this.element.closest('.archive-editor')
// On initial tree creation, it is not possible for any entries to be selected
// (The entries also haven't been added to the DOM yet)
if (archiveEditorElement) {
for (const selected of archiveEditorElement.querySelectorAll('.selected')) {
selected.classList.remove('selected')
}
}
this.element.classList.add('selected')
}
}

View File

@ -0,0 +1,72 @@
const DefaultFileIcons = require('./default-file-icons')
const {Emitter, CompositeDisposable} = require('atom')
const path = require('path')
let iconServices
module.exports = function getIconServices () {
if (!iconServices) iconServices = new IconServices()
return iconServices
}
class IconServices {
constructor () {
this.emitter = new Emitter()
this.elementIcons = null
this.elementIconDisposables = new CompositeDisposable()
this.fileIcons = DefaultFileIcons
}
onDidChange (callback) {
return this.emitter.on('did-change', callback)
}
resetElementIcons () {
this.setElementIcons(null)
}
resetFileIcons () {
this.setFileIcons(DefaultFileIcons)
}
setElementIcons (service) {
if (service !== this.elementIcons) {
if (this.elementIconDisposables != null) {
this.elementIconDisposables.dispose()
}
if (service) { this.elementIconDisposables = new CompositeDisposable() }
this.elementIcons = service
return this.emitter.emit('did-change')
}
}
setFileIcons (service) {
if (service !== this.fileIcons) {
this.fileIcons = service
return this.emitter.emit('did-change')
}
}
updateDirectoryIcon (view) {
view.entrySpan.classList.add('directory', 'icon', 'icon-file-directory')
if (this.elementIcons) {
view.iconDisposable = this.elementIcons(view.entrySpan, view.entry.path, {isDirectory: true})
}
}
updateFileIcon (view) {
const nameClasses = ['file', 'icon']
if (this.elementIcons) {
const fullPath = path.join(view.archivePath, view.entry.path)
const disposable = this.elementIcons(view.name, fullPath)
view.disposables.add(disposable)
this.elementIconDisposables.add(disposable)
} else {
let typeClass = this.fileIcons.iconClassForPath(view.entry.path, 'archive-view') || []
if (!Array.isArray(typeClass) && typeClass) {
typeClass = typeClass.toString().split(/\s+/g)
}
nameClasses.push(...typeClass)
}
view.name.classList.add(...nameClasses)
}
}

513
packages/archive-view/package-lock.json generated Normal file
View File

@ -0,0 +1,513 @@
{
"name": "archive-view",
"version": "0.66.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "archive-view",
"version": "0.66.0",
"license": "MIT",
"dependencies": {
"etch": "^0.14.1",
"humanize-plus": "~1.8.2",
"ls-archive": "1.3.4",
"temp": "^0.9.4"
},
"engines": {
"atom": "*"
}
},
"node_modules/async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"node_modules/block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==",
"dependencies": {
"inherits": "~2.0.0"
},
"engines": {
"node": "0.4 || >=0.5.8"
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==",
"engines": {
"node": "*"
}
},
"node_modules/colors": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
"integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw==",
"engines": {
"node": ">=0.1.90"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"node_modules/etch": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/etch/-/etch-0.14.1.tgz",
"integrity": "sha512-+IwqSDBhaQFMUHJu4L/ir0dhDoW5IIihg4Z9lzsIxxne8V0PlSg0gnk2STaKWjGJQnDR4cxpA+a/dORX9kycTA=="
},
"node_modules/fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"dependencies": {
"pend": "~1.2.0"
}
},
"node_modules/fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"node_modules/fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"dependencies": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
},
"engines": {
"node": ">=0.6"
}
},
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"dependencies": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
},
"engines": {
"node": "*"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"node_modules/humanize-plus": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/humanize-plus/-/humanize-plus-1.8.2.tgz",
"integrity": "sha512-jaLeQyyzjjINGv7O9JJegjsaUcWjSj/1dcXvLEgU3pGdqCdP1PiC/uwr+saJXhTNBHZtmKnmpXyazgh+eceRxA==",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"dependencies": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"node_modules/ls-archive": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/ls-archive/-/ls-archive-1.3.4.tgz",
"integrity": "sha512-7GmjZOckV+gzm4PM1/LcWIsZIRsSkAVmIchoEf5xjquNKU0Ti5KUvGQ3dl/7VsbZIduMOPwRDXrvpo3LVJ0Pmg==",
"dependencies": {
"async": "~0.2.9",
"colors": "~0.6.2",
"optimist": "~0.5.2",
"rimraf": "~2.2.6",
"tar": "^2.2.1",
"yauzl": "^2.9.1"
},
"bin": {
"lsa": "bin/lsa"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"dependencies": {
"minimist": "^1.2.6"
},
"bin": {
"mkdirp": "bin/cmd.js"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"dependencies": {
"wrappy": "1"
}
},
"node_modules/optimist": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz",
"integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==",
"dependencies": {
"wordwrap": "~0.0.2"
}
},
"node_modules/path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"node_modules/rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg==",
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/tar": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"deprecated": "This version of tar is no longer supported, and will not receive security updates. Please upgrade asap.",
"dependencies": {
"block-stream": "*",
"fstream": "^1.0.12",
"inherits": "2"
}
},
"node_modules/temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"dependencies": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/temp/node_modules/rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"dependencies": {
"glob": "^7.1.3"
},
"bin": {
"rimraf": "bin.js"
}
},
"node_modules/wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"dependencies": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
},
"dependencies": {
"async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
},
"balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
},
"block-stream": {
"version": "0.0.9",
"resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz",
"integrity": "sha512-OorbnJVPII4DuUKbjARAe8u8EfqOmkEEaSFIyoQ7OjTHn6kafxWl0wLgoZ2rXaYd7MyLcDaU4TmhfxtwgcccMQ==",
"requires": {
"inherits": "~2.0.0"
}
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"buffer-crc32": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz",
"integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="
},
"colors": {
"version": "0.6.2",
"resolved": "https://registry.npmjs.org/colors/-/colors-0.6.2.tgz",
"integrity": "sha512-OsSVtHK8Ir8r3+Fxw/b4jS1ZLPXkV6ZxDRJQzeD7qo0SqMXWrHDM71DgYzPMHY8SFJ0Ao+nNU2p1MmwdzKqPrw=="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="
},
"etch": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/etch/-/etch-0.14.1.tgz",
"integrity": "sha512-+IwqSDBhaQFMUHJu4L/ir0dhDoW5IIihg4Z9lzsIxxne8V0PlSg0gnk2STaKWjGJQnDR4cxpA+a/dORX9kycTA=="
},
"fd-slicer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz",
"integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==",
"requires": {
"pend": "~1.2.0"
}
},
"fs.realpath": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
"integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="
},
"fstream": {
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
"integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
"requires": {
"graceful-fs": "^4.1.2",
"inherits": "~2.0.0",
"mkdirp": ">=0.5 0",
"rimraf": "2"
}
},
"glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
"integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.1.1",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"graceful-fs": {
"version": "4.2.10",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
},
"humanize-plus": {
"version": "1.8.2",
"resolved": "https://registry.npmjs.org/humanize-plus/-/humanize-plus-1.8.2.tgz",
"integrity": "sha512-jaLeQyyzjjINGv7O9JJegjsaUcWjSj/1dcXvLEgU3pGdqCdP1PiC/uwr+saJXhTNBHZtmKnmpXyazgh+eceRxA=="
},
"inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
"integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==",
"requires": {
"once": "^1.3.0",
"wrappy": "1"
}
},
"inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="
},
"ls-archive": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/ls-archive/-/ls-archive-1.3.4.tgz",
"integrity": "sha512-7GmjZOckV+gzm4PM1/LcWIsZIRsSkAVmIchoEf5xjquNKU0Ti5KUvGQ3dl/7VsbZIduMOPwRDXrvpo3LVJ0Pmg==",
"requires": {
"async": "~0.2.9",
"colors": "~0.6.2",
"optimist": "~0.5.2",
"rimraf": "~2.2.6",
"tar": "^2.2.1",
"yauzl": "^2.9.1"
}
},
"minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"requires": {
"brace-expansion": "^1.1.7"
}
},
"minimist": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz",
"integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g=="
},
"mkdirp": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
"requires": {
"minimist": "^1.2.6"
}
},
"once": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
"requires": {
"wrappy": "1"
}
},
"optimist": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.5.2.tgz",
"integrity": "sha512-r9M8ZpnM9SXV5Wii7TCqienfcaY3tAiJe9Jchof87icbmbruKgK0xKXngmrnowTDnEawmmI1Qbha59JEoBkBGA==",
"requires": {
"wordwrap": "~0.0.2"
}
},
"path-is-absolute": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
"integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="
},
"pend": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz",
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="
},
"rimraf": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.2.8.tgz",
"integrity": "sha512-R5KMKHnPAQaZMqLOsyuyUmcIjSeDm+73eoqQpaXA7AZ22BL+6C+1mcUscgOsNd8WVlJuvlgAPsegcx7pjlV0Dg=="
},
"tar": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/tar/-/tar-2.2.2.tgz",
"integrity": "sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA==",
"requires": {
"block-stream": "*",
"fstream": "^1.0.12",
"inherits": "2"
}
},
"temp": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz",
"integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==",
"requires": {
"mkdirp": "^0.5.1",
"rimraf": "~2.6.2"
},
"dependencies": {
"rimraf": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz",
"integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==",
"requires": {
"glob": "^7.1.3"
}
}
}
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw=="
},
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"yauzl": {
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz",
"integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==",
"requires": {
"buffer-crc32": "~0.2.3",
"fd-slicer": "~1.1.0"
}
}
}
}

View File

@ -0,0 +1,33 @@
{
"name": "archive-view",
"version": "0.66.0",
"description": "View the files and folders inside archive files",
"main": "./lib/archive-editor",
"dependencies": {
"etch": "^0.14.1",
"humanize-plus": "~1.8.2",
"ls-archive": "1.3.4",
"temp": "^0.9.4"
},
"repository": "https://github.com/pulsar-edit/pulsar",
"license": "MIT",
"engines": {
"atom": "*"
},
"deserializers": {
"ArchiveEditor": "deserialize",
"ArchiveEditorView": "deserialize"
},
"consumedServices": {
"atom.file-icons": {
"versions": {
"1.0.0": "consumeFileIcons"
}
},
"file-icons.element-icons": {
"versions": {
"1.0.0": "consumeElementIcons"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

View File

@ -0,0 +1,46 @@
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars
const path = require('path')
const ArchiveEditor = require('../lib/archive-editor')
const ArchiveEditorView = require('../lib/archive-editor-view')
describe('ArchiveEditor', () => {
const tarPath = path.join(__dirname, 'fixtures', 'nested.tar')
// Don't log during specs
beforeEach(() => spyOn(console, 'warn'))
describe('.deserialize', () => {
it('returns undefined if no file exists at the given path', () => {
const editor1 = new ArchiveEditorView(tarPath)
const state = editor1.serialize()
editor1.destroy()
const editor2 = ArchiveEditor.deserialize(state)
expect(editor2).toBeDefined()
editor2.destroy()
state.path = 'bogus'
expect(ArchiveEditor.deserialize(state)).toBeUndefined()
})
})
describe('.deactivate()', () => {
it('removes all ArchiveEditorViews from the workspace and does not open any new ones', async () => {
const getArchiveEditorViews = () => {
return atom.workspace.getPaneItems().filter(item => item instanceof ArchiveEditorView)
}
await atom.packages.activatePackage('archive-view')
await atom.workspace.open(path.join(__dirname, 'fixtures', 'nested.tar'))
await atom.workspace.open(path.join(__dirname, 'fixtures', 'invalid.zip'))
await atom.workspace.open()
expect(getArchiveEditorViews().length).toBe(2)
await atom.packages.deactivatePackage('archive-view')
expect(getArchiveEditorViews().length).toBe(0)
await atom.workspace.open(path.join(__dirname, 'fixtures', 'nested.tar'))
expect(getArchiveEditorViews().length).toBe(0)
})
})
})

View File

@ -0,0 +1,276 @@
const {Disposable, File} = require('atom')
const getIconServices = require('../lib/get-icon-services')
const {it, fit, ffit, fffit, beforeEach, afterEach, conditionPromise} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars
async function condition (handler) {
if (jasmine.isSpy(window.setTimeout)) {
jasmine.useRealClock()
}
return conditionPromise(handler)
}
describe('ArchiveEditorView', () => {
let archiveEditorView, onDidChangeCallback, onDidRenameCallback, onDidDeleteCallback
beforeEach(async () => {
spyOn(File.prototype, 'onDidChange').andCallFake(function (callback) {
if (/\.tar$/.test(this.getPath())) {
onDidChangeCallback = callback
}
return new Disposable()
})
spyOn(File.prototype, 'onDidRename').andCallFake(function (callback) {
if (/\.tar$/.test(this.getPath())) {
onDidRenameCallback = callback
}
return new Disposable()
})
spyOn(File.prototype, 'onDidDelete').andCallFake(function (callback) {
if (/\.tar$/.test(this.getPath())) {
onDidDeleteCallback = callback
}
return new Disposable()
})
await atom.packages.activatePackage('archive-view')
archiveEditorView = await atom.workspace.open('nested.tar')
})
describe('.constructor()', () => {
it('displays the files and folders in the archive file', async () => {
expect(archiveEditorView.element).toExist()
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
const directoryElements = archiveEditorView.element.querySelectorAll('.directory')
expect(directoryElements.length).toBe(6)
expect(directoryElements[0].textContent).toBe('d1')
expect(directoryElements[1].textContent).toBe('d2')
expect(directoryElements[2].textContent).toBe('d3')
expect(directoryElements[3].textContent).toBe('d4')
expect(directoryElements[4].textContent).toBe('da')
expect(directoryElements[5].textContent).toBe('db')
const fileElements = archiveEditorView.element.querySelectorAll('.file')
expect(fileElements.length).toBe(3)
expect(fileElements[0].textContent).toBe('f1.txt')
expect(fileElements[1].textContent).toBe('f2.txt')
expect(fileElements[2].textContent).toBe('fa.txt')
})
it('selects the first file', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
expect(archiveEditorView.element.querySelector('.selected').textContent).toBe('f1.txt')
})
})
describe('.copy()', () => {
it('returns a new ArchiveEditorView for the same file', () => {
const newArchiveView = archiveEditorView.copy()
expect(newArchiveView.getPath()).toBe(archiveEditorView.getPath())
})
})
describe('archive summary', () => {
beforeEach(async () => {
await atom.workspace.open('multiple-entries.zip')
archiveEditorView = atom.workspace.getActivePaneItem()
jasmine.attachToDOM(atom.views.getView(atom.workspace))
})
it('shows correct statistics', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
const heading = archiveEditorView.element.querySelector('.inset-panel .panel-heading')
expect(heading).not.toBe(null)
expect(heading.textContent).toBe('704 bytes with 4 files and 1 folder')
})
})
describe('when core:move-up/core:move-down is triggered', () => {
let selectedEntry
const dispatch = (command) => {
atom.commands.dispatch(archiveEditorView.element.querySelector('.selected'), command)
selectedEntry = archiveEditorView.element.querySelector('.selected').textContent
return true
}
it('selects the next/previous file', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
expect(archiveEditorView.element).toBeDefined()
dispatch('core:move-up') && expect(selectedEntry).toBe('f1.txt')
dispatch('core:move-down') && expect(selectedEntry).toBe('f2.txt')
dispatch('core:move-down') && expect(selectedEntry).toBe('fa.txt')
dispatch('core:move-down') && expect(selectedEntry).toBe('fa.txt')
dispatch('core:move-up') && expect(selectedEntry).toBe('f2.txt')
dispatch('core:move-up') && expect(selectedEntry).toBe('f1.txt')
})
})
describe('when a file is clicked', () => {
it('copies the contents to a temp file and opens it in a new editor', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
archiveEditorView.element.querySelectorAll('.file')[2].click()
await condition(() => atom.workspace.getActivePane().getItems().length > 1)
expect(atom.workspace.getActivePaneItem().getText()).toBe('hey there\n')
expect(atom.workspace.getActivePaneItem().getTitle()).toBe('fa.txt')
})
})
describe('when a directory is clicked', () => {
it('collapses/expands itself', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
let directory = archiveEditorView.element.querySelectorAll('.list-nested-item.entry')[0]
expect(directory.classList.contains('collapsed')).toBeFalsy()
directory.querySelector('.list-item').click()
expect(directory.classList.contains('collapsed')).toBeTruthy()
directory.querySelector('.list-item').click()
expect(directory.classList.contains('collapsed')).toBeFalsy()
})
})
describe('when core:confirm is triggered', () => {
it('copies the contents to a temp file and opens it in a new editor', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
atom.commands.dispatch(archiveEditorView.element.querySelector('.file'), 'core:confirm')
await condition(() => atom.workspace.getActivePane().getItems().length > 1)
expect(atom.workspace.getActivePaneItem().getText()).toBe('')
expect(atom.workspace.getActivePaneItem().getTitle()).toBe('f1.txt')
})
})
describe('when the file is modified', () => {
it('refreshes the view', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
spyOn(archiveEditorView, 'refresh')
onDidChangeCallback()
expect(archiveEditorView.refresh).toHaveBeenCalled()
})
})
describe('when the file is renamed', () => {
it('refreshes the view and updates the title', async () => {
spyOn(File.prototype, 'getPath').andReturn('nested-renamed.tar')
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
spyOn(archiveEditorView, 'refresh').andCallThrough()
spyOn(archiveEditorView, 'getTitle')
onDidRenameCallback()
expect(archiveEditorView.refresh).toHaveBeenCalled()
expect(archiveEditorView.getTitle).toHaveBeenCalled()
})
})
describe('when the file is removed', () => {
it('destroys the view', async () => {
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
expect(atom.workspace.getActivePane().getItems().length).toBe(1)
onDidDeleteCallback()
expect(atom.workspace.getActivePaneItem()).toBeUndefined()
})
})
describe('when the file is invalid', () => {
beforeEach(async () => {
await atom.workspace.open('invalid.zip')
archiveEditorView = atom.workspace.getActivePaneItem()
jasmine.attachToDOM(atom.views.getView(atom.workspace))
})
it('shows the error', async () => {
await condition(() => archiveEditorView.refs.errorMessage.offsetHeight > 0)
expect(archiveEditorView.refs.errorMessage.textContent.length).toBeGreaterThan(0)
})
})
describe('FileIcons', () => {
async function openFile () {
await atom.workspace.open('file-icons.zip')
archiveEditorView = atom.workspace.getActivePaneItem()
jasmine.attachToDOM(atom.views.getView(atom.workspace))
}
describe('Icon service', () => {
const service = { iconClassForPath () {} }
beforeEach(() => openFile())
it('provides a default service', () => {
expect(getIconServices().fileIcons).toBeDefined()
expect(getIconServices().fileIcons).not.toBeNull()
})
it('allows the default to be overridden', () => {
getIconServices().setFileIcons(service)
expect(getIconServices().fileIcons).toBe(service)
})
it('allows service to be reset without hassle', () => {
getIconServices().setFileIcons(service)
getIconServices().resetFileIcons()
expect(getIconServices().fileIcons).not.toBe(service)
})
})
describe('Class handling', () => {
function findEntryContainingText (text) {
for (const entry of archiveEditorView.element.querySelectorAll('.list-item.entry')) {
if (entry.textContent.includes(text)) { return entry }
}
return null
}
function checkMultiClass () {
expect(findEntryContainingText('adobe.pdf').querySelector('.file.icon').className).toBe('file icon text pdf-icon document')
expect(findEntryContainingText('spacer.gif').querySelector('.file.icon').className).toBe('file icon binary gif-icon image')
expect(findEntryContainingText('font.ttf').querySelector('.file.icon').className).toBe('file icon binary ttf-icon font')
}
it('displays default file-icons', async () => {
await openFile()
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
expect(findEntryContainingText('adobe.pdf').querySelector('.file.icon.icon-file-pdf').length).not.toBe(0)
expect(findEntryContainingText('spacer.gif').querySelector('.file.icon.icon-file-media').length).not.toBe(0)
expect(findEntryContainingText('sunn.o').querySelector('.file.icon.icon-file-binary').length).not.toBe(0)
})
it('allows multiple classes to be passed', async () => {
getIconServices().setFileIcons({
iconClassForPath: (path) => {
switch (path.match(/\w*$/)[0]) {
case 'pdf': return 'text pdf-icon document'
case 'ttf': return 'binary ttf-icon font'
case 'gif': return 'binary gif-icon image'
}
}
})
await openFile()
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
checkMultiClass()
})
it('allows an array of classes to be passed', async () => {
getIconServices().setFileIcons({
iconClassForPath: (path) => {
switch (path.match(/\w*$/)[0]) {
case 'pdf': return ['text', 'pdf-icon', 'document']
case 'ttf': return ['binary', 'ttf-icon', 'font']
case 'gif': return ['binary', 'gif-icon', 'image']
}
}
})
await openFile()
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
checkMultiClass()
})
it('identifies context to icon-service providers', async () => {
getIconServices().setFileIcons({
iconClassForPath: (path, context) => `icon-${context}`
})
await openFile()
await condition(() => archiveEditorView.element.querySelectorAll('.entry').length > 0)
const icons = findEntryContainingText('adobe.pdf').querySelectorAll('.file.icon-archive-view')
expect(icons.length).not.toBe(0)
})
})
})
})

View File

@ -0,0 +1,103 @@
/** @babel */
export function beforeEach (fn) {
global.beforeEach(function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
export function afterEach (fn) {
global.afterEach(function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
['it', 'fit', 'ffit', 'fffit'].forEach(function (name) {
module.exports[name] = function (description, fn) {
if (fn === undefined) {
global[name](description)
return
}
global[name](description, function () {
const result = fn()
if (result instanceof Promise) {
waitsForPromise(() => result)
}
})
}
})
export async function conditionPromise (condition, description = 'anonymous condition') {
const startTime = Date.now()
while (true) {
await timeoutPromise(100)
if (await condition()) {
return
}
if (Date.now() - startTime > 5000) {
throw new Error('Timed out waiting on ' + description)
}
}
}
export function timeoutPromise (timeout) {
return new Promise(function (resolve) {
global.setTimeout(resolve, timeout)
})
}
function waitsForPromise (fn) {
const promise = fn()
global.waitsFor('spec promise to resolve', function (done) {
promise.then(done, function (error) {
jasmine.getEnv().currentSpec.fail(error)
done()
})
})
}
export function emitterEventPromise (emitter, event, timeout = 15000) {
return new Promise((resolve, reject) => {
const timeoutHandle = setTimeout(() => {
reject(new Error(`Timed out waiting for '${event}' event`))
}, timeout)
emitter.once(event, () => {
clearTimeout(timeoutHandle)
resolve()
})
})
}
export function promisify (original) {
return function (...args) {
return new Promise((resolve, reject) => {
args.push((err, ...results) => {
if (err) {
reject(err)
} else {
resolve(...results)
}
})
return original(...args)
})
}
}
export function promisifySome (obj, fnNames) {
const result = {}
for (const fnName of fnNames) {
result[fnName] = promisify(obj[fnName])
}
return result
}

Binary file not shown.

View File

@ -0,0 +1 @@
invalid

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,24 @@
@import "ui-variables";
.archive-editor {
background-color: @inset-panel-background-color;
overflow: auto;
contain: strict;
.archive-container {
height: 100%;
width: 100%;
.inset-panel {
border-width: 0;
.panel-heading {
border-radius: 0;
}
.archive-tree {
padding: 5px;
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"name": "atom-dark-syntax",
"version": "0.29.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atom-dark-syntax",
"version": "0.29.1",
"license": "MIT",
"engines": {
"atom": ">0.50.0"
}
}
}
}

16
packages/atom-dark-ui/package-lock.json generated Normal file
View File

@ -0,0 +1,16 @@
{
"name": "atom-dark-ui",
"version": "0.53.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atom-dark-ui",
"version": "0.53.3",
"license": "MIT",
"engines": {
"atom": ">0.40.0"
}
}
}
}

View File

@ -0,0 +1,16 @@
{
"name": "atom-light-syntax",
"version": "0.29.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atom-light-syntax",
"version": "0.29.1",
"license": "MIT",
"engines": {
"atom": ">0.40.0"
}
}
}
}

16
packages/atom-light-ui/package-lock.json generated Normal file
View File

@ -0,0 +1,16 @@
{
"name": "atom-light-ui",
"version": "0.46.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "atom-light-ui",
"version": "0.46.3",
"license": "MIT",
"engines": {
"atom": ">0.50.0"
}
}
}
}

View File

@ -0,0 +1 @@
spec/fixtures

3
packages/autocomplete-plus/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
node_modules
.DS_Store
npm-debug.log

View File

@ -0,0 +1,16 @@
pairs:
ns: Nathan Sobo; nathan
cj: Corey Johnson; cj
dg: David Graham; dgraham
ks: Kevin Sawicki; kevin
jc: Jerry Cheung; jerry
bl: Brian Lopez; brian
jp: Justin Palmer; justin
gt: Garen Torikian; garen
mc: Matt Colyer; mcolyer
bo: Ben Ogle; benogle
jr: Jason Rudolph; jasonrudolph
jl: Jessica Lord; jlord
email:
domain: github.com
#global: true

View File

@ -0,0 +1,109 @@
# Autocomplete+ package
Displays possible autocomplete suggestions on keystroke (or manually by typing `ctrl-space`) and inserts a suggestion in the editor if confirmed.
![autocomplete+](https://cloud.githubusercontent.com/assets/744740/7656861/9fb8bcc4-faea-11e4-9814-9dca218ded93.png)
[Changelog](https://github.com/atom/autocomplete-plus/releases)
## Installation
`autocomplete+` is bundled with Pulsar. You don't have to do anything to install it.
## Providers
`autocomplete+` has a powerful autocomplete provider API, allowing provider authors to add language-specific behavior to this package.
You should *definitely* install additional providers (the default provider bundled with this package is somewhat crude): https://github.com/atom/autocomplete-plus/wiki/Autocomplete-Providers
## Usage
Just type some stuff, and autocomplete+ will automatically show you some suggestions.
Press `UP` and `DOWN` to select another suggestion, press `TAB` or `ENTER` to confirm your selection. You can change the default keymap in `Preferences`:
* Keymap For Confirming A Suggestion
Additionally, the confirm keymap can be customized in your keymap.cson:
```coffeescript
'atom-text-editor.autocomplete-active':
'tab': 'unset!'
'ctrl-shift-a': 'autocomplete-plus:confirm'
```
If setting custom keybindings, use the `none` setting for the confirmation keymap. All this option does is not set any other keybindings. This allows the `TAB` and `ENTER` keys to be used like normal, without side effects.
### Remapping Movement Commands
By default, autocomplete-plus commandeers the editor's core movement commands when the suggestion list is open. You may want to change these movement commands to use your own keybindings.
First you need to set the `autocomplete-plus.useCoreMovementCommands` setting to `false`, which you can do from the `autocomplete-plus` settings in the settings view.
![core-movement](https://cloud.githubusercontent.com/assets/69169/8839134/72a9c7e6-3087-11e5-9d1f-8d3d15961327.jpg)
Or by adding this to your config file:
```coffee
"*":
"autocomplete-plus":
"useCoreMovementCommands": false
```
Then add these to your keymap file:
```coffeescript
'body atom-text-editor.autocomplete-active':
'ctrl-p': 'autocomplete-plus:move-up'
'ctrl-n': 'autocomplete-plus:move-down'
'pageup': 'autocomplete-plus:page-up'
'pagedown': 'autocomplete-plus:page-down'
'home': 'autocomplete-plus:move-to-top'
'end': 'autocomplete-plus:move-to-bottom'
```
## Features
* Shows suggestions while typing
* Includes a default provider (`SymbolProvider`):
* Wordlist generation happens when you open a file, while editing the file, and on save
* Suggestions are calculated using `fuzzaldrin`
* Exposes a provider API which can be used to extend the functionality of the package and provide targeted / contextually correct suggestions
* Disable autocomplete for file(s) via blacklisting, e.g. `*.md` to blacklist Markdown files
* Disable autocomplete for editor scope(s) via blacklisting
* Expands a snippet if an autocomplete+ provider includes one in a suggestion
* Allows external editors to register for autocompletions
## Provider API
Great autocomplete depends on having great autocomplete providers. If there is not already a great provider for the language / grammar that you are working in, please consider creating a provider.
[Read the `Provider API` documentation](https://github.com/atom/autocomplete-plus/wiki/Provider-API) to learn how to create a new autocomplete provider.
## `SymbolProvider` Configuration
If the default `SymbolProvider` is missing useful information for the language / grammar you're working with, please take a look at the [`SymbolProvider` Config API](https://github.com/atom/autocomplete-plus/wiki/SymbolProvider-Config-API).
## The `watchEditor` API
The `watchEditor` method on the `AutocompleteManager` object is exposed as a [provided service](http://flight-manual.atom.io/behind-atom/sections/interacting-with-other-packages-via-services/), named `autocomplete.watchEditor`. The method allows external editors to register for autocompletions from providers with a given set of labels. Disposing the returned object will undo this request. External packages can access this service with the following code.
In `package.json`:
```
{
"consumedServices": {
"autocomplete.watchEditor": {
"versions": {
"1.0.0": "consumeAutocompleteWatchEditor"
}
}
}
}
```
In the main module file:
```
consumeAutocompleteWatchEditor(watchEditor) {
this.autocompleteDisposable = watchEditor(
this.editor, ['symbol-provider']
)
}
```

View File

@ -0,0 +1,37 @@
{
"max_line_length": {
"level": "ignore"
},
"no_empty_param_list": {
"level": "error"
},
"arrow_spacing": {
"level": "error"
},
"no_interpolation_in_single_quotes": {
"level": "error"
},
"no_debugger": {
"level": "error"
},
"prefer_english_operator": {
"level": "error"
},
"colon_assignment_spacing": {
"spacing": {
"left": 0,
"right": 1
},
"level": "error"
},
"braces_spacing": {
"spaces": 0,
"level": "error"
},
"spacing_after_comma": {
"level": "error"
},
"no_stand_alone_at": {
"level": "error"
}
}

View File

@ -0,0 +1,6 @@
'atom-text-editor':
'ctrl-space': 'autocomplete-plus:activate'
'atom-text-editor.autocomplete-active':
'escape': 'autocomplete-plus:cancel'
'f1': 'autocomplete-plus:navigate-to-description-more-link'

View File

@ -0,0 +1,799 @@
const {CompositeDisposable, Disposable, Point, Range} = require('atom')
const path = require('path')
const fuzzaldrin = require('fuzzaldrin')
const fuzzaldrinPlus = require('fuzzaldrin-plus')
const ProviderManager = require('./provider-manager')
const SuggestionList = require('./suggestion-list')
const {UnicodeLetters} = require('./unicode-helpers')
const getAdditionalWordCharacters = require('./get-additional-word-characters')
const MAX_LEGACY_PREFIX_LENGTH = 80
const wordCharacterRegexCache = new Map()
// Deferred requires
let minimatch = null
let grim = null
module.exports =
class AutocompleteManager {
constructor () {
this.autosaveEnabled = false
this.backspaceTriggersAutocomplete = true
this.autoConfirmSingleSuggestionEnabled = true
this.bracketMatcherPairs = ['()', '[]', '{}', '""', "''", '``', '“”', '', '«»', '']
this.buffer = null
this.compositionInProgress = false
this.disposed = false
this.editor = null
this.editorLabels = null
this.editorSubscriptions = null
this.editorView = null
this.providerManager = null
this.ready = false
this.subscriptions = null
this.suggestionList = null
this.suppressForClasses = []
this.shouldDisplaySuggestions = false
this.prefixRegex = null
this.wordPrefixRegex = null
this.updateCurrentEditor = this.updateCurrentEditor.bind(this)
this.handleCommands = this.handleCommands.bind(this)
this.findSuggestions = this.findSuggestions.bind(this)
this.getSuggestionsFromProviders = this.getSuggestionsFromProviders.bind(this)
this.displaySuggestions = this.displaySuggestions.bind(this)
this.hideSuggestionList = this.hideSuggestionList.bind(this)
this.showOrHideSuggestionListForBufferChanges = this.showOrHideSuggestionListForBufferChanges.bind(this)
this.providerManager = new ProviderManager()
this.suggestionList = new SuggestionList()
this.watchedEditors = new WeakSet()
}
initialize () {
this.subscriptions = new CompositeDisposable()
this.providerManager.initialize()
this.suggestionList.initialize()
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableExtendedUnicodeSupport', enableExtendedUnicodeSupport => {
if (enableExtendedUnicodeSupport) {
this.prefixRegex = new RegExp(`(['"~\`!@#\\$%^&*\\(\\)\\{\\}\\[\\]=+,/\\?>])?(([${UnicodeLetters}\\d_]+[${UnicodeLetters}\\d_-]*)|([.:;[{(< ]+))$`)
this.wordPrefixRegex = new RegExp(`^[${UnicodeLetters}\\d_]+[${UnicodeLetters}\\d_-]*$`)
} else {
this.prefixRegex = /(\b|['"~`!@#$%^&*(){}[\]=+,/?>])((\w+[\w-]*)|([.:;[{(< ]+))$/
this.wordPrefixRegex = /^\w+[\w-]*$/
}
}
))
this.subscriptions.add(this.providerManager)
this.handleEvents()
this.handleCommands()
this.subscriptions.add(this.suggestionList) // We're adding this last so it is disposed after events
this.ready = true
}
setSnippetsManager (snippetsManager) {
this.snippetsManager = snippetsManager
}
updateCurrentEditor (currentEditor, labels) {
if (currentEditor === this.editor) { return }
if (this.editorSubscriptions) {
this.editorSubscriptions.dispose()
}
this.editorSubscriptions = null
// Stop tracking editor + buffer
this.editor = null
this.editorView = null
this.buffer = null
this.isCurrentFileBlackListedCache = null
if (!this.editorIsValid(currentEditor)) { return }
// Track the new editor, editorView, and buffer and set
// the labels for its providers.
this.editor = currentEditor
this.editorLabels = labels
this.editorView = atom.views.getView(this.editor)
this.buffer = this.editor.getBuffer()
this.editorSubscriptions = new CompositeDisposable()
// Subscribe to buffer events:
this.editorSubscriptions.add(this.buffer.onDidSave((e) => { this.bufferSaved(e) }))
this.editorSubscriptions.add(this.buffer.onDidChangeText(this.showOrHideSuggestionListForBufferChanges))
// Watch IME Events To Allow IME To Function Without The Suggestion List Showing
const compositionStart = () => {
this.compositionInProgress = true
}
const compositionEnd = () => {
this.compositionInProgress = false
}
this.editorView.addEventListener('compositionstart', compositionStart)
this.editorView.addEventListener('compositionend', compositionEnd)
this.editorSubscriptions.add(new Disposable(() => {
if (this.editorView) {
this.editorView.removeEventListener('compositionstart', compositionStart)
this.editorView.removeEventListener('compositionend', compositionEnd)
}
}))
// Subscribe to editor events:
// Close the overlay when the cursor moved without changing any text
this.editorSubscriptions.add(this.editor.onDidChangeCursorPosition((e) => { this.cursorMoved(e) }))
return this.editorSubscriptions.add(this.editor.onDidChangePath(() => {
this.isCurrentFileBlackListedCache = null
}))
}
editorIsValid (editor) {
// TODO: remove conditional when `isTextEditor` is shipped.
if (typeof atom.workspace.isTextEditor === 'function') {
return atom.workspace.isTextEditor(editor)
} else {
if (!editor) { return false }
// Should we disqualify TextEditors with the Grammar text.plain.null-grammar?
return (editor.getText != null)
}
}
// Makes the autocomplete manager watch the `editor`.
// When the watched `editor` is focused, it will provide autocompletions from
// providers with the given `labels`.
//
// Returns a {Disposable} to stop watching the `editor`.
watchEditor (editor, labels) {
if (this.watchedEditors.has(editor)) return
let view = atom.views.getView(editor)
if (view.hasFocus()) {
this.updateCurrentEditor(editor, labels)
}
let focusListener = (element) => this.updateCurrentEditor(editor, labels)
view.addEventListener('focus', focusListener)
let blurListener = (element) => this.hideSuggestionList()
view.addEventListener('blur', blurListener)
let disposable = new Disposable(() => {
view.removeEventListener('focus', focusListener)
view.removeEventListener('blur', blurListener)
if (this.editor === editor) {
this.updateCurrentEditor(null)
}
})
this.watchedEditors.add(editor)
this.subscriptions.add(disposable)
return new Disposable(() => {
disposable.dispose()
if (this.subscriptions != null) {
this.subscriptions.remove(disposable)
}
this.watchedEditors.delete(editor)
})
}
handleEvents () {
this.subscriptions.add(atom.workspace.observeTextEditors((editor) => {
const disposable = this.watchEditor(editor, ['workspace-center'])
editor.onDidDestroy(() => disposable.dispose())
}))
// Watch config values
this.subscriptions.add(atom.config.observe('autosave.enabled', (value) => { this.autosaveEnabled = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.backspaceTriggersAutocomplete', (value) => { this.backspaceTriggersAutocomplete = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableAutoActivation', (value) => { this.autoActivationEnabled = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableAutoConfirmSingleSuggestion', (value) => { this.autoConfirmSingleSuggestionEnabled = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.consumeSuffix', (value) => { this.consumeSuffix = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.useAlternateScoring', (value) => { this.useAlternateScoring = value }))
this.subscriptions.add(atom.config.observe('autocomplete-plus.fileBlacklist', (value) => {
if (value) {
this.fileBlacklist = value.map((s) => { return s.trim() })
}
this.isCurrentFileBlackListedCache = null
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.suppressActivationForEditorClasses', value => {
this.suppressForClasses = []
for (let i = 0; i < value.length; i++) {
const selector = value[i]
const classes = (selector.trim().split('.').filter((className) => className.trim()).map((className) => className.trim()))
if (classes.length) { this.suppressForClasses.push(classes) }
}
}))
// Handle events from suggestion list
this.subscriptions.add(this.suggestionList.onDidConfirm((e) => { this.confirm(e) }))
this.subscriptions.add(this.suggestionList.onDidCancel(this.hideSuggestionList))
this.subscriptions.add(this.suggestionList.onDidSelect(suggestion => { this.getDetailsOnSelect(suggestion) }))
}
handleCommands () {
return this.subscriptions.add(atom.commands.add('atom-text-editor', {
'autocomplete-plus:activate': (event) => {
this.shouldDisplaySuggestions = true
let activatedManually = true
if (event.detail && event.detail.activatedManually !== null && typeof event.detail.activatedManually !== 'undefined') {
activatedManually = event.detail.activatedManually
}
this.findSuggestions(activatedManually)
},
'autocomplete-plus:navigate-to-description-more-link': () => {
let suggestionListView = atom.views.getView(this.editor)
let descriptionContainer = suggestionListView.querySelector('.suggestion-description')
if (descriptionContainer !== null && descriptionContainer.style.display === 'block') {
let descriptionMoreLink = descriptionContainer.querySelector('.suggestion-description-more-link')
require('electron').shell.openExternal(descriptionMoreLink.href)
}
}
}))
}
// Private: Finds suggestions for the current prefix, sets the list items,
// positions the overlay and shows it
findSuggestions (activatedManually) {
if (this.disposed) { return }
if ((this.providerManager == null) || (this.editor == null) || (this.buffer == null)) { return }
if (this.isCurrentFileBlackListed()) { return }
const cursor = this.editor.getLastCursor()
if (cursor == null) { return }
const bufferPosition = cursor.getBufferPosition()
const scopeDescriptor = cursor.getScopeDescriptor()
const prefix = this.getPrefix(this.editor, bufferPosition, scopeDescriptor) // Passed to providers with API version >= 4.0.0
const legacyPrefix = this.getLegacyPrefix(this.editor, bufferPosition) // Passed to providers with API version < 4.0.0
return this.getSuggestionsFromProviders({editor: this.editor, bufferPosition, scopeDescriptor, prefix, legacyPrefix, activatedManually})
}
getSuggestionsFromProviders (options) {
let suggestionsPromise
const providers = this.providerManager.applicableProviders(this.editorLabels, options.scopeDescriptor)
const providerPromises = []
providers.forEach(provider => {
const apiVersion = this.providerManager.apiVersionForProvider(provider)
let getSuggestions
let upgradedOptions
if (apiVersion === 1) {
getSuggestions = provider.requestHandler.bind(provider)
upgradedOptions = {
editor: options.editor,
prefix: options.prefix,
bufferPosition: options.bufferPosition,
position: options.bufferPosition,
scope: options.scopeDescriptor,
scopeChain: options.scopeDescriptor.getScopeChain(),
buffer: options.editor.getBuffer(),
cursor: options.editor.getLastCursor()
}
} else {
getSuggestions = provider.getSuggestions.bind(provider)
if (apiVersion < 4) {
upgradedOptions = Object.assign({}, options)
upgradedOptions.prefix = options.legacyPrefix
} else {
upgradedOptions = Object.assign({}, options)
delete upgradedOptions.legacyPrefix
}
}
return providerPromises.push(Promise.resolve(getSuggestions(upgradedOptions)).then(providerSuggestions => {
if (providerSuggestions == null) { return }
// TODO API: remove upgrading when 1.0 support is removed
let hasDeprecations = false
if (apiVersion > 1 && providerSuggestions.length) {
hasDeprecations = this.deprecateForSuggestion(provider, providerSuggestions[0])
}
if (hasDeprecations || apiVersion === 1) {
providerSuggestions = providerSuggestions.map((suggestion) => {
const newSuggestion = {
text: suggestion.text != null ? suggestion.text : suggestion.word,
snippet: suggestion.snippet,
replacementPrefix: suggestion.replacementPrefix != null ? suggestion.replacementPrefix : suggestion.prefix,
className: suggestion.className,
type: suggestion.type
}
if ((newSuggestion.rightLabelHTML == null) && suggestion.renderLabelAsHtml) { newSuggestion.rightLabelHTML = suggestion.label }
if ((newSuggestion.rightLabel == null) && !suggestion.renderLabelAsHtml) { newSuggestion.rightLabel = suggestion.label }
return newSuggestion
})
}
let hasEmpty = false // Optimization: only create another array when there are empty items
for (let i = 0; i < providerSuggestions.length; i++) {
const suggestion = providerSuggestions[i]
if (!suggestion.snippet && !suggestion.text) { hasEmpty = true }
// Suggestions are mutable and are updated with a new replacement prefix. In order to
// distinguish between suggestion that had original prefix and assigned one, we use
// `isPrefixModified` flag. If it is `true`, we reset replacement prefix.
if (suggestion.replacementPrefix == null || !!suggestion.isPrefixModified) {
if (apiVersion < 4) {
suggestion.replacementPrefix = this.wordPrefixRegex.test(options.prefix) ? options.prefix : ''
} else {
suggestion.replacementPrefix = options.prefix
}
suggestion.isPrefixModified = true
}
suggestion.provider = provider
}
if (hasEmpty) {
const res = []
for (const s of providerSuggestions) {
if (s.snippet || s.text) {
res.push(s)
}
}
providerSuggestions = res
}
if (provider.filterSuggestions) {
providerSuggestions = this.filterSuggestions(providerSuggestions, options)
}
return providerSuggestions
}))
})
if (!providerPromises || !providerPromises.length) {
return
}
suggestionsPromise = Promise.all(providerPromises)
this.currentSuggestionsPromise = suggestionsPromise
return this.currentSuggestionsPromise
.then(this.mergeSuggestionsFromProviders)
.then(suggestions => {
if (this.currentSuggestionsPromise !== suggestionsPromise) { return }
if (options.activatedManually && this.shouldDisplaySuggestions && this.autoConfirmSingleSuggestionEnabled && suggestions.length === 1) {
// When there is one suggestion in manual mode, just confirm it
return this.confirm(suggestions[0])
} else {
return this.displaySuggestions(suggestions, options)
}
}
)
}
filterSuggestions (suggestions, {prefix}) {
const results = []
const fuzzaldrinProvider = this.useAlternateScoring ? fuzzaldrinPlus : fuzzaldrin
for (let i = 0; i < suggestions.length; i++) {
// sortScore mostly preserves in the original sorting. The function is
// chosen such that suggestions with a very high match score can break out.
let score
const suggestion = suggestions[i]
suggestion.sortScore = Math.max((-i / 10) + 3, 0) + 1
suggestion.score = null
const text = (suggestion.snippet || suggestion.text)
const suggestionPrefix = suggestion.replacementPrefix != null ? suggestion.replacementPrefix : prefix
const prefixIsEmpty = !suggestionPrefix || suggestionPrefix === ' '
const firstCharIsMatch = !prefixIsEmpty && suggestionPrefix[0].toLowerCase() === text[0].toLowerCase()
if (prefixIsEmpty) {
results.push(suggestion)
}
if (firstCharIsMatch && (score = fuzzaldrinProvider.score(text, suggestionPrefix)) > 0) {
suggestion.score = score * suggestion.sortScore
results.push(suggestion)
}
}
results.sort(this.reverseSortOnScoreComparator)
return results
}
reverseSortOnScoreComparator (a, b) {
let bscore = b.score
if (!bscore) {
bscore = b.sortScore
}
let ascore = a.score
if (!ascore) {
ascore = b.sortScore
}
return bscore - ascore
}
// providerSuggestions - array of arrays of suggestions provided by all called providers
mergeSuggestionsFromProviders (providerSuggestions) {
return providerSuggestions.reduce((suggestions, providerSuggestions) => {
if (providerSuggestions && providerSuggestions.length) {
suggestions = suggestions.concat(providerSuggestions)
}
return suggestions
}, [])
}
deprecateForSuggestion (provider, suggestion) {
let hasDeprecations = false
if (suggestion.word != null) {
hasDeprecations = true
if (typeof grim === 'undefined' || grim === null) { grim = require('grim') }
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
returns suggestions with a \`word\` attribute.
The \`word\` attribute is now \`text\`.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (suggestion.prefix != null) {
hasDeprecations = true
if (typeof grim === 'undefined' || grim === null) { grim = require('grim') }
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
returns suggestions with a \`prefix\` attribute.
The \`prefix\` attribute is now \`replacementPrefix\` and is optional.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (suggestion.label != null) {
hasDeprecations = true
if (typeof grim === 'undefined' || grim === null) { grim = require('grim') }
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
returns suggestions with a \`label\` attribute.
The \`label\` attribute is now \`rightLabel\` or \`rightLabelHTML\`.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (suggestion.onWillConfirm != null) {
hasDeprecations = true
if (typeof grim === 'undefined' || grim === null) { grim = require('grim') }
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
returns suggestions with a \`onWillConfirm\` callback.
The \`onWillConfirm\` callback is no longer supported.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (suggestion.onDidConfirm != null) {
hasDeprecations = true
if (typeof grim === 'undefined' || grim === null) { grim = require('grim') }
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
returns suggestions with a \`onDidConfirm\` callback.
The \`onDidConfirm\` callback is now a \`onDidInsertSuggestion\` callback on the provider itself.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
return hasDeprecations
}
displaySuggestions (suggestions, options) {
switch (atom.config.get('autocomplete-plus.similarSuggestionRemoval')) {
case 'textOrSnippet': {
suggestions = this.getUniqueSuggestions(suggestions, (suggestion) => suggestion.text + suggestion.snippet)
break
}
}
if (this.shouldDisplaySuggestions && suggestions.length) {
return this.showSuggestionList(suggestions, options)
} else {
return this.hideSuggestionList()
}
}
getUniqueSuggestions (suggestions, uniqueKeyFunction) {
const seen = {}
const result = []
for (let i = 0; i < suggestions.length; i++) {
const suggestion = suggestions[i]
const val = uniqueKeyFunction(suggestion)
if (!seen[val]) {
result.push(suggestion)
seen[val] = true
}
}
return result
}
getPrefix (editor, position, scopeDescriptor) {
const wordCharacterRegex = this.getWordCharacterRegex(scopeDescriptor)
const line = editor.getBuffer().getTextInRange([[position.row, 0], position])
let startColumn = position.column
while (startColumn > 0) {
let prevChar = line[startColumn - 1]
if (wordCharacterRegex.test(prevChar)) {
startColumn--
} else {
break
}
}
return line.slice(startColumn)
}
getWordCharacterRegex (scopeDescriptor) {
const additionalWordChars = getAdditionalWordCharacters(scopeDescriptor)
let regex = wordCharacterRegexCache.get(additionalWordChars)
if (!regex) {
regex = new RegExp(`[${UnicodeLetters}${additionalWordChars.replace(']', '\\]')}\\d]`)
wordCharacterRegexCache.set(additionalWordChars, regex)
}
return regex
}
getLegacyPrefix (editor, bufferPosition) {
const {row, column} = bufferPosition
const line = editor.getTextInRange(new Range(
new Point(row, Math.max(0, column - MAX_LEGACY_PREFIX_LENGTH)),
bufferPosition
))
const prefix = this.prefixRegex.exec(line)
if (!prefix || !prefix[2] || prefix[2].length === MAX_LEGACY_PREFIX_LENGTH) return ''
return prefix[2]
}
// Private: Gets called when the user successfully confirms a suggestion
//
// match - An {Object} representing the confirmed suggestion
confirm (suggestion) {
if ((this.editor == null) || (suggestion == null) || !!this.disposed) { return }
const apiVersion = this.providerManager.apiVersionForProvider(suggestion.provider)
const triggerPosition = this.editor.getLastCursor().getBufferPosition()
// TODO API: Remove as this is no longer used
if (suggestion.onWillConfirm) {
suggestion.onWillConfirm()
}
const selections = this.editor.getSelections()
if (selections && selections.length) {
for (const s of selections) {
if (s && s.clear) {
s.clear()
}
}
}
this.hideSuggestionList()
this.replaceTextWithMatch(suggestion)
if (apiVersion > 1) {
if (suggestion.provider && suggestion.provider.onDidInsertSuggestion) {
suggestion.provider.onDidInsertSuggestion({editor: this.editor, suggestion, triggerPosition})
}
} else {
if (suggestion.onDidConfirm) {
suggestion.onDidConfirm()
}
}
}
getDetailsOnSelect (suggestion) {
if (suggestion != null && suggestion.provider && suggestion.provider.getSuggestionDetailsOnSelect) {
Promise.resolve(suggestion.provider.getSuggestionDetailsOnSelect(suggestion))
.then(detailedSuggestion => {
this.suggestionList.replaceItem(suggestion, detailedSuggestion)
})
}
}
showSuggestionList (suggestions, options) {
if (this.disposed) { return }
this.suggestionList.changeItems(suggestions)
return this.suggestionList.show(this.editor, options)
}
hideSuggestionList () {
if (this.disposed) { return }
this.suggestionList.changeItems(null)
this.suggestionList.hide()
this.shouldDisplaySuggestions = false
}
requestHideSuggestionList (command) {
if (this.hideTimeout == null) {
this.hideTimeout = setTimeout(() => {
this.hideSuggestionList()
this.hideTimeout = null
}, 0)
}
this.shouldDisplaySuggestions = false
}
cancelHideSuggestionListRequest () {
clearTimeout(this.hideTimeout)
this.hideTimeout = null
}
// Private: Replaces the current prefix with the given match.
//
// match - The match to replace the current prefix with
replaceTextWithMatch (suggestion) {
if (this.editor == null) { return }
const cursors = this.editor.getCursors()
if (cursors == null) { return }
return this.editor.transact(() => {
for (let i = 0; i < cursors.length; i++) {
const cursor = cursors[i]
const endPosition = cursor.getBufferPosition()
const beginningPosition = [endPosition.row, endPosition.column - suggestion.replacementPrefix.length]
if (this.editor.getTextInBufferRange([beginningPosition, endPosition]) === suggestion.replacementPrefix) {
const suffix = this.consumeSuffix ? this.getSuffix(this.editor, endPosition, suggestion) : ''
if (suffix.length) { cursor.moveRight(suffix.length) }
cursor.selection.selectLeft(suggestion.replacementPrefix.length + suffix.length)
if ((suggestion.snippet != null) && (this.snippetsManager != null)) {
this.snippetsManager.insertSnippet(suggestion.snippet, this.editor, cursor)
} else {
cursor.selection.insertText(suggestion.text != null ? suggestion.text : suggestion.snippet, {
autoIndentNewline: this.editor.shouldAutoIndent(),
autoDecreaseIndent: this.editor.shouldAutoIndent()
})
}
}
}
}
)
}
getSuffix (editor, bufferPosition, suggestion) {
// This just chews through the suggestion and tries to match the suggestion
// substring with the lineText starting at the cursor. There is probably a
// more efficient way to do this.
let suffix = (suggestion.snippet != null ? suggestion.snippet : suggestion.text)
const endPosition = [bufferPosition.row, bufferPosition.column + suffix.length]
const endOfLineText = editor.getTextInBufferRange([bufferPosition, endPosition])
const nonWordCharacters = new Set(atom.config.get('editor.nonWordCharacters').split(''))
while (suffix) {
if (endOfLineText.startsWith(suffix) && !nonWordCharacters.has(suffix[0])) { break }
suffix = suffix.slice(1)
}
return suffix
}
// Private: Checks whether the current file is blacklisted.
//
// Returns {Boolean} that defines whether the current file is blacklisted
isCurrentFileBlackListed () {
// minimatch is slow. Not necessary to do this computation on every request for suggestions
let left
if (this.isCurrentFileBlackListedCache != null) { return this.isCurrentFileBlackListedCache }
if ((this.fileBlacklist == null) || this.fileBlacklist.length === 0) {
this.isCurrentFileBlackListedCache = false
return this.isCurrentFileBlackListedCache
}
if (typeof minimatch === 'undefined' || minimatch === null) { minimatch = require('minimatch') }
const fileName = path.basename((left = this.buffer.getPath()) != null ? left : '')
for (let i = 0; i < this.fileBlacklist.length; i++) {
const blacklistGlob = this.fileBlacklist[i]
if (minimatch(fileName, blacklistGlob)) {
this.isCurrentFileBlackListedCache = true
return this.isCurrentFileBlackListedCache
}
}
this.isCurrentFileBlackListedCache = false
return this.isCurrentFileBlackListedCache
}
// Private: Gets called when the content has been modified
requestNewSuggestions () {
let delay = atom.config.get('autocomplete-plus.autoActivationDelay')
if (this.delayTimeout != null) {
clearTimeout(this.delayTimeout)
}
if (delay) {
this.delayTimeout = setTimeout(this.findSuggestions, delay)
} else {
this.findSuggestions()
}
this.shouldDisplaySuggestions = true
}
cancelNewSuggestionsRequest () {
if (this.delayTimeout != null) {
clearTimeout(this.delayTimeout)
}
this.shouldDisplaySuggestions = false
}
// Private: Gets called when the cursor has moved. Cancels the autocompletion if
// the text has not been changed.
//
// data - An {Object} containing information on why the cursor has been moved
cursorMoved ({textChanged}) {
// The delay is a workaround for the backspace case. The way atom implements
// backspace is to select left 1 char, then delete. This results in a
// cursorMoved event with textChanged == false. So we delay, and if the
// bufferChanged handler decides to show suggestions, it will cancel the
// hideSuggestionList request. If there is no bufferChanged event,
// suggestionList will be hidden.
if (!textChanged) this.requestHideSuggestionList()
}
// Private: Gets called when the user saves the document. Cancels the
// autocompletion.
bufferSaved () {
if (!this.autosaveEnabled) { return this.hideSuggestionList() }
}
showOrHideSuggestionListForBufferChanges ({changes}) {
if (this.disposed) return
const lastCursorPosition = this.editor.getLastCursor().getBufferPosition()
const changeOccurredNearLastCursor = changes.some(({newRange}) => {
return newRange.containsPoint(lastCursorPosition)
})
if (!changeOccurredNearLastCursor) return
let shouldActivate = false
if (this.autoActivationEnabled || this.suggestionList.isActive() && !this.compositionInProgress) {
shouldActivate = changes.some(({oldText, newText}) => {
if (this.autoActivationEnabled || this.suggestionList.isActive()) {
if (newText.length > 0) {
// Activate on space, a non-whitespace character, or a bracket-matcher pair.
if (newText === ' ' ||
newText.trim().length === 1 ||
(newText.length === 2 && this.bracketMatcherPairs.includes(newText))) return true
} else if (oldText.length > 0 && (this.backspaceTriggersAutocomplete || this.suggestionList.isActive())) {
// Suggestion list must be either active or backspaceTriggersAutocomplete must be true for activation to occur.
// Activate on removal of a space, a non-whitespace character, or a bracket-matcher pair.
if (oldText === ' ' ||
oldText.trim().length === 1 ||
(oldText.length === 2 && this.bracketMatcherPairs.includes(oldText))) return true
}
}
})
if (shouldActivate && this.shouldSuppressActivationForEditorClasses()) shouldActivate = false
}
if (shouldActivate) {
this.cancelHideSuggestionListRequest()
this.requestNewSuggestions()
} else {
this.cancelNewSuggestionsRequest()
this.hideSuggestionList()
}
}
shouldSuppressActivationForEditorClasses () {
for (let i = 0; i < this.suppressForClasses.length; i++) {
const classNames = this.suppressForClasses[i]
let containsCount = 0
for (let j = 0; j < classNames.length; j++) {
const className = classNames[j]
if (this.editorView.classList.contains(className)) { containsCount += 1 }
}
if (containsCount === classNames.length) { return true }
}
return false
}
// Public: Clean up, stop listening to events
dispose () {
this.hideSuggestionList()
this.disposed = true
this.ready = false
if (this.editorSubscriptions) {
this.editorSubscriptions.dispose()
}
this.editorSubscriptions = null
if (this.subscriptions) {
this.subscriptions.dispose()
}
this.subscriptions = null
this.suggestionList = null
this.providerManager = null
}
}

View File

@ -0,0 +1,13 @@
const POSSIBLE_WORD_CHARACTERS = '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?_-…'.split('')
module.exports =
function getAdditionalWordCharacters (scopeDescriptor) {
const nonWordCharacters = atom.config.get('editor.nonWordCharacters', {scope: scopeDescriptor})
let result = atom.config.get('autocomplete-plus.extraWordCharacters', {scope: scopeDescriptor})
POSSIBLE_WORD_CHARACTERS.forEach(character => {
if (!nonWordCharacters.includes(character)) {
result += character
}
})
return result
}

View File

@ -0,0 +1,88 @@
const {CompositeDisposable} = require('atom')
const AutocompleteManager = require('./autocomplete-manager')
module.exports = {
subscriptions: null,
autocompleteManager: new AutocompleteManager(),
// Public: Creates AutocompleteManager instances for all active and future editors (soon, just a single AutocompleteManager)
activate () {
this.subscriptions = new CompositeDisposable()
if (!this.autocompleteManager) this.autocompleteManager = new AutocompleteManager()
this.subscriptions.add(this.autocompleteManager)
this.autocompleteManager.initialize()
},
// Public: Cleans everything up, removes all AutocompleteManager instances
deactivate () {
if (this.subscriptions) {
this.subscriptions.dispose()
}
this.subscriptions = null
this.autocompleteManager = null
},
provideWatchEditor () {
return this.autocompleteManager.watchEditor.bind(this.autocompleteManager)
},
consumeSnippets (snippetsManager) {
this.autocompleteManager.setSnippetsManager(snippetsManager)
},
/*
Section: Provider API
*/
// 1.0.0 API
// service - {provider: provider1}
consumeProvider_1 (service) {
if (!service || !service.provider) {
return
}
return this.consumeProvider([service.provider], 1)
},
// 1.1.0 API
// service - {providers: [provider1, provider2, ...]}
consumeProvider_1_1 (service) {
if (!service || !service.providers) {
return
}
return this.consumeProvider(service.providers, 1)
},
// 2.0.0 API
consumeProvider_2 (providers) {
return this.consumeProvider(providers, 2)
},
// 3.0.0 API
consumeProvider_3 (providers) {
return this.consumeProvider(providers, 3)
},
// 4.0.0 API Simplifies prefix computation
consumeProvider_4 (providers) {
return this.consumeProvider(providers, 4)
},
consumeProvider (providers, apiVersion = 3) {
if (!providers) {
return
}
if (providers && !Array.isArray(providers)) {
providers = [providers]
}
if (!providers.length > 0) {
return
}
const registrations = new CompositeDisposable()
for (let i = 0; i < providers.length; i++) {
const provider = providers[i]
registrations.add(this.autocompleteManager.providerManager.registerProvider(provider, apiVersion))
}
return registrations
}
}

View File

@ -0,0 +1,4 @@
'use babel'
const API_VERSION = Symbol('Private property: Semantic version of the service endpoint.')
export {API_VERSION}

View File

@ -0,0 +1,164 @@
const { Selector } = require('selector-kit')
const {selectorsMatchScopeChain} = require('./scope-helpers')
module.exports =
class ProviderConfig {
constructor (options = {}) {
this.atomConfig = options.atomConfig || atom.config
this.config = {}
this.scopesToTypeMap = {}
this.defaultConfig = {
class: {
selector: '.class.name, .inherited-class, .instance.type',
typePriority: 4
},
function: {
selector: '.function.name',
typePriority: 3
},
variable: {
selector: '.variable',
typePriority: 2
},
'': {
selector: '.source',
typePriority: 1
}
}
this.currentScopeDescriptor = null
}
getSuggestionsForScopeDescriptor (scopeDescriptor) {
this.buildConfigIfScopeChanged(scopeDescriptor)
const suggestions = []
for (const type of Object.keys(this.config)) {
if (this.config[type].suggestions) {
for (let k = 0; k < this.config[type].suggestions.length; k++) {
suggestions.push(this.config[type].suggestions[k])
}
}
}
return suggestions
}
scopeDescriptorToType (scopeDescriptor) {
const scopeChain = '.' + scopeDescriptor.scopes.join(' .')
if (this.scopesToTypeMap[scopeChain] !== undefined) {
return this.scopesToTypeMap[scopeChain]
}
this.buildConfigIfScopeChanged(scopeDescriptor)
let matchingType = null
let highestTypePriority = -1
const config = this.config
for (const type of Object.keys(config)) {
let {selectors, typePriority} = config[type]
if (selectors == null) continue
if (typePriority == null) typePriority = 0
if (typePriority > highestTypePriority &&
selectorsMatchScopeChain(selectors, scopeChain)) {
matchingType = type
highestTypePriority = typePriority
}
}
this.scopesToTypeMap[scopeChain] = matchingType
return matchingType
}
buildConfigIfScopeChanged (scopeDescriptor) {
if (
this.currentScopeDescriptor == null ||
!this.currentScopeDescriptor.isEqual(scopeDescriptor)
) {
this.buildConfig(scopeDescriptor)
this.currentScopeDescriptor = scopeDescriptor
return this.currentScopeDescriptor
}
}
buildConfig (scopeDescriptor) {
this.config = {}
const legacyCompletions = this.settingsForScopeDescriptor(scopeDescriptor, 'editor.completions')
const allConfigEntries = this.settingsForScopeDescriptor(scopeDescriptor, 'autocomplete.symbols')
// TODO: ported from symbol provider - not sure about this
// Config entries are reverse sorted in order of specificity. We want most
// specific to win; this simplifies the loop.
allConfigEntries.reverse()
for (let i = 0; i < legacyCompletions.length; i++) {
const { value } = legacyCompletions[i]
if (Array.isArray(value) && value.length) {
this.addLegacyConfigEntry(value)
}
}
let addedConfigEntry = false
for (let j = 0; j < allConfigEntries.length; j++) {
const { value } = allConfigEntries[j]
if (!Array.isArray(value) && typeof value === 'object') {
this.addConfigEntry(value)
addedConfigEntry = true
}
}
if (!addedConfigEntry) { return this.addConfigEntry(this.defaultConfig) }
}
addLegacyConfigEntry (suggestions) {
suggestions = (suggestions.map((suggestion) => ({text: suggestion, type: 'builtin'})))
if (this.config.builtin == null) {
this.config.builtin = {suggestions: []}
}
this.config.builtin.suggestions = this.config.builtin.suggestions.concat(suggestions)
return this.config.builtin.suggestions
}
addConfigEntry (config) {
for (const type in config) {
const options = config[type]
if (this.config[type] == null) { this.config[type] = {} }
if (options.selector != null) { this.config[type].selectors = Selector.create(options.selector) }
this.config[type].typePriority = options.typePriority != null ? options.typePriority : 1
const suggestions = this.sanitizeSuggestionsFromConfig(options.suggestions, type)
if ((suggestions != null) && suggestions.length) { this.config[type].suggestions = suggestions }
}
}
sanitizeSuggestionsFromConfig (suggestions, type) {
if ((suggestions != null) && Array.isArray(suggestions)) {
const sanitizedSuggestions = []
for (let i = 0; i < suggestions.length; i++) {
let suggestion = suggestions[i]
if (typeof suggestion === 'string') {
sanitizedSuggestions.push({text: suggestion, type})
} else if (typeof suggestions[0] === 'object' && ((suggestion.text != null) || (suggestion.snippet != null))) {
suggestion = Object.assign({}, {type}, suggestion)
sanitizedSuggestions.push(suggestion)
}
}
return sanitizedSuggestions
} else {
return null
}
}
settingsForScopeDescriptor (scopeDescriptor, keyPath) {
const config = this.atomConfig.getAll(keyPath, {scope: scopeDescriptor})
return config
}
}

View File

@ -0,0 +1,303 @@
const {CompositeDisposable, Disposable} = require('atom')
const {isFunction, isString} = require('./type-helpers')
const {Selector} = require('selector-kit')
const stableSort = require('stable')
const grim = require('grim')
const {selectorsMatchScopeChain} = require('./scope-helpers')
const {API_VERSION} = require('./private-symbols')
const SubsequenceProvider = require('./subsequence-provider')
const ProviderMetadata = require('./provider-metadata')
module.exports =
class ProviderManager {
constructor () {
this.defaultProvider = null
this.defaultProviderRegistration = null
this.providers = new Map()
this.store = null
this.subscriptions = null
this.globalBlacklist = null
this.applicableProviders = this.applicableProviders.bind(this)
this.toggleDefaultProvider = this.toggleDefaultProvider.bind(this)
this.setGlobalBlacklist = this.setGlobalBlacklist.bind(this)
this.metadataForProvider = this.metadataForProvider.bind(this)
this.apiVersionForProvider = this.apiVersionForProvider.bind(this)
this.addProvider = this.addProvider.bind(this)
this.removeProvider = this.removeProvider.bind(this)
this.registerProvider = this.registerProvider.bind(this)
}
initialize () {
this.subscriptions = new CompositeDisposable()
this.globalBlacklist = new CompositeDisposable()
this.subscriptions.add(this.globalBlacklist)
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableBuiltinProvider', value => this.toggleDefaultProvider(value)))
this.subscriptions.add(atom.config.observe('autocomplete-plus.scopeBlacklist', value => this.setGlobalBlacklist(value)))
}
dispose () {
this.toggleDefaultProvider(false)
if (this.subscriptions && this.subscriptions.dispose) {
this.subscriptions.dispose()
}
this.subscriptions = null
this.globalBlacklist = null
this.providers = null
}
applicableProviders (labels, scopeDescriptor) {
let providers = this.getProvidersForLabels(labels)
providers = this.filterProvidersByScopeDescriptor(providers, scopeDescriptor)
providers = this.sortProviders(providers, scopeDescriptor)
providers = this.filterProvidersByExcludeLowerPriority(providers)
return this.removeMetadata(providers)
}
getProvidersForLabels (labels) {
let result = new Set()
for (let label of labels) {
if (this.providers.has(label)) {
this.providers.get(label).forEach(result.add.bind(result))
}
}
return Array.from(result)
}
filterProvidersByScopeDescriptor (providers, scopeDescriptor) {
const scopeChain = scopeChainForScopeDescriptor(scopeDescriptor)
if (!scopeChain) { return [] }
if ((this.globalBlacklistSelectors != null) && selectorsMatchScopeChain(this.globalBlacklistSelectors, scopeChain)) { return [] }
const matchingProviders = []
let disableDefaultProvider = false
let defaultProviderMetadata = null
for (let i = 0; i < providers.length; i++) {
const providerMetadata = providers[i]
const {provider} = providerMetadata
if (provider === this.defaultProvider) {
defaultProviderMetadata = providerMetadata
}
if (providerMetadata.matchesScopeChain(scopeChain)) {
matchingProviders.push(providerMetadata)
if (providerMetadata.shouldDisableDefaultProvider(scopeChain)) {
disableDefaultProvider = true
}
}
}
if (disableDefaultProvider) {
const index = matchingProviders.indexOf(defaultProviderMetadata)
if (index > -1) { matchingProviders.splice(index, 1) }
}
return matchingProviders
}
sortProviders (providers, scopeDescriptor) {
const scopeChain = scopeChainForScopeDescriptor(scopeDescriptor)
return stableSort(providers, (providerA, providerB) => {
const priorityA = providerA.provider.suggestionPriority != null ? providerA.provider.suggestionPriority : 1
const priorityB = providerB.provider.suggestionPriority != null ? providerB.provider.suggestionPriority : 1
let difference = priorityB - priorityA
if (difference === 0) {
const specificityA = providerA.getSpecificity(scopeChain)
const specificityB = providerB.getSpecificity(scopeChain)
difference = specificityB - specificityA
}
return difference
}
)
}
filterProvidersByExcludeLowerPriority (providers) {
let lowestAllowedPriority = 0
for (let i = 0; i < providers.length; i++) {
const providerMetadata = providers[i]
const {provider} = providerMetadata
if (provider.excludeLowerPriority) {
lowestAllowedPriority = Math.max(lowestAllowedPriority, provider.inclusionPriority != null ? provider.inclusionPriority : 0)
}
}
return providers.filter((providerMetadata) => (providerMetadata.provider.inclusionPriority != null ? providerMetadata.provider.inclusionPriority : 0) >= lowestAllowedPriority).map((providerMetadata) => providerMetadata)
}
removeMetadata (providers) {
return providers.map(providerMetadata => providerMetadata.provider)
}
toggleDefaultProvider (enabled) {
if (enabled == null) return
if (enabled) {
if (this.defaultProvider != null || this.defaultProviderRegistration != null) return
this.defaultProvider = new SubsequenceProvider()
this.defaultProviderRegistration = this.registerProvider(this.defaultProvider, this.defaultProvider.apiVersion)
} else {
if (this.defaultProviderRegistration) this.defaultProviderRegistration.dispose()
if (this.defaultProvider) this.defaultProvider.dispose()
this.defaultProviderRegistration = null
this.defaultProvider = null
}
}
setGlobalBlacklist (globalBlacklist) {
this.globalBlacklistSelectors = null
if (globalBlacklist && globalBlacklist.length) {
this.globalBlacklistSelectors = Selector.create(globalBlacklist)
}
}
isValidProvider (provider, apiVersion) {
if (apiVersion >= 2) {
return (provider != null) &&
isFunction(provider.getSuggestions) &&
((isString(provider.selector) && !!provider.selector.length) ||
(isString(provider.scopeSelector) && !!provider.scopeSelector.length))
} else {
return (provider != null) && isFunction(provider.requestHandler) && isString(provider.selector) && !!provider.selector.length
}
}
metadataForProvider (provider) {
for (let providers of this.providers.values()) {
for (let i = 0; i < providers.length; i++) {
const providerMetadata = providers[i]
if (providerMetadata.provider === provider) { return providerMetadata }
}
}
return null
}
apiVersionForProvider (provider) {
if (this.metadataForProvider(provider) && this.metadataForProvider(provider).apiVersion) {
return this.metadataForProvider(provider).apiVersion
}
}
isProviderRegistered (provider) {
return (this.metadataForProvider(provider) != null)
}
addProvider (provider, apiVersion = 3) {
if (this.isProviderRegistered(provider)) { return }
let providerMetadata = new ProviderMetadata(provider, apiVersion)
let labels = providerMetadata.getLabels()
for (var label of labels) {
if (!this.providers.has(label)) {
this.providers.set(label, [])
}
this.providers.get(label).push(providerMetadata)
}
if (provider.dispose != null) { return this.subscriptions.add(provider) }
}
removeProvider (provider) {
if (!this.providers) { return }
for (let providers of this.providers.values()) {
for (let i = 0; i < providers.length; i++) {
const providerMetadata = providers[i]
if (providerMetadata.provider === provider) {
providers.splice(i, 1)
break
}
}
}
if (provider.dispose != null) {
if (this.subscriptions) {
this.subscriptions.remove(provider)
}
}
}
registerProvider (provider, apiVersion = 3) {
if (provider == null) { return }
provider[API_VERSION] = apiVersion
switch (apiVersion) {
case 2:
if ((provider.id != null) && provider !== this.defaultProvider) {
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
contains an \`id\` property.
An \`id\` attribute on your provider is no longer necessary.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (provider.requestHandler != null) {
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
contains a \`requestHandler\` property.
\`requestHandler\` has been renamed to \`getSuggestions\`.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
if (provider.blacklist != null) {
grim.deprecate(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
contains a \`blacklist\` property.
\`blacklist\` has been renamed to \`disableForScopeSelector\`.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API`
)
}
break
case 3:
if (provider.selector != null) {
throw new Error(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
specifies \`selector\` instead of the \`scopeSelector\` attribute.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API.`)
}
if (provider.disableForSelector != null) {
throw new Error(`Autocomplete provider '${provider.constructor.name}(${provider.id})'
specifies \`disableForSelector\` instead of the \`disableForScopeSelector\`
attribute.
See https://github.com/atom/autocomplete-plus/wiki/Provider-API.`)
}
break
}
if (!this.isValidProvider(provider, apiVersion)) {
console.warn(`Provider ${provider.constructor.name} is not valid`, provider)
return new Disposable()
}
if (this.isProviderRegistered(provider)) { return }
this.addProvider(provider, apiVersion)
const disposable = new Disposable(() => {
this.removeProvider(provider)
})
// When the provider is disposed, remove its registration
const originalDispose = provider.dispose
if (originalDispose) {
provider.dispose = () => {
originalDispose.call(provider)
disposable.dispose()
}
}
return disposable
}
}
const scopeChainForScopeDescriptor = (scopeDescriptor) => {
// TODO: most of this is temp code to understand #308
const type = typeof scopeDescriptor
let hasScopeChain = false
if (type === 'object' && scopeDescriptor && scopeDescriptor.getScopeChain) {
hasScopeChain = true
}
if (type === 'string') {
return scopeDescriptor
} else if (type === 'object' && hasScopeChain) {
const scopeChain = scopeDescriptor.getScopeChain()
if ((scopeChain != null) && (scopeChain.replace == null)) {
const json = JSON.stringify(scopeDescriptor)
throw new Error(`01: ScopeChain is not correct type: ${type}; ${json}`)
}
return scopeChain
} else {
const json = JSON.stringify(scopeDescriptor)
throw new Error(`02: ScopeChain is not correct type: ${type}; ${json}`)
}
}

View File

@ -0,0 +1,69 @@
'use babel'
import { Selector } from 'selector-kit'
import { selectorForScopeChain, selectorsMatchScopeChain } from './scope-helpers'
export default class ProviderMetadata {
constructor (provider, apiVersion) {
this.provider = provider
this.apiVersion = apiVersion
// TODO API: remove this when 2.0 support is removed
if (this.provider.selector != null) {
this.scopeSelectors = Selector.create(this.provider.selector)
} else {
this.scopeSelectors = Selector.create(this.provider.scopeSelector)
}
// TODO API: remove this when 2.0 support is removed
if (this.provider.disableForSelector != null) {
this.disableForScopeSelectors = Selector.create(this.provider.disableForSelector)
} else if (this.provider.disableForScopeSelector != null) {
this.disableForScopeSelectors = Selector.create(this.provider.disableForScopeSelector)
}
// TODO API: remove this when 1.0 support is removed
let providerBlacklist
if (this.provider.providerblacklist && this.provider.providerblacklist['autocomplete-plus-fuzzyprovider']) {
providerBlacklist = this.provider.providerblacklist['autocomplete-plus-fuzzyprovider']
}
if (providerBlacklist) {
this.disableDefaultProviderSelectors = Selector.create(providerBlacklist)
}
}
getLabels () {
// The default label will let the provider be used for
// the main text editors of the workspace.
return this.provider.labels || ['workspace-center']
}
matchesScopeChain (scopeChain) {
if (this.disableForScopeSelectors != null) {
if (selectorsMatchScopeChain(this.disableForScopeSelectors, scopeChain)) { return false }
}
if (selectorsMatchScopeChain(this.scopeSelectors, scopeChain)) {
return true
} else {
return false
}
}
shouldDisableDefaultProvider (scopeChain) {
if (this.disableDefaultProviderSelectors != null) {
return selectorsMatchScopeChain(this.disableDefaultProviderSelectors, scopeChain)
} else {
return false
}
}
getSpecificity (scopeChain) {
const selector = selectorForScopeChain(this.scopeSelectors, scopeChain)
if (selector) {
return selector.getSpecificity()
} else {
return 0
}
}
}

View File

@ -0,0 +1,74 @@
'use babel'
import slick from 'atom-slick'
const EscapeCharacterRegex = /[-!"#$%&'*+,/:;=?@|^~()<>{}[\]]/g
const cachedMatchesBySelector = {}
const getCachedMatch = (selector, scopeChain) => {
const cachedMatchesByScopeChain = cachedMatchesBySelector[selector]
if (cachedMatchesByScopeChain) {
return cachedMatchesByScopeChain[scopeChain]
}
}
const setCachedMatch = (selector, scopeChain, match) => {
let cachedMatchesByScopeChain = cachedMatchesBySelector[selector]
if (!cachedMatchesByScopeChain) {
cachedMatchesByScopeChain = {}
cachedMatchesBySelector[selector] = cachedMatchesByScopeChain
}
cachedMatchesByScopeChain[scopeChain] = match
cachedMatchesByScopeChain[scopeChain]
}
const parseScopeChain = (scopeChain) => {
scopeChain = scopeChain.replace(EscapeCharacterRegex, (match) => {
return '\\' + match[0]
})
const parsed = slick.parse(scopeChain)[0]
if (!parsed || parsed.length === 0) {
return []
}
const result = []
for (let i = 0; i < parsed.length; i++) {
result.push(parsed[i])
}
return result
}
const selectorForScopeChain = (selectors, scopeChain) => {
for (let i = 0; i < selectors.length; i++) {
const selector = selectors[i]
const cachedMatch = getCachedMatch(selector, scopeChain)
if (cachedMatch != null) {
if (cachedMatch) {
return selector
} else {
continue
}
} else {
const scopes = parseScopeChain(scopeChain)
while (scopes.length > 0) {
if (selector.matches(scopes)) {
setCachedMatch(selector, scopeChain, true)
return selector
}
scopes.pop()
}
setCachedMatch(selector, scopeChain, false)
}
}
return null
}
const selectorsMatchScopeChain = (selectors, scopeChain) => { return selectorForScopeChain(selectors, scopeChain) != null }
const buildScopeChainString = (scopes) => { return `.${scopes.join(' .')}` }
export { selectorsMatchScopeChain, selectorForScopeChain, buildScopeChainString }

View File

@ -0,0 +1,99 @@
'use babel'
export default class SnippetParser {
reset () {
this.inSnippet = false
this.inSnippetBody = false
this.snippetStart = -1
this.snippetEnd = -1
this.bodyStart = -1
this.bodyEnd = -1
this.escapedBraceIndices = null
}
findSnippets (text) {
if (text.length <= 0 || text.indexOf('$') === -1) { return } // No snippets
this.reset()
const snippets = []
// We're not using a regex because escaped right braces cannot be tracked without lookbehind,
// which doesn't exist yet for javascript; consequently we need to iterate through each character.
// This might feel ugly, but it's necessary.
for (let index = 0; index < text.length; index++) {
if (this.inSnippet && this.snippetEnd === index) {
let body = text.slice(this.bodyStart, this.bodyEnd + 1)
body = this.removeBraceEscaping(body, this.bodyStart, this.escapedBraceIndices)
snippets.push({snippetStart: this.snippetStart, snippetEnd: this.snippetEnd, bodyStart: this.bodyStart, bodyEnd: this.bodyEnd, body})
this.reset()
continue
}
if (this.inSnippet && index >= this.bodyStart && index <= this.bodyEnd) { this.inBody = true }
if (this.inSnippet && (index > this.bodyEnd || index < this.bodyStart)) { this.inBody = false }
if (this.bodyStart === -1 || this.bodyEnd === -1) { this.inBody = false }
if (this.inSnippet && !this.inBody) { continue }
if (this.inSnippet && this.inBody) { continue }
// Determine if we've found a new snippet
if (!this.inSnippet && text.indexOf('${', index) === index) {
// Find index of colon
let colonIndex = text.indexOf(':', index + 3)
if (colonIndex !== -1) {
// Disqualify snippet unless the text between '${' and ':' are digits
const groupStart = index + 2
const groupEnd = colonIndex - 1
if (groupEnd >= groupStart) {
for (let i = groupStart; i < groupEnd; i++) {
if (isNaN(parseInt(text.charAt(i)))) { colonIndex = -1 }
}
} else {
colonIndex = -1
}
}
// Find index of '}'
let rightBraceIndex = -1
if (colonIndex !== -1) {
let i = index + 4
while (true) {
rightBraceIndex = text.indexOf('}', i)
if (rightBraceIndex === -1) { break }
if (text.charAt(rightBraceIndex - 1) === '\\') {
if (this.escapedBraceIndices == null) { this.escapedBraceIndices = [] }
this.escapedBraceIndices.push(rightBraceIndex - 1)
} else {
break
}
i = rightBraceIndex + 1
}
}
if (colonIndex !== -1 && rightBraceIndex !== -1 && colonIndex < rightBraceIndex) {
this.inSnippet = true
this.inBody = false
this.snippetStart = index
this.snippetEnd = rightBraceIndex
this.bodyStart = colonIndex + 1
this.bodyEnd = rightBraceIndex - 1
continue
} else {
this.reset()
}
}
}
return snippets
}
removeBraceEscaping (body, bodyStart, escapedBraceIndices) {
if (escapedBraceIndices != null) {
for (let i = 0; i < escapedBraceIndices.length; i++) {
const bodyIndex = escapedBraceIndices[i]
body = removeCharFromString(body, bodyIndex - bodyStart - i)
}
}
return body
}
};
const removeCharFromString = (str, index) => str.slice(0, index) + str.slice(index + 1)

View File

@ -0,0 +1,265 @@
const { CompositeDisposable, TextBuffer } = require('atom')
const ProviderConfig = require('./provider-config')
const getAdditionalWordCharacters = require('./get-additional-word-characters')
module.exports =
class SubsequenceProvider {
constructor (options = {}) {
this.apiVersion = 4
this.defaults()
this.subscriptions = new CompositeDisposable()
this.watchedBuffers = new Map()
if (options.atomConfig) {
this.atomConfig = options.atomConfig
}
if (options.atomWorkspace) {
this.atomWorkspace = options.atomWorkspace
}
this.providerConfig = new ProviderConfig({
atomConfig: this.atomConfig
})
// make this.X available where X is the autocomplete-plus.X setting
const settings = [
'autocomplete-plus.enableExtendedUnicodeSupport', // TODO
'autocomplete-plus.minimumWordLength',
'autocomplete-plus.includeCompletionsFromAllBuffers',
'autocomplete-plus.useLocalityBonus',
'autocomplete-plus.strictMatching'
]
settings.forEach(property => {
this.subscriptions.add(this.atomConfig.observe(property, val => {
this[property.split('.')[1]] = val
}))
})
this.subscriptions.add(this.atomWorkspace.observeTextEditors((e) => {
this.watchBuffer(e)
}))
this.configSuggestionsBuffer = new TextBuffer()
}
inspect () {
return `SubsequenceProvider {apiVersion: ${this.apiVersion}}`
}
defaults () {
this.atomConfig = atom.config
this.atomWorkspace = atom.workspace
this.possibleWordCharacters = '/\\()"\':,.;<>~!@#$%^&*|+=[]{}`?_-…'.split('')
this.enableExtendedUnicodeSupport = false
this.maxSuggestions = 20
this.maxResultsPerBuffer = 20
this.maxSearchRowDelta = 3000
this.labels = ['workspace-center', 'default', 'subsequence-provider']
this.scopeSelector = '*'
this.inclusionPriority = 0
this.suggestionPriority = 0
this.watchedBuffers = null
}
dispose () {
return this.subscriptions.dispose()
}
watchBuffer (editor) {
const buffer = editor.getBuffer()
if (!this.watchedBuffers.has(buffer)) {
const bufferSubscriptions = new CompositeDisposable()
bufferSubscriptions.add(buffer.onDidDestroy(() => {
bufferSubscriptions.dispose()
this.watchedBuffers.delete(buffer)
}))
}
this.watchedBuffers.set(buffer, editor)
}
// This is kind of a hack. We throw the config suggestions in a buffer, so
// we can use .findWordsWithSubsequence on them.
configSuggestionsToSubsequenceMatches (suggestions, prefix) {
if (!suggestions || suggestions.length === 0) {
return Promise.resolve([])
}
const suggestionText = suggestions
.map(sug => sug.displayText || sug.snippet || sug.text)
.join('\n')
this.configSuggestionsBuffer.buffer.setText(suggestionText)
return this.configSuggestionsBuffer.findWordsWithSubsequence(
prefix,
'(){}[] :;,$@%',
this.maxResultsPerBuffer
).then(matches => {
for (let k = 0; k < matches.length; k++) {
matches[k].configSuggestion = suggestions[matches[k].positions[0].row]
}
return matches
})
}
clampedRange (maxDelta, cursorRow, maxRow) {
const clampedMinRow = Math.max(0, cursorRow - maxDelta)
const clampedMaxRow = Math.min(maxRow, cursorRow + maxDelta)
const actualMinRowDelta = cursorRow - clampedMinRow
const actualMaxRowDelta = clampedMaxRow - cursorRow
return {
start: {
row: clampedMinRow - maxDelta + actualMaxRowDelta,
column: 0
},
end: {
row: clampedMaxRow + maxDelta - actualMinRowDelta,
column: 0
}
}
}
bufferToSubsequenceMatches (prefix, additionalWordCharacters, buffer) {
const position = this.watchedBuffers.get(buffer).getCursorBufferPosition()
const searchRange = this.clampedRange(
this.maxSearchRowDelta,
position.row,
buffer.getEndPosition().row
)
return buffer.findWordsWithSubsequenceInRange(
prefix,
additionalWordCharacters,
this.maxResultsPerBuffer,
searchRange
)
}
/*
Section: Suggesting Completions
*/
getSuggestions ({editor, bufferPosition, prefix, scopeDescriptor}) {
if (!prefix) {
return
}
if (prefix.trim().length < this.minimumWordLength) {
return
}
const buffers = this.includeCompletionsFromAllBuffers
? Array.from(this.watchedBuffers.keys())
: [editor.getBuffer()]
const currentEditorBuffer = editor.getBuffer()
const lastCursorPosition = editor.getLastCursor().getBufferPosition()
const additionalWordCharacters = getAdditionalWordCharacters(scopeDescriptor)
const configSuggestions = this.providerConfig.getSuggestionsForScopeDescriptor(
scopeDescriptor
)
const configMatches = this.configSuggestionsToSubsequenceMatches(
configSuggestions,
prefix
)
const subsequenceMatchToType = (match) => {
const editor = this.watchedBuffers.get(match.buffer)
const scopeDescriptor = editor.scopeDescriptorForBufferPosition(match.positions[0])
return this.providerConfig.scopeDescriptorToType(scopeDescriptor)
}
const matchToSuggestion = match => {
return match.configSuggestion || {
text: match.word,
type: subsequenceMatchToType(match),
characterMatchIndices: match.matchIndices
}
}
const bufferResultsToSuggestions = matchesByBuffer => {
const relevantMatches = []
let matchedWords = new Set()
let match
for (let k = 0; k < matchesByBuffer.length; k++) {
// The findWordsWithSubsequence method will return `null`
// if the async work was cancelled due to the buffer being
// mutated since it was enqueued. We return `null` in this
// case because `getSuggestions` will be called again anyway.
if (!matchesByBuffer[k]) return null
const buffer = buffers[k]
for (let l = 0; l < matchesByBuffer[k].length; l++) {
match = matchesByBuffer[k][l]
if (match.word === prefix) continue
if (matchedWords.has(match.word)) continue
if (this.strictMatching && match.word.indexOf(prefix) !== 0) continue
let matchIsUnderCursor = false
if (buffer === currentEditorBuffer && match.score > 0) {
let closestDistance
for (const position of match.positions) {
const distance = Math.abs(position.row - lastCursorPosition.row)
if (closestDistance == null || distance < closestDistance) {
closestDistance = distance
}
if (
distance === 0 &&
lastCursorPosition.column >= position.column &&
lastCursorPosition.column <= position.column + match.word.length
) {
matchIsUnderCursor = true
break
}
}
if (this.useLocalityBonus) {
match.score += Math.floor(11 / (1 + 0.04 * closestDistance))
}
}
if (matchIsUnderCursor) continue
match.buffer = buffer
relevantMatches.push(match)
matchedWords.add(match.word)
}
}
return relevantMatches
.sort(compareMatches)
.slice(0, this.maxSuggestions)
.map(matchToSuggestion)
}
return Promise
.all(
buffers
.map(this.bufferToSubsequenceMatches.bind(this, prefix, additionalWordCharacters))
.concat(configMatches)
)
.then(bufferResultsToSuggestions)
}
}
const compareMatches = (a, b) => {
if (a.score - b.score === 0) {
return a.word.length - b.word.length
}
return b.score - a.score
}

View File

@ -0,0 +1,697 @@
const {CompositeDisposable} = require('atom')
const SnippetParser = require('./snippet-parser')
const {isString} = require('./type-helpers')
const fuzzaldrinPlus = require('fuzzaldrin-plus')
const {marked} = require('marked')
const createDOMPurify = require('dompurify')
const createSuggestionFrag = () => {
const frag = document.createDocumentFragment()
const children = ['icon-container', 'left-label', 'word-container', 'right-label']
children.forEach(c => {
let el = document.createElement('span')
el.className = c
if (c === 'word-container') {
let innerEl = document.createElement('span')
innerEl.className = 'word'
el.appendChild(innerEl)
}
frag.appendChild(el)
})
return frag
}
const ListTemplate = `<div class="suggestion-list-scroller">
<ol class="list-group"></ol>
</div>
<div class="suggestion-description">
<span class="suggestion-description-content"></span>
<a class="suggestion-description-more-link" href="#">More..</a>
</div>`
const iconTypeToClass = {
'snippet': 'icon-move-right',
'import': 'icon-package',
'require': 'icon-package',
'module': 'icon-package',
'package': 'icon-package',
'tag': 'icon-code',
'attribute': 'icon-tag'
}
const SnippetStart = 1
const SnippetEnd = 2
const SnippetStartAndEnd = 3
module.exports = class SuggestionListElement {
constructor (model) {
this.element = document.createElement('autocomplete-suggestion-list')
this.maxItems = 200
this.emptySnippetGroupRegex = /(\$\{\d+:\})|(\$\{\d+\})|(\$\d+)/ig
this.slashesInSnippetRegex = /\\\\/g
this.nodePool = null
this.subscriptions = new CompositeDisposable()
this.element.classList.add('popover-list', 'select-list', 'autocomplete-suggestion-list')
this.registerMouseHandling()
this.element.addEventListener('scroll', this.onScroll.bind(this), true)
this.snippetParser = new SnippetParser()
this.nodePool = []
this.element.innerHTML = ListTemplate
this.ol = this.element.querySelector('.list-group')
this.scroller = this.element.querySelector('.suggestion-list-scroller')
this.descriptionContainer = this.element.querySelector('.suggestion-description')
this.descriptionContent = this.element.querySelector('.suggestion-description-content')
this.descriptionMoreLink = this.element.querySelector('.suggestion-description-more-link')
this.model = model
if (this.model == null) { return }
this.subscriptions.add(this.model.onDidChangeItems(this.itemsChanged.bind(this)))
this.subscriptions.add(this.model.onDidChangeItem(this.itemChanged.bind(this)))
this.subscriptions.add(this.model.onDidSelectNext(this.moveSelectionDown.bind(this)))
this.subscriptions.add(this.model.onDidSelectPrevious(this.moveSelectionUp.bind(this)))
this.subscriptions.add(this.model.onDidSelectPageUp(this.moveSelectionPageUp.bind(this)))
this.subscriptions.add(this.model.onDidSelectPageDown(this.moveSelectionPageDown.bind(this)))
this.subscriptions.add(this.model.onDidSelectTop(this.moveSelectionToTop.bind(this)))
this.subscriptions.add(this.model.onDidSelectBottom(this.moveSelectionToBottom.bind(this)))
this.subscriptions.add(this.model.onDidConfirmSelection(this.confirmSelection.bind(this)))
this.subscriptions.add(this.model.onDidconfirmSelectionIfNonDefault(this.confirmSelectionIfNonDefault.bind(this)))
this.subscriptions.add(this.model.onDidDispose(this.dispose.bind(this)))
this.subscriptions.add(atom.config.observe('autocomplete-plus.suggestionListFollows', suggestionListFollows => {
this.suggestionListFollows = suggestionListFollows
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.maxVisibleSuggestions', maxVisibleSuggestions => {
this.maxVisibleSuggestions = maxVisibleSuggestions
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.useAlternateScoring', useAlternateScoring => {
this.useAlternateScoring = useAlternateScoring
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.moveToCancel', moveToCancel => {
this.moveToCancel = moveToCancel
}))
process.nextTick(() => this.render())
}
// This should be unnecessary but the events we need to override
// are handled at a level that can't be blocked by react synthetic
// events because they are handled at the document
registerMouseHandling () {
this.element.onmousewheel = event => event.stopPropagation()
this.element.onmousedown = (event) => {
const item = this.findItem(event)
if (item && item.dataset && item.dataset.index) {
this.selectedIndex = item.dataset.index
event.stopPropagation()
}
}
this.element.onmouseup = (event) => {
const item = this.findItem(event)
if (item && item.dataset && item.dataset.index) {
event.stopPropagation()
this.confirmSelection()
}
}
}
onScroll (event) {
atom.views.updateDocument(this.renderExtraItems.bind(this))
}
findItem (event) {
let item = event.target
while (item.tagName !== 'LI' && item !== this.element) { item = item.parentNode }
if (item.tagName === 'LI') { return item }
}
updateDescription (item) {
if (!item) {
if (this.model && this.model.items) {
item = this.model.items[this.selectedIndex]
}
}
if (!item) {
return
}
if (item.descriptionMarkdown && item.descriptionMarkdown.length > 0) {
this.descriptionContainer.style.display = 'block'
this.descriptionContent.innerHTML = createDOMPurify().sanitize(
marked(item.descriptionMarkdown, {
gfm: true,
breaks: true,
sanitize: false
})
)
this.setDescriptionMoreLink(item)
} else if (item.description && item.description.length > 0) {
this.descriptionContainer.style.display = 'block'
this.descriptionContent.textContent = item.description
this.setDescriptionMoreLink(item)
} else {
this.descriptionContainer.style.display = 'none'
}
}
setDescriptionMoreLink (item) {
if ((item.descriptionMoreURL != null) && (item.descriptionMoreURL.length != null)) {
this.descriptionMoreLink.style.display = 'inline'
this.descriptionMoreLink.setAttribute('href', item.descriptionMoreURL)
} else {
this.descriptionMoreLink.style.display = 'none'
this.descriptionMoreLink.setAttribute('href', '#')
}
}
itemChanged ({suggestion, index}) {
atom.views.updateDocument(this.renderItem.bind(this, suggestion, index))
atom.views.updateDocument(this.updateDescription.bind(this))
}
itemsChanged () {
if (this.model && this.model.items && this.model.items.length) {
return this.render()
} else {
return atom.views.updateDocument(this.returnItemsToPool.bind(this, 0))
}
}
render () {
this.nonDefaultIndex = false
this.selectedIndex = 0
this.model.select(this.getSelectedItem())
if (atom.views.pollAfterNextUpdate) {
atom.views.pollAfterNextUpdate()
}
atom.views.updateDocument(this.renderItems.bind(this))
atom.views.readDocument(this.readUIPropsFromDOM.bind(this))
}
moveSelectionUp () {
if (this.selectedIndex > 0) {
return this.setSelectedIndex(this.selectedIndex - 1)
} else if (this.moveToCancel) {
this.model.activeEditor.moveUp(1)
return this.model.cancel()
} else {
return this.setSelectedIndex(this.visibleItems().length - 1)
}
}
moveSelectionDown () {
if (this.selectedIndex < (this.visibleItems().length - 1)) {
return this.setSelectedIndex(this.selectedIndex + 1)
} else if (this.moveToCancel) {
this.model.activeEditor.moveDown(1)
return this.model.cancel()
} else {
return this.setSelectedIndex(0)
}
}
moveSelectionPageUp () {
const newIndex = Math.max(0, this.selectedIndex - this.maxVisibleSuggestions)
if (this.moveToCancel) {
const lines = this.model.activeEditor.getScreenLineCount()
this.model.activeEditor.moveUp(lines)
return this.model.cancel()
} else if (this.selectedIndex !== newIndex) {
return this.setSelectedIndex(newIndex)
}
}
moveSelectionPageDown () {
const itemsLength = this.visibleItems().length
const newIndex = Math.min(itemsLength - 1, this.selectedIndex + this.maxVisibleSuggestions)
if (this.moveToCancel) {
const lines = this.model.activeEditor.getScreenLineCount()
this.model.activeEditor.moveDown(lines)
return this.model.cancel()
} else if (this.selectedIndex !== newIndex) {
return this.setSelectedIndex(newIndex)
}
}
moveSelectionToTop () {
const newIndex = 0
if (this.moveToCancel) {
this.model.activeEditor.moveToTop()
return this.model.cancel()
} else if (this.selectedIndex !== newIndex) {
return this.setSelectedIndex(newIndex)
}
}
moveSelectionToBottom () {
const newIndex = this.visibleItems().length - 1
if (this.moveToCancel) {
this.model.activeEditor.moveToBottom()
return this.model.cancel()
} else if (this.selectedIndex !== newIndex) {
return this.setSelectedIndex(newIndex)
}
}
setSelectedIndex (index) {
this.nonDefaultIndex = true
this.selectedIndex = index
this.model.select(this.getSelectedItem())
if (index > this.maxVisibleSuggestions + 1) {
atom.views.updateDocument(this.renderExtraItems.bind(this))
}
return atom.views.updateDocument(this.renderSelectedItem.bind(this))
}
visibleItems () {
if (this.model && this.model.items) {
return this.model.items.slice(0, this.maxItems)
}
}
// Private: Get the currently selected item
//
// Returns the selected {Object}
getSelectedItem () {
if (this.model && this.model.items) {
return this.model.items[this.selectedIndex]
}
}
// Private: Confirms the currently selected item or cancels the list view
// if no item has been selected
confirmSelection () {
if (!this.model.isActive()) { return }
const item = this.getSelectedItem()
if (item != null) {
return this.model.confirm(item)
} else {
return this.model.cancel()
}
}
// Private: Confirms the currently selected item only if it is not the default
// item or cancels the view if none has been selected.
confirmSelectionIfNonDefault (event) {
if (!this.model.isActive()) { return }
if (this.nonDefaultIndex) {
return this.confirmSelection()
} else {
this.model.cancel()
return event.abortKeyBinding()
}
}
renderItems () {
let left
this.element.style.width = null
const items = (left = this.visibleItems()) != null ? left : []
let longestDesc = 0
let longestDescIndex = null
for (let index = 0; index < Math.min(items.length, this.maxVisibleSuggestions + 1); index++) {
const item = items[index]
this.renderItem(item, index)
const descLength = this.descriptionLength(item)
if (descLength > longestDesc) {
longestDesc = descLength
longestDescIndex = index
}
}
// Defer the rendering of suggestions that are not initially visible
if (items.length > this.maxVisibleSuggestions + 1) {
this.extraItems = items.slice(this.maxVisibleSuggestions + 1)
} else {
this.extraItems = null
}
this.updateDescription(items[longestDescIndex])
return atom.views.updateDocument(this.returnItemsToPool.bind(this, items.length))
}
renderExtraItems () {
if (this.extraItems) {
this.extraItems.forEach((item, index) => {
this.renderItem(item, index + this.maxVisibleSuggestions + 1)
})
}
this.extraItems = null
}
returnItemsToPool (pivotIndex) {
if (!this.ol) { return }
let li = this.ol.childNodes[pivotIndex]
while ((this.ol != null) && li) {
li.remove()
this.nodePool.push(li)
li = this.ol.childNodes[pivotIndex]
}
}
descriptionLength (item) {
let count = 0
if (item.description != null) {
count += item.description.length
}
if (item.descriptionMoreURL != null) {
count += 6
}
return count
}
renderSelectedItem () {
if (this.selectedLi && this.selectedLi.classList) {
this.selectedLi.classList.remove('selected')
}
this.selectedLi = this.ol.childNodes[this.selectedIndex]
if (this.selectedLi != null) {
this.selectedLi.classList.add('selected')
this.scrollSelectedItemIntoView()
return this.updateDescription()
}
}
// This is reading the DOM in the updateDOM cycle. If we dont, there is a flicker :/
scrollSelectedItemIntoView () {
const { scrollTop } = this.scroller
const selectedItemTop = this.selectedLi.offsetTop
if (selectedItemTop < scrollTop) {
// scroll up
this.scroller.scrollTop = selectedItemTop
return
}
const { itemHeight } = this.uiProps
const scrollerHeight = (this.maxVisibleSuggestions * itemHeight) + this.uiProps.paddingHeight
if (selectedItemTop + itemHeight > scrollTop + scrollerHeight) {
// scroll down
this.scroller.scrollTop = (selectedItemTop - scrollerHeight) + itemHeight
}
}
readUIPropsFromDOM () {
let wordContainer
if (this.selectedLi) {
wordContainer = this.selectedLi.querySelector('.word-container')
}
if (!this.uiProps) { this.uiProps = {} }
this.uiProps.width = this.element.offsetWidth + 1
this.uiProps.marginLeft = 0
if (wordContainer && wordContainer.offsetLeft) {
this.uiProps.marginLeft = -wordContainer.offsetLeft
}
if (!this.uiProps.itemHeight && this.selectedLi) {
this.uiProps.itemHeight = this.selectedLi.offsetHeight
}
if (!this.uiProps.paddingHeight) {
this.uiProps.paddingHeight = parseInt(getComputedStyle(this.element)['padding-top']) + parseInt(getComputedStyle(this.element)['padding-bottom'])
if (!this.uiProps.paddingHeight) {
this.uiProps.paddingHeight = 0
}
}
// Update UI during this read, so that when polling the document the latest
// changes can be picked up.
return this.updateUIForChangedProps()
}
updateUIForChangedProps () {
this.scroller.style['max-height'] = `${(this.maxVisibleSuggestions * this.uiProps.itemHeight) + this.uiProps.paddingHeight}px`
this.element.style.width = `${this.uiProps.width}px`
if (this.suggestionListFollows === 'Word') {
this.element.style['margin-left'] = `${this.uiProps.marginLeft}px`
}
return this.updateDescription()
}
// Splits the classes on spaces so as not to anger the DOM gods
addClassToElement (element, classNames) {
if (!classNames) { return }
const classes = classNames.split(' ')
if (classes) {
for (let i = 0; i < classes.length; i++) {
let className = classes[i]
className = className.trim()
if (className) { element.classList.add(className) }
}
}
}
renderItem ({iconHTML, type, snippet, text, displayText, className, replacementPrefix, leftLabel, leftLabelHTML, rightLabel, rightLabelHTML, characterMatchIndices}, index) {
let li = this.ol.childNodes[index]
if (!li) {
if (this.nodepool && this.nodePool.length > 0) {
li = this.nodePool.pop()
} else {
li = document.createElement('li')
li.appendChild(createSuggestionFrag())
}
li.dataset.index = index
this.ol.appendChild(li)
}
li.className = ''
if (index === this.selectedIndex) { li.classList.add('selected') }
if (className) { this.addClassToElement(li, className) }
if (index === this.selectedIndex) { this.selectedLi = li }
const iconContainer = document.createElement('span')
iconContainer.className = 'icon-container'
li.replaceChild(iconContainer, li.querySelector('.icon-container'))
const sanitizedType = isString(type) ? type : ''
const sanitizedIconHTML = isString(iconHTML) ? iconHTML : undefined
if ((sanitizedIconHTML || sanitizedType) && iconHTML !== false) {
let icon = document.createElement('i')
icon.className = 'icon'
if (sanitizedIconHTML != null) {
icon.innerHTML = sanitizedIconHTML
} else {
let defaultIcon
if (iconTypeToClass[sanitizedType] != null) {
defaultIcon = document.createElement('i')
defaultIcon.className = iconTypeToClass[sanitizedType]
} else if (sanitizedType) {
defaultIcon = document.createElement('span')
defaultIcon.className = 'icon-letter'
defaultIcon.textContent = sanitizedType[0]
}
if (defaultIcon) {
icon.appendChild(defaultIcon)
}
}
if (type) { this.addClassToElement(icon, type) }
iconContainer.appendChild(icon)
}
const wordSpan = document.createElement('span')
wordSpan.className = 'word'
wordSpan.appendChild(
this.getDisplayFragment(text, snippet, displayText, replacementPrefix, characterMatchIndices)
)
li.querySelector('.word-container')
.replaceChild(wordSpan, li.querySelector('.word'))
const leftLabelSpan = li.querySelector('.left-label')
if (leftLabelHTML != null) {
leftLabelSpan.innerHTML = leftLabelHTML
} else if (leftLabel != null) {
leftLabelSpan.textContent = leftLabel
} else {
leftLabelSpan.textContent = ''
}
const rightLabelSpan = li.querySelector('.right-label')
if (rightLabelHTML != null) {
rightLabelSpan.innerHTML = rightLabelHTML
} else if (rightLabel != null) {
rightLabelSpan.textContent = rightLabel
} else {
rightLabelSpan.textContent = ''
}
}
getDisplayFragment (text, snippet, displayText, replacementPrefix, characterMatchIndices) {
let replacementText = text
let snippetIndices
if (typeof displayText === 'string') {
replacementText = displayText
} else if (typeof snippet === 'string') {
replacementText = this.removeEmptySnippets(snippet)
const snippets = this.snippetParser.findSnippets(replacementText)
replacementText = this.removeSnippetsFromText(snippets, replacementText)
snippetIndices = this.findSnippetIndices(snippets)
}
if (!characterMatchIndices) {
characterMatchIndices = this.findCharacterMatchIndices(replacementText, replacementPrefix)
} else {
characterMatchIndices = characterMatchIndices.reduce((matches, index) => {
matches[index] = true
return matches
}, {})
}
const appendNonMatchChars = (el, nonMatchChars) => {
if (nonMatchChars) {
el.appendChild(
document.createTextNode(nonMatchChars)
)
}
}
let frag = document.createDocumentFragment()
let workingEl = frag
var nonMatchChars = ''
for (let index = 0; index < replacementText.length; index++) {
if (snippetIndices && (snippetIndices[index] === SnippetStart || snippetIndices[index] === SnippetStartAndEnd)) {
appendNonMatchChars(workingEl, nonMatchChars)
nonMatchChars = ''
let s = document.createElement('span')
s.className = 'snippet-completion'
workingEl = s
}
if (characterMatchIndices && characterMatchIndices[index]) {
appendNonMatchChars(workingEl, nonMatchChars)
nonMatchChars = ''
let s = document.createElement('span')
s.className = 'character-match'
s.textContent = replacementText[index]
workingEl.appendChild(s)
} else {
nonMatchChars += replacementText[index]
}
if (snippetIndices && (snippetIndices[index] === SnippetEnd || snippetIndices[index] === SnippetStartAndEnd)) {
appendNonMatchChars(workingEl, nonMatchChars)
nonMatchChars = ''
frag.appendChild(workingEl)
workingEl = frag
}
}
appendNonMatchChars(workingEl, nonMatchChars)
return frag
}
removeEmptySnippets (text) {
if (!text || !text.length || text.indexOf('$') === -1) { return text } // No snippets
return text.replace(this.emptySnippetGroupRegex, '') // Remove all occurrences of $0 or ${0} or ${0:}
}
// Will convert 'abc(${1:d}, ${2:e})f' => 'abc(d, e)f'
//
// * `snippets` {Array} from `SnippetParser.findSnippets`
// * `text` {String} to remove snippets from
//
// Returns {String}
removeSnippetsFromText (snippets, text) {
if (!text || !text.length || !snippets || !snippets.length) {
return text
}
let index = 0
let result = ''
for (const {snippetStart, snippetEnd, body} of snippets) {
result += text.slice(index, snippetStart) + body
index = snippetEnd + 1
}
if (index !== text.length) {
result += text.slice(index, text.length)
}
result = result.replace(this.slashesInSnippetRegex, '\\')
return result
}
// Computes the indices of snippets in the resulting string from
// `removeSnippetsFromText`.
//
// * `snippets` {Array} from `SnippetParser.findSnippets`
//
// e.g. A replacement of 'abc(${1:d})e' is replaced to 'abc(d)e' will result in
//
// `{4: SnippetStartAndEnd}`
//
// Returns {Object} of {index: SnippetStart|End|StartAndEnd}
findSnippetIndices (snippets) {
if (!snippets) {
return
}
const indices = {}
let offsetAccumulator = 0
for (const {snippetStart, snippetEnd, body} of snippets) {
const bodyLength = body.length
const snippetLength = (snippetEnd - snippetStart) + 1
const startIndex = snippetStart - offsetAccumulator
const endIndex = (startIndex + bodyLength) - 1
offsetAccumulator += snippetLength - bodyLength
if (startIndex === endIndex) {
indices[startIndex] = SnippetStartAndEnd
} else {
indices[startIndex] = SnippetStart
indices[endIndex] = SnippetEnd
}
}
return indices
}
// Finds the indices of the chars in text that are matched by replacementPrefix
//
// e.g. text = 'abcde', replacementPrefix = 'acd' Will result in
//
// {0: true, 2: true, 3: true}
//
// Returns an {Object}
findCharacterMatchIndices (text, replacementPrefix) {
if (!text || !text.length || !replacementPrefix || !replacementPrefix.length) { return }
const matches = {}
if (this.useAlternateScoring) {
const matchIndices = fuzzaldrinPlus.match(text, replacementPrefix)
for (const i of matchIndices) {
matches[i] = true
}
} else {
let wordIndex = 0
for (let i = 0; i < replacementPrefix.length; i++) {
const ch = replacementPrefix[i]
while (wordIndex < text.length && text[wordIndex].toLowerCase() !== ch.toLowerCase()) {
wordIndex += 1
}
if (wordIndex >= text.length) { break }
matches[wordIndex] = true
wordIndex += 1
}
}
return matches
}
dispose () {
this.subscriptions.dispose()
if (this.parentNode) {
this.parentNode.removeChild(this)
}
}
}

View File

@ -0,0 +1,382 @@
const {Emitter, CompositeDisposable} = require('atom')
const { UnicodeLetters } = require('./unicode-helpers')
const SuggestionListElement = require('./suggestion-list-element')
module.exports =
class SuggestionList {
constructor () {
this.wordPrefixRegex = null
this.cancel = this.cancel.bind(this)
this.confirm = this.confirm.bind(this)
this.confirmSelection = this.confirmSelection.bind(this)
this.confirmSelectionIfNonDefault = this.confirmSelectionIfNonDefault.bind(this)
this.show = this.show.bind(this)
this.showAtBeginningOfPrefix = this.showAtBeginningOfPrefix.bind(this)
this.showAtCursorPosition = this.showAtCursorPosition.bind(this)
this.hide = this.hide.bind(this)
this.destroyOverlay = this.destroyOverlay.bind(this)
this.activeEditor = null
}
initialize () {
this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
this.subscriptions.add(atom.commands.add('atom-text-editor.autocomplete-active', {
'autocomplete-plus:confirm': this.confirmSelection,
'autocomplete-plus:confirmIfNonDefault': this.confirmSelectionIfNonDefault,
'autocomplete-plus:cancel': this.cancel
}))
this.subscriptions.add(atom.config.observe('autocomplete-plus.enableExtendedUnicodeSupport', (enableExtendedUnicodeSupport) => {
if (enableExtendedUnicodeSupport) {
this.wordPrefixRegex = new RegExp(`^[${UnicodeLetters}\\d_-]`)
} else {
this.wordPrefixRegex = /^[\w-]/
}
return this.wordPrefixRegex
}))
}
get suggestionListElement () {
if (!this._suggestionListElement) {
this._suggestionListElement = new SuggestionListElement(this)
}
return this._suggestionListElement
}
addBindings (editor) {
if (this.bindings && this.bindings.dispose) {
this.bindings.dispose()
}
this.bindings = new CompositeDisposable()
const completionKey = atom.config.get('autocomplete-plus.confirmCompletion') || ''
const keys = {}
if (completionKey.indexOf('tab') > -1) { keys['tab'] = 'autocomplete-plus:confirm' }
if (completionKey.indexOf('enter') > -1) {
if (completionKey.indexOf('always') > -1) {
keys['enter'] = 'autocomplete-plus:confirmIfNonDefault'
} else {
keys['enter'] = 'autocomplete-plus:confirm'
}
}
this.bindings.add(atom.keymaps.add(
'atom-text-editor.autocomplete-active',
{'atom-text-editor.autocomplete-active': keys})
)
const useCoreMovementCommands = atom.config.get('autocomplete-plus.useCoreMovementCommands')
const commandNamespace = useCoreMovementCommands ? 'core' : 'autocomplete-plus'
const commands = {}
commands[`${commandNamespace}:move-up`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectPrevious()
return event.stopImmediatePropagation()
}
}
commands[`${commandNamespace}:move-down`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectNext()
return event.stopImmediatePropagation()
}
}
commands[`${commandNamespace}:page-up`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectPageUp()
return event.stopImmediatePropagation()
}
}
commands[`${commandNamespace}:page-down`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectPageDown()
return event.stopImmediatePropagation()
}
}
commands[`${commandNamespace}:move-to-top`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectTop()
return event.stopImmediatePropagation()
}
}
commands[`${commandNamespace}:move-to-bottom`] = (event) => {
if (this.isActive() && this.items && this.items.length > 1) {
this.selectBottom()
return event.stopImmediatePropagation()
}
}
this.bindings.add(atom.commands.add(
atom.views.getView(editor), commands)
)
return this.bindings.add(
atom.config.onDidChange('autocomplete-plus.useCoreMovementCommands', () => {
return this.addBindings(editor)
}
))
}
/*
Section: Event Triggers
*/
cancel () {
return this.emitter.emit('did-cancel')
}
confirm (match) {
return this.emitter.emit('did-confirm', match)
}
confirmSelection () {
return this.emitter.emit('did-confirm-selection')
}
confirmSelectionIfNonDefault (event) {
return this.emitter.emit('did-confirm-selection-if-non-default', event)
}
select (suggestion) {
return this.emitter.emit('did-select', suggestion)
}
selectNext () {
return this.emitter.emit('did-select-next')
}
selectPrevious () {
return this.emitter.emit('did-select-previous')
}
selectPageUp () {
return this.emitter.emit('did-select-page-up')
}
selectPageDown () {
return this.emitter.emit('did-select-page-down')
}
selectTop () {
return this.emitter.emit('did-select-top')
}
selectBottom () {
return this.emitter.emit('did-select-bottom')
}
/*
Section: Events
*/
onDidConfirmSelection (fn) {
return this.emitter.on('did-confirm-selection', fn)
}
onDidconfirmSelectionIfNonDefault (fn) {
return this.emitter.on('did-confirm-selection-if-non-default', fn)
}
onDidConfirm (fn) {
return this.emitter.on('did-confirm', fn)
}
onDidSelect (fn) {
return this.emitter.on('did-select', fn)
}
onDidSelectNext (fn) {
return this.emitter.on('did-select-next', fn)
}
onDidSelectPrevious (fn) {
return this.emitter.on('did-select-previous', fn)
}
onDidSelectPageUp (fn) {
return this.emitter.on('did-select-page-up', fn)
}
onDidSelectPageDown (fn) {
return this.emitter.on('did-select-page-down', fn)
}
onDidSelectTop (fn) {
return this.emitter.on('did-select-top', fn)
}
onDidSelectBottom (fn) {
return this.emitter.on('did-select-bottom', fn)
}
onDidCancel (fn) {
return this.emitter.on('did-cancel', fn)
}
onDidDispose (fn) {
return this.emitter.on('did-dispose', fn)
}
onDidChangeItems (fn) {
return this.emitter.on('did-change-items', fn)
}
onDidChangeItem (fn) {
return this.emitter.on('did-change-item', fn)
}
isActive () {
return (this.activeEditor != null)
}
show (editor, options) {
if (atom.config.get('autocomplete-plus.suggestionListFollows') === 'Cursor') {
return this.showAtCursorPosition(editor, options)
} else {
let { prefix } = options
let followRawPrefix = false
for (let i = 0; i < this.items.length; i++) {
const item = this.items[i]
if (item.replacementPrefix != null) {
prefix = item.replacementPrefix.trim()
followRawPrefix = true
break
}
}
return this.showAtBeginningOfPrefix(editor, prefix, followRawPrefix)
}
}
showAtBeginningOfPrefix (editor, prefix, followRawPrefix = false) {
let bufferPosition
if (editor) {
bufferPosition = editor.getCursorBufferPosition()
if (followRawPrefix || this.wordPrefixRegex.test(prefix)) {
bufferPosition = bufferPosition.translate([0, -prefix.length])
}
}
if (this.activeEditor === editor) {
if (!bufferPosition.isEqual(this.displayBufferPosition)) {
this.displayBufferPosition = bufferPosition
if (this.suggestionMarker) {
this.suggestionMarker.setBufferRange([bufferPosition, bufferPosition])
}
}
} else {
this.destroyOverlay()
if (editor) {
this.activeEditor = editor
this.displayBufferPosition = bufferPosition
const marker = this.suggestionMarker = editor.markBufferRange([bufferPosition, bufferPosition])
this.overlayDecoration = editor.decorateMarker(marker, {type: 'overlay', item: this.suggestionListElement, position: 'tail', class: 'autocomplete-plus'})
const editorElement = atom.views.getView(this.activeEditor)
if (editorElement && editorElement.classList) {
editorElement.classList.add('autocomplete-active')
}
this.addBindings(editor)
}
}
}
showAtCursorPosition (editor) {
if (this.activeEditor === editor || (editor == null)) { return }
this.destroyOverlay()
let marker
if (editor.getLastCursor()) {
marker = editor.getLastCursor().getMarker()
}
if (marker) {
this.activeEditor = editor
const editorElement = atom.views.getView(this.activeEditor)
if (editorElement && editorElement.classList) {
editorElement.classList.add('autocomplete-active')
}
this.overlayDecoration = editor.decorateMarker(marker, {type: 'overlay', item: this.suggestionListElement, class: 'autocomplete-plus'})
return this.addBindings(editor)
}
}
hide () {
this.destroyOverlay()
if (this.activeEditor === null) {
return
}
if (this.bindings && this.bindings.dispose) {
this.bindings.dispose()
}
this.activeEditor = null
return this.activeEditor
}
destroyOverlay () {
if (this.suggestionMarker && this.suggestionMarker.destroy) {
this.suggestionMarker.destroy()
} else if (this.overlayDecoration && this.overlayDecoration.destroy) {
this.overlayDecoration.destroy()
}
const editorElement = atom.views.getView(this.activeEditor)
if (editorElement && editorElement.classList) {
atom.views.updateDocument(() => {
editorElement.classList.remove('autocomplete-active')
})
}
this.suggestionMarker = undefined
this.overlayDecoration = undefined
return this.overlayDecoration
}
changeItems (items) {
this.items = items
return this.emitter.emit('did-change-items', this.items)
}
replaceItem (oldSuggestion, newSuggestion) {
if (newSuggestion == null) {
return
}
if (this.items == null) {
return
}
let itemChanged = false
let itemIndex
this.items.forEach((suggestion, idx) => {
if (suggestion === oldSuggestion) {
this.items[idx] = newSuggestion
itemChanged = true
itemIndex = idx
}
})
if (itemChanged) {
this.emitter.emit('did-change-item', {
suggestion: newSuggestion,
index: itemIndex
})
}
}
// Public: Clean up, stop listening to events
dispose () {
if (this.subscriptions) {
this.subscriptions.dispose()
}
if (this.bindings && this.bindings.dispose) {
this.bindings.dispose()
}
this.emitter.emit('did-dispose')
return this.emitter.dispose()
}
}

View File

@ -0,0 +1,13 @@
'use babel'
const isFunction = value => isType(value, 'function')
const isString = value => isType(value, 'string')
const isType = (value, typeName) => {
const t = typeof value
if (t == null) { return false }
return t === typeName
}
export { isFunction, isString }

View File

@ -0,0 +1,4 @@
'use babel'
const UnicodeLetters = 'A-Za-z\\xAA\\xB5\\xBA\\xC0-\\xD6\\xD8-\\xF6\\xF8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0-\\u08B4\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183\\u2184\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005\\u3006\\u3031-\\u3035\\u303B\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FD5\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7AD\\uA7B0-\\uA7B7\\uA7F7-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB65\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC'
export { UnicodeLetters }

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,258 @@
{
"name": "autocomplete-plus",
"version": "2.42.6",
"main": "./lib/main",
"description": "Display possible completions in the editor while typing",
"repository": "https://github.com/atom/autocomplete-plus",
"license": "MIT",
"engines": {
"atom": ">=0.189.0 <2.0.0"
},
"dependencies": {
"atom-slick": "^2.0.0",
"dompurify": "^2.2.6",
"fuzzaldrin": "^2.1.0",
"fuzzaldrin-plus": "^0.6.0",
"grim": "^2.0.1",
"marked": "^4.0.10",
"minimatch": "^3.0.3",
"selector-kit": "^0.1",
"stable": "^0.1.5",
"underscore-plus": "^1.6.6"
},
"devDependencies": {
"coffeelint": "^1.9.7",
"fs-plus": ">=2.4.0",
"standard": "^8.3.0",
"temp": ">=0.7.0"
},
"providedServices": {
"autocomplete.watchEditor": {
"versions": {
"1.0.0": "provideWatchEditor"
}
}
},
"consumedServices": {
"autocomplete.provider": {
"versions": {
"1.0.0": "consumeProvider_1",
"1.1.0": "consumeProvider_1_1",
"2.0.0": "consumeProvider_2",
"3.0.0": "consumeProvider_3",
"4.0.0": "consumeProvider_4"
}
},
"snippets": {
"versions": {
"0.1.0": "consumeSnippets"
}
}
},
"configSchema": {
"enableAutoActivation": {
"title": "Show Suggestions On Keystroke",
"description": "Suggestions will show as you type if this preference is enabled. If it is disabled, you can still see suggestions by using the keymapping for 'autocomplete-plus:activate' (shown below).",
"type": "boolean",
"default": true,
"order": 1
},
"autoActivationDelay": {
"title": "Delay Before Suggestions Are Shown",
"description": "If you are experiencing performance issues when typing, you should try increasing this value to a non-zero number (e.g. 100).",
"type": "integer",
"default": 0,
"order": 2
},
"maxVisibleSuggestions": {
"title": "Maximum Visible Suggestions",
"description": "The suggestion list will only show this many suggestions.",
"type": "integer",
"default": 10,
"minimum": 1,
"order": 3
},
"confirmCompletion": {
"title": "Keymap For Confirming A Suggestion",
"description": "You should use the key(s) indicated here to confirm a suggestion from the suggestion list and have it inserted into the file.",
"type": "string",
"default": "tab and enter",
"enum": [
"tab",
"enter",
"tab and enter",
"tab always, enter when suggestion explicitly selected",
"none"
],
"order": 4
},
"useCoreMovementCommands": {
"title": "Use Core Movement Commands",
"description": "Disable this if you want to bind your own keystrokes to move around the suggestion list. You will also need to add definitions to your keymap. See: https://github.com/atom/autocomplete-plus#remapping-movement-commands",
"type": "boolean",
"default": true,
"order": 5
},
"fileBlacklist": {
"title": "File Blacklist",
"description": "Suggestions will not be provided for files matching this list, e.g. `*.md` for Markdown files. To blacklist more than one file extension, use comma as a separator, e.g. `*.md, *.txt` (both Markdown and text files).",
"type": "array",
"default": [
".*"
],
"items": {
"type": "string"
},
"order": 6
},
"scopeBlacklist": {
"title": "Scope Blacklist",
"description": "Suggestions will not be provided for scopes matching this list. See: http://flight-manual.atom.io/behind-atom/sections/scoped-settings-scopes-and-scope-descriptors/",
"type": "array",
"default": [],
"items": {
"type": "string"
},
"order": 7
},
"includeCompletionsFromAllBuffers": {
"title": "Include Completions From All Buffers",
"description": "For grammars with no registered provider(s), the default provider will include completions from all buffers, instead of just the buffer you are currently editing.",
"type": "boolean",
"default": true,
"order": 8
},
"strictMatching": {
"title": "Use Strict Matching For Built-In Provider",
"description": "Fuzzy searching is performed if this is disabled; if it is enabled, suggestions must begin with the prefix from the current word.",
"type": "boolean",
"default": false,
"order": 9
},
"minimumWordLength": {
"description": "Only autocomplete when you've typed at least this many characters. Note: May not affect external providers.",
"type": "integer",
"default": 3,
"order": 10
},
"extraWordCharacters": {
"description": "Characters to consider part of words for the purpose of autocomplete, even if they are included in the editor.nonWordCharacters setting",
"type": "string",
"default": "",
"order": 10.5
},
"enableBuiltinProvider": {
"title": "Enable Built-In Provider",
"description": "The package comes with a built-in provider that will provide suggestions using the words in your current buffer or all open buffers. You will get better suggestions by installing additional autocomplete+ providers. To stop using the built-in provider, disable this option.",
"type": "boolean",
"default": true,
"order": 11
},
"builtinProviderBlacklist": {
"title": "Built-In Provider Blacklist",
"description": "Don't use the built-in provider for these selector(s).",
"type": "string",
"default": ".source.gfm",
"order": 12
},
"backspaceTriggersAutocomplete": {
"title": "Allow Backspace To Trigger Autocomplete",
"description": "If enabled, typing `backspace` will show the suggestion list if suggestions are available. If disabled, suggestions will not be shown while backspacing.",
"type": "boolean",
"default": false,
"order": 13
},
"enableAutoConfirmSingleSuggestion": {
"title": "Automatically Confirm Single Suggestion",
"description": "If enabled, automatically insert suggestion on manual activation with 'autocomplete-plus:activate' when there is only one match.",
"type": "boolean",
"default": true,
"order": 14
},
"suggestionListFollows": {
"title": "Suggestions List Follows",
"description": "With 'Cursor' the suggestion list appears at the cursor's position. With 'Word' it appears at the beginning of the word that's being completed.",
"type": "string",
"default": "Word",
"enum": [
"Word",
"Cursor"
],
"order": 15
},
"suppressActivationForEditorClasses": {
"title": "Suppress Activation For Editor Classes",
"description": "Don't auto-activate when any of these classes are present in the editor.",
"type": "array",
"default": [
"vim-mode.command-mode",
"vim-mode.visual-mode",
"vim-mode.operator-pending-mode"
],
"items": {
"type": "string"
},
"order": 17
},
"consumeSuffix": {
"title": "Consume suggestion text following the cursor",
"description": "Completing a suggestion consumes text following the cursor matching the suffix of the chosen suggestion.",
"type": "boolean",
"default": true,
"order": 18
},
"useAlternateScoring": {
"description": "Prefers runs of consecutive characters, acronyms and start of words. (Experimental)",
"type": "boolean",
"default": true,
"order": 19
},
"useLocalityBonus": {
"description": "Gives words near the cursor position a higher score than those far away",
"type": "boolean",
"default": true,
"order": 20
},
"enableExtendedUnicodeSupport": {
"description": "Identifies non-latin alphabet characters as letters",
"type": "boolean",
"default": false,
"order": 21
},
"similarSuggestionRemoval": {
"description": "Should similar suggestions be removed from the list? If so how to determine they are similar.",
"type": "string",
"default": "none",
"enum": [
{
"value": "none",
"description": "Do not remove similar suggestions"
},
{
"value": "textOrSnippet",
"description": "Remove when the insertion text or snippet match"
}
],
"order": 22
},
"moveToCancel": {
"description": "Moving up when the first item is selected or down when the last item is selected cancels the suggestion list.",
"type": "boolean",
"default": false,
"order": 23
}
},
"standard": {
"globals": [
"CustomEvent",
"HTMLElement",
"advanceClock",
"atom",
"getComputedStyle",
"waitsForPromise"
],
"ignore": [
"**/spec/fixtures/**"
]
}
}

View File

@ -0,0 +1,114 @@
/* eslint-env jasmine */
const { waitForAutocomplete, timeoutPromise, conditionPromise } = require('./spec-helper')
const path = require('path')
describe('Async providers', () => {
let editorView, editor, mainModule, autocompleteManager, registration
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('editor.fontSize', '16')
// Set the completion delay
atom.config.set('autocomplete-plus.autoActivationDelay', 100)
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
editor = await atom.workspace.open('sample.js')
await atom.packages.activatePackage('language-javascript')
// Activate the package
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() => {
autocompleteManager = mainModule.autocompleteManager
return autocompleteManager
})
})
afterEach(() => {
if (registration) {
registration.dispose()
}
})
describe('when an async provider is registered', () => {
beforeEach(() => {
let testAsyncProvider = {
getSuggestions (options) {
return new Promise((resolve) => {
setTimeout(() => {
resolve(
[{
text: 'asyncProvided',
replacementPrefix: 'asyncProvided',
rightLabel: 'asyncProvided'
}]
)
}, 10)
})
},
scopeSelector: '.source.js'
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testAsyncProvider)
})
it('should provide completions when a provider returns a promise that results in an array of suggestions', async () => {
editor.moveToBottom()
editor.insertText('o')
await waitForAutocomplete(editor)
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
expect(suggestionListView.element.querySelector('li .right-label')).toHaveText('asyncProvided')
})
})
describe('when a provider takes a long time to provide suggestions', () => {
beforeEach(() => {
let testAsyncProvider = {
scopeSelector: '.source.js',
getSuggestions (options) {
return new Promise(resolve => {
setTimeout(() =>
resolve(
[{
text: 'asyncProvided',
replacementPrefix: 'asyncProvided',
rightLabel: 'asyncProvided'
}]
)
, 1000)
})
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testAsyncProvider)
})
it('does not show the suggestion list when it is triggered then no longer needed', async () => {
editorView = atom.views.getView(editor)
editor.moveToBottom()
editor.insertText('o')
// Waiting will kick off the suggestion request
editor.insertText('\r')
await timeoutPromise(100)
// Expect nothing because the provider has not come back yet
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
// Wait til the longass provider comes back
await timeoutPromise(1000)
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
})
})
})

View File

@ -0,0 +1,109 @@
'use babel'
/* eslint-env jasmine */
let temp = require('temp').track()
import path from 'path'
import fs from 'fs-plus'
import { conditionPromise } from './spec-helper'
describe('Autocomplete Manager', () => {
let directory
let filePath
let editorView
let editor
let mainModule
let autocompleteManager
let createSuggestionsPromise
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
directory = temp.mkdirSync()
let sample = `var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};
`
filePath = path.join(directory, 'sample.js')
fs.writeFileSync(filePath, sample)
// Enable autosave
atom.config.set('autosave.enabled', true)
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('editor.fontSize', '16')
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
await atom.packages.activatePackage('autosave')
editor = await atom.workspace.open(filePath)
editorView = atom.views.getView(editor)
await atom.packages.activatePackage('language-javascript')
// Activate the package
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() =>
mainModule && mainModule.autocompleteManager && mainModule.autocompleteManager.ready
)
autocompleteManager = mainModule.autocompleteManager
let { displaySuggestions } = autocompleteManager
const suggestionsPromises = new Set()
createSuggestionsPromise = function () {
return new Promise(resolve => {
suggestionsPromises.add(resolve)
})
}
spyOn(autocompleteManager, 'displaySuggestions').andCallFake((suggestions, options) => {
displaySuggestions(suggestions, options)
for (const resolve of suggestionsPromises) {
resolve()
}
suggestionsPromises.clear()
})
})
describe('autosave compatibility', () =>
it('keeps the suggestion list open while saving', async () => {
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
// Trigger an autocompletion
const firstEventPromise = createSuggestionsPromise()
editor.moveToBottom()
editor.moveToBeginningOfLine()
editor.insertText('f')
await firstEventPromise
const secondEventPromise = createSuggestionsPromise()
editor.save()
expect(editorView.querySelector('.autocomplete-plus')).toExist()
editor.insertText('u')
await secondEventPromise
editor.save()
expect(editorView.querySelector('.autocomplete-plus')).toExist()
// Accept suggestion
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
atom.commands.dispatch(suggestionListView.element, 'autocomplete-plus:confirm')
expect(editor.getBuffer().getLastLine()).toEqual('function')
})
)
})

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,58 @@
'use babel'
/* eslint-env jasmine */
import { conditionPromise, waitForAutocomplete } from './spec-helper'
import path from 'path'
describe('Autocomplete Manager', () => {
let editorView
let editor
let mainModule
beforeEach(() => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('editor.fontSize', '16')
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
})
describe('Undo a completion', () => {
beforeEach(async () => {
jasmine.useRealClock()
atom.config.set('autocomplete-plus.enableAutoActivation', true)
editor = await atom.workspace.open('sample.js')
await atom.packages.activatePackage('language-javascript')
// Activate the package
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() =>
mainModule.autocompleteManager && mainModule.autocompleteManager.ready
)
})
it('restores the previous state', async () => {
// Trigger an autocompletion
editor.moveToBottom()
editor.moveToBeginningOfLine()
editor.insertText('f')
await waitForAutocomplete(editor)
// Accept suggestion
editorView = atom.views.getView(editor)
atom.commands.dispatch(editorView, 'autocomplete-plus:confirm')
expect(editor.getBuffer().getLastLine()).toEqual('function')
editor.undo()
expect(editor.getBuffer().getLastLine()).toEqual('f')
})
})
})

View File

@ -0,0 +1,4 @@
This is a simple markdown file. Autocompletion should not work here.
====================================================================
![](http://i.lvme.me/xmeh35.jpg)

View File

@ -0,0 +1,5 @@
body {
font-size: 1234px;
width: 110%;
font-weight: bold !important;
}

View File

@ -0,0 +1,7 @@
SomeModule = require 'some-module'
class MyClass extends SomeModule
constructor: ->
quicksort: ->
# do quicksort here

View File

@ -0,0 +1,13 @@
var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};

View File

@ -0,0 +1 @@
Some text.

View File

@ -0,0 +1,15 @@
var thisIsAReallyReallyReallyLongCompletion = function () { };
var quicksort = function () {
var sort = function(items) {
if (items.length <= 1) return items;
var pivot = items.shift(), current, left = [], right = [];
while(items.length > 0) {
current = items.shift();
current < pivot ? left.push(current) : right.push(current);
}
return sort(left).concat(pivot).concat(sort(right));
};
return sort(Array.apply(this, arguments));
};

View File

@ -0,0 +1,60 @@
'use babel'
/* eslint-env jasmine */
import { conditionPromise, waitForAutocomplete } from './spec-helper'
import path from 'path'
describe('Autocomplete', () => {
let editorView
let editor
let autocompleteManager
let mainModule
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('autocomplete-plus.fileBlacklist', ['.*', '*.md'])
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
editor = await atom.workspace.open('sample.js')
await atom.packages.activatePackage('language-javascript')
// Activate the package
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() =>
mainModule.autocompleteManager && mainModule.autocompleteManager.ready
)
autocompleteManager = mainModule.autocompleteManager
editorView = atom.views.getView(editor)
})
describe('@activate()', () =>
it('activates autocomplete and initializes AutocompleteManager', () => {
expect(autocompleteManager).toBeDefined()
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
})
)
describe('@deactivate()', () => {
it('removes all autocomplete views', async () => {
// Trigger an autocompletion
editor.moveToBottom()
editor.insertText('A')
await waitForAutocomplete(editor)
expect(editorView.querySelector('.autocomplete-plus')).toExist()
// Deactivate the package
await atom.packages.deactivatePackage('autocomplete-plus')
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
})
})
})

View File

@ -0,0 +1,340 @@
'use babel'
/* eslint-env jasmine */
import { triggerAutocompletion, waitForAutocomplete, conditionPromise } from './spec-helper'
import grim from 'grim'
import path from 'path'
describe('Provider API Legacy', () => {
let [editor, mainModule, autocompleteManager, registration, testProvider] = []
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
jasmine.snapshotDeprecations()
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('editor.fontSize', '16')
// Set the completion delay
atom.config.set('autocomplete-plus.autoActivationDelay', 100)
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
await atom.packages.activatePackage('language-javascript')
editor = await atom.workspace.open('sample.js')
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() => {
autocompleteManager = mainModule.autocompleteManager
return autocompleteManager
})
})
afterEach(() => {
if (registration && registration.dispose) {
registration.dispose()
}
registration = null
if (testProvider && testProvider.dispose) {
testProvider.dispose()
}
testProvider = null
jasmine.restoreDeprecationsSnapshot()
})
describe('Provider with API v2.0 registered as 3.0', () =>
it('throws exceptions for renamed provider properties on registration', () => {
expect(() =>
mainModule.consumeProvider_3_0({
selector: '*',
getSuggestions () {}
})
).toThrow()
expect(() =>
mainModule.consumeProvider_3_0({
disableForSelector: '*',
getSuggestions () {}
})
).toThrow()
})
)
describe('Provider with API v1.0 registered as 2.0', () => {
it('raises deprecations for provider attributes on registration', () => {
let numberDeprecations = grim.getDeprecationsLength()
class SampleProvider {
constructor () {
this.id = 'sample-provider'
this.selector = '.source.js,.source.coffee'
this.blacklist = '.comment'
}
requestHandler (options) { return [{word: 'ohai', prefix: 'ohai'}] }
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', new SampleProvider())
expect(grim.getDeprecationsLength() - numberDeprecations).toBe(3)
let deprecations = grim.getDeprecations()
let deprecation = deprecations[deprecations.length - 3]
expect(deprecation.getMessage()).toContain('`id`')
expect(deprecation.getMessage()).toContain('SampleProvider')
deprecation = deprecations[deprecations.length - 2]
expect(deprecation.getMessage()).toContain('`requestHandler`')
deprecation = deprecations[deprecations.length - 1]
expect(deprecation.getMessage()).toContain('`blacklist`')
})
it('raises deprecations when old API parameters are used in the 2.0 API', async () => {
class SampleProvider {
constructor () {
this.selector = '.source.js,.source.coffee'
}
getSuggestions (options) {
return [{
word: 'ohai',
prefix: 'ohai',
label: '<span style="color: red">ohai</span>',
renderLabelAsHtml: true,
className: 'ohai'
}
]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', new SampleProvider())
let numberDeprecations = grim.getDeprecationsLength()
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
expect(grim.getDeprecationsLength() - numberDeprecations).toBe(3)
let deprecations = grim.getDeprecations()
let deprecation = deprecations[deprecations.length - 3]
expect(deprecation.getMessage()).toContain('`word`')
expect(deprecation.getMessage()).toContain('SampleProvider')
deprecation = deprecations[deprecations.length - 2]
expect(deprecation.getMessage()).toContain('`prefix`')
deprecation = deprecations[deprecations.length - 1]
expect(deprecation.getMessage()).toContain('`label`')
})
it('raises deprecations when hooks are passed via each suggestion', async () => {
class SampleProvider {
constructor () {
this.selector = '.source.js,.source.coffee'
}
getSuggestions (options) {
return [{
text: 'ohai',
replacementPrefix: 'ohai',
onWillConfirm () {},
onDidConfirm () {}
}
]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', new SampleProvider())
let numberDeprecations = grim.getDeprecationsLength()
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
expect(grim.getDeprecationsLength() - numberDeprecations).toBe(2)
let deprecations = grim.getDeprecations()
let deprecation = deprecations[deprecations.length - 2]
expect(deprecation.getMessage()).toContain('`onWillConfirm`')
expect(deprecation.getMessage()).toContain('SampleProvider')
deprecation = deprecations[deprecations.length - 1]
expect(deprecation.getMessage()).toContain('`onDidConfirm`')
})
})
describe('Provider API v1.1.0', () =>
it('registers the provider specified by {providers: [provider]}', () => {
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
testProvider = {
selector: '.source.js,.source.coffee',
requestHandler (options) { return [{word: 'ohai', prefix: 'ohai'}] }
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '1.1.0', {providers: [testProvider]})
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
})
)
describe('Provider API v1.0.0', () => {
let [registration1, registration2, registration3] = []
afterEach(() => {
if (registration1) {
registration1.dispose()
}
if (registration2) {
registration2.dispose()
}
if (registration3) {
registration3.dispose()
}
})
it('passes the correct parameters to requestHandler', async () => {
testProvider = {
selector: '.source.js,.source.coffee',
requestHandler (options) { return [ {word: 'ohai', prefix: 'ohai'} ] }
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '1.0.0', {provider: testProvider})
spyOn(testProvider, 'requestHandler')
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let args = testProvider.requestHandler.mostRecentCall.args[0]
expect(args.editor).toBeDefined()
expect(args.buffer).toBeDefined()
expect(args.cursor).toBeDefined()
expect(args.position).toBeDefined()
expect(args.scope).toBeDefined()
expect(args.scopeChain).toBeDefined()
expect(args.prefix).toBeDefined()
})
it('should allow registration of a provider', async () => {
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
testProvider = {
requestHandler (options) {
return [{
word: 'ohai',
prefix: 'ohai',
label: '<span style="color: red">ohai</span>',
renderLabelAsHtml: true,
className: 'ohai'
}
]
},
selector: '.source.js,.source.coffee'
}
// Register the test provider
registration = atom.packages.serviceHub.provide('autocomplete.provider', '1.0.0', {provider: testProvider})
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.go')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
expect(suggestionListView.element.querySelector('li .right-label')).toHaveHtml('<span style="color: red">ohai</span>')
expect(suggestionListView.element.querySelector('li')).toHaveClass('ohai')
})
it('should dispose a provider registration correctly', () => {
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
testProvider = {
requestHandler (options) {
return [{
word: 'ohai',
prefix: 'ohai'
}]
},
selector: '.source.js,.source.coffee'
}
// Register the test provider
registration = atom.packages.serviceHub.provide('autocomplete.provider', '1.0.0', {provider: testProvider})
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.go')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
registration.dispose()
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
registration.dispose()
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
})
it('should remove a providers registration if the provider is disposed', () => {
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
testProvider = {
requestHandler (options) {
return [{
word: 'ohai',
prefix: 'ohai'
}]
},
selector: '.source.js,.source.coffee',
dispose () { }
}
// Register the test provider
registration = atom.packages.serviceHub.provide('autocomplete.provider', '1.0.0', {provider: testProvider})
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(2)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[1]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.go')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
testProvider.dispose()
expect(autocompleteManager.providerManager.store).toBeDefined()
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.coffee')[0]).toEqual(autocompleteManager.providerManager.defaultProvider)
})
})
})

View File

@ -0,0 +1,293 @@
'use babel'
/* eslint-env jasmine */
import {waitForAutocomplete, triggerAutocompletion, conditionPromise} from './spec-helper'
import path from 'path'
describe('Provider API', () => {
let [editor, mainModule, autocompleteManager, registration, testProvider, testProvider2] = []
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('editor.fontSize', '16')
// Set the completion delay
atom.config.set('autocomplete-plus.autoActivationDelay', 100)
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
// Activate the package
await atom.packages.activatePackage('language-javascript')
editor = await atom.workspace.open('sample.js')
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() => {
autocompleteManager = mainModule.autocompleteManager
return autocompleteManager
})
})
afterEach(() => {
if (registration && registration.dispose) {
registration.dispose()
}
registration = null
if (testProvider && testProvider.dispose) {
testProvider.dispose()
}
testProvider = null
})
describe('Provider API v2.0.0', () => {
describe('common functionality', () => {
it('registers the provider specified by [provider]', () => {
testProvider = {
scopeSelector: '.source.js,.source.coffee',
getSuggestions (options) { return [{text: 'ohai', replacementPrefix: 'ohai'}] }
}
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', [testProvider])
return expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
})
it('registers the provider specified by the naked provider', () => {
testProvider = {
scopeSelector: '.source.js,.source.coffee',
getSuggestions (options) { return [{text: 'ohai', replacementPrefix: 'ohai'}] }
}
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
})
it('registers the provider under the given list of labels, the default being [\'workspace-center\']', () => {
testProvider = {
scopeSelector: '.source.js,.source.coffee',
getSuggestions (options) { return [{text: 'ohai', replacementPrefix: 'ohai'}] }
}
testProvider2 = {
labels: ['testProvider2'],
scopeSelector: '.source.js,.source.coffee',
getSuggestions (options) { return [{text: 'ohai', replacementPrefix: 'ohai'}] }
}
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
expect(autocompleteManager.providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider2)
expect(autocompleteManager.providerManager.applicableProviders(['testProvider2'], '.source.js').length).toEqual(1)
expect(autocompleteManager.providerManager.applicableProviders(['testProvider2', 'workspace-center'], '.source.js').length).toEqual(3)
})
it('passes the correct parameters to getSuggestions for the version', async () => {
testProvider = {
scopeSelector: '.source.js,.source.coffee',
getSuggestions (options) { return [{text: 'ohai', replacementPrefix: 'ohai'}] }
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
spyOn(testProvider, 'getSuggestions')
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let args = testProvider.getSuggestions.mostRecentCall.args[0]
expect(args.editor).toBeDefined()
expect(args.bufferPosition).toBeDefined()
expect(args.scopeDescriptor).toBeDefined()
expect(args.prefix).toBeDefined()
expect(args.scope).not.toBeDefined()
expect(args.scopeChain).not.toBeDefined()
expect(args.buffer).not.toBeDefined()
expect(args.cursor).not.toBeDefined()
})
it('correctly displays the suggestion options', async () => {
testProvider = {
scopeSelector: '.source.js, .source.coffee',
getSuggestions (options) {
return [{
text: 'ohai',
replacementPrefix: 'o',
rightLabelHTML: '<span style="color: red">ohai</span>',
description: 'There be documentation'
}]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
expect(suggestionListView.element.querySelector('li .right-label')).toHaveHtml('<span style="color: red">ohai</span>')
expect(suggestionListView.element.querySelector('.word')).toHaveText('ohai')
expect(suggestionListView.element.querySelector('.suggestion-description-content')).toHaveText('There be documentation')
expect(suggestionListView.element.querySelector('.suggestion-description-more-link').style.display).toBe('none')
})
it('favors the `displayText` over text or snippet suggestion options', async () => {
testProvider = {
scopeSelector: '.source.js, .source.coffee',
getSuggestions (options) {
return [{
text: 'ohai',
snippet: 'snippet',
displayText: 'displayOHAI',
replacementPrefix: 'o',
rightLabelHTML: '<span style="color: red">ohai</span>',
description: 'There be documentation'
}]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
expect(suggestionListView.element.querySelector('.word')).toHaveText('displayOHAI')
})
it('correctly displays the suggestion description and More link', async () => {
testProvider = {
scopeSelector: '.source.js, .source.coffee',
getSuggestions (options) {
return [{
text: 'ohai',
replacementPrefix: 'o',
rightLabelHTML: '<span style="color: red">ohai</span>',
description: 'There be documentation',
descriptionMoreURL: 'http://google.com'
}]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
let suggestionListView = autocompleteManager.suggestionList.suggestionListElement
let content = suggestionListView.element.querySelector('.suggestion-description-content')
let moreLink = suggestionListView.element.querySelector('.suggestion-description-more-link')
expect(content).toHaveText('There be documentation')
expect(moreLink).toHaveText('More..')
expect(moreLink.style.display).toBe('inline')
expect(moreLink.getAttribute('href')).toBe('http://google.com')
})
it('it calls getSuggestionDetailsOnSelect if available and replaces suggestion', async () => {
testProvider = {
scopeSelector: '.source.js, .source.coffee',
getSuggestions (options) {
return [{
text: 'ohai'
}]
},
getSuggestionDetailsOnSelect (suggestion) {
return Object.assign({}, suggestion, {description: 'foo'})
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
triggerAutocompletion(editor, true, 'o')
await waitForAutocomplete(editor)
expect(autocompleteManager.suggestionList.items[0].description).toBe('foo')
})
})
describe('when the filterSuggestions option is set to true', () => {
let getSuggestions = () => autocompleteManager.suggestionList.items.map(({text}) => ({text}))
beforeEach(() => editor.setText(''))
it('filters suggestions based on the default prefix', async () => {
testProvider = {
scopeSelector: '.source.js',
filterSuggestions: true,
getSuggestions (options) {
return [
{text: 'okwow'},
{text: 'ohai'},
{text: 'ok'},
{text: 'cats'},
{text: 'something'}
]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
editor.insertText('o')
editor.insertText('k')
await waitForAutocomplete(editor)
expect(getSuggestions()).toEqual([
{text: 'ok'},
{text: 'okwow'}
])
})
it('filters suggestions based on the specified replacementPrefix for each suggestion', async () => {
testProvider = {
scopeSelector: '.source.js',
filterSuggestions: true,
getSuggestions (options) {
return [
{text: 'ohai'},
{text: 'hai'},
{text: 'okwow', replacementPrefix: 'k'},
{text: 'ok', replacementPrefix: 'nope'},
{text: '::cats', replacementPrefix: '::c'},
{text: 'something', replacementPrefix: 'sm'}
]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
editor.insertText('h')
await waitForAutocomplete(editor)
expect(getSuggestions()).toEqual([
{text: '::cats'},
{text: 'hai'},
{text: 'something'}
])
})
it('allows all suggestions when the prefix is an empty string / space', async () => {
testProvider = {
scopeSelector: '.source.js',
filterSuggestions: true,
getSuggestions (options) {
return [
{text: 'ohai'},
{text: 'hai'},
{text: 'okwow', replacementPrefix: ' '},
{text: 'ok', replacementPrefix: 'nope'}
]
}
}
registration = atom.packages.serviceHub.provide('autocomplete.provider', '2.0.0', testProvider)
editor.insertText('h')
editor.insertText(' ')
await waitForAutocomplete(editor)
expect(getSuggestions()).toEqual([
{text: 'ohai'},
{text: 'hai'},
{text: 'okwow'}
])
})
})
})
})

View File

@ -0,0 +1,555 @@
'use babel'
/* eslint-env jasmine */
import ProviderManager from '../lib/provider-manager'
describe('Provider Manager', () => {
let [providerManager, testProvider, registration] = []
beforeEach(() => {
atom.config.set('autocomplete-plus.enableBuiltinProvider', true)
providerManager = new ProviderManager()
providerManager.initialize()
testProvider = {
getSuggestions (options) {
return [{
text: 'ohai',
replacementPrefix: 'ohai'
}]
},
scopeSelector: '.source.js',
dispose () {}
}
})
afterEach(() => {
if (registration && registration.dispose) {
registration.dispose()
}
registration = null
if (testProvider && testProvider.dispose) {
testProvider.dispose()
}
testProvider = null
if (providerManager && providerManager.dispose) {
providerManager.dispose()
}
providerManager = null
})
describe('when no providers have been registered, and enableBuiltinProvider is true', () => {
beforeEach(() => atom.config.set('autocomplete-plus.enableBuiltinProvider', true))
it('is constructed correctly', () => {
expect(providerManager.providers).toBeDefined()
expect(providerManager.subscriptions).toBeDefined()
expect(providerManager.defaultProvider).toBeDefined()
})
it('disposes correctly', () => {
providerManager.dispose()
expect(providerManager.providers).toBeNull()
expect(providerManager.subscriptions).toBeNull()
expect(providerManager.defaultProvider).toBeNull()
})
it('registers the default provider for all scopes', () => {
expect(providerManager.applicableProviders(['workspace-center'], '*').length).toBe(1)
expect(providerManager.applicableProviders(['workspace-center'], '*')[0]).toBe(providerManager.defaultProvider)
})
it('adds providers', () => {
expect(providerManager.isProviderRegistered(testProvider)).toEqual(false)
expect(hasDisposable(providerManager.subscriptions, testProvider)).toBe(false)
providerManager.addProvider(testProvider, 3)
expect(providerManager.isProviderRegistered(testProvider)).toEqual(true)
let apiVersion = providerManager.apiVersionForProvider(testProvider)
expect(apiVersion).toEqual(3)
expect(hasDisposable(providerManager.subscriptions, testProvider)).toBe(true)
})
it('removes providers', () => {
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
expect(hasDisposable(providerManager.subscriptions, testProvider)).toBe(false)
providerManager.addProvider(testProvider)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
expect(hasDisposable(providerManager.subscriptions, testProvider)).toBe(true)
providerManager.removeProvider(testProvider)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
expect(hasDisposable(providerManager.subscriptions, testProvider)).toBe(false)
})
it('can identify a provider with a missing getSuggestions', () => {
let bogusProvider = {
badgetSuggestions (options) {},
scopeSelector: '.source.js',
dispose () {}
}
expect(providerManager.isValidProvider({}, 3)).toEqual(false)
expect(providerManager.isValidProvider(bogusProvider, 3)).toEqual(false)
expect(providerManager.isValidProvider(testProvider, 3)).toEqual(true)
})
it('can identify a provider with an invalid getSuggestions', () => {
let bogusProvider = {
getSuggestions: 'yo, this is a bad handler',
scopeSelector: '.source.js',
dispose () {}
}
expect(providerManager.isValidProvider({}, 3)).toEqual(false)
expect(providerManager.isValidProvider(bogusProvider, 3)).toEqual(false)
expect(providerManager.isValidProvider(testProvider, 3)).toEqual(true)
})
it('can identify a provider with a missing scope selector', () => {
let bogusProvider = {
getSuggestions (options) {},
aSelector: '.source.js',
dispose () {}
}
expect(providerManager.isValidProvider(bogusProvider, 3)).toEqual(false)
expect(providerManager.isValidProvider(testProvider, 3)).toEqual(true)
})
it('can identify a provider with an invalid scope selector', () => {
let bogusProvider = {
getSuggestions (options) {},
scopeSelector: '',
dispose () {}
}
expect(providerManager.isValidProvider(bogusProvider, 3)).toEqual(false)
expect(providerManager.isValidProvider(testProvider, 3)).toEqual(true)
bogusProvider = {
getSuggestions (options) {},
scopeSelector: false,
dispose () {}
}
expect(providerManager.isValidProvider(bogusProvider, 3)).toEqual(false)
})
it('correctly identifies a 1.0 provider', () => {
let bogusProvider = {
selector: '.source.js',
requestHandler: 'yo, this is a bad handler',
dispose () {}
}
expect(providerManager.isValidProvider({}, 1)).toEqual(false)
expect(providerManager.isValidProvider(bogusProvider, 1)).toEqual(false)
let legitProvider = {
selector: '.source.js',
requestHandler () {},
dispose () {}
}
expect(providerManager.isValidProvider(legitProvider, 1)).toEqual(true)
})
it('registers a valid provider', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
})
it('removes a registration', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
registration.dispose()
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
})
it('does not create duplicate registrations for the same scope', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
})
it('does not register an invalid provider', () => {
let bogusProvider = {
getSuggestions: 'yo, this is a bad handler',
scopeSelector: '.source.js',
dispose () {
}
}
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(bogusProvider)).toBe(-1)
expect(providerManager.metadataForProvider(bogusProvider)).toBeFalsy()
registration = providerManager.registerProvider(bogusProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(bogusProvider)).toBe(-1)
expect(providerManager.metadataForProvider(bogusProvider)).toBeFalsy()
})
it('registers a provider with a blacklist', () => {
testProvider = {
getSuggestions (options) {
return [{
text: 'ohai',
replacementPrefix: 'ohai'
}]
},
scopeSelector: '.source.js',
disableForScopeSelector: '.source.js .comment',
dispose () {
}
}
expect(providerManager.isValidProvider(testProvider, 3)).toEqual(true)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(1)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeFalsy()
registration = providerManager.registerProvider(testProvider)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').length).toEqual(2)
expect(providerManager.applicableProviders(['workspace-center'], '.source.js').indexOf(testProvider)).not.toBe(-1)
expect(providerManager.metadataForProvider(testProvider)).toBeTruthy()
})
})
describe('when no providers have been registered, and enableBuiltinProvider is false', () => {
beforeEach(() => atom.config.set('autocomplete-plus.enableBuiltinProvider', false))
it('does not register the default provider for all scopes', () => {
expect(providerManager.applicableProviders(['workspace-center'], '*').length).toBe(0)
expect(providerManager.defaultProvider).toEqual(null)
expect(providerManager.defaultProviderRegistration).toEqual(null)
})
})
describe('when providers have been registered', () => {
let [testProvider1, testProvider2, testProvider3, testProvider4, testProvider5] = []
beforeEach(() => {
atom.config.set('autocomplete-plus.enableBuiltinProvider', true)
providerManager = new ProviderManager()
providerManager.initialize()
testProvider1 = {
scopeSelector: '.source.js',
getSuggestions (options) {
return [{
text: 'ohai2',
replacementPrefix: 'ohai2'
}]
},
dispose () {}
}
testProvider2 = {
scopeSelector: '.source.js .variable.js',
disableForScopeSelector: '.source.js .variable.js .comment2',
providerblacklist: {
'autocomplete-plus-fuzzyprovider': '.source.js .variable.js .comment3'
},
getSuggestions (options) {
return [{
text: 'ohai2',
replacementPrefix: 'ohai2'
}]
},
dispose () {}
}
testProvider3 = {
scopeSelector: '*',
getSuggestions (options) {
return [{
text: 'ohai3',
replacementPrefix: 'ohai3'
}]
},
dispose () {}
}
testProvider4 = {
labels: ['workspace-center', 'label1'],
scopeSelector: '.source.js .comment',
getSuggestions (options) {
return [{
text: 'ohai4',
replacementPrefix: 'ohai4'
}]
},
dispose () {}
}
testProvider5 = {
labels: ['label1'],
scopeSelector: '*',
getSuggestions (options) {
return [{
text: 'ohai5',
replacementPrefix: 'ohai5'
}]
},
dispose () {}
}
providerManager.registerProvider(testProvider1)
providerManager.registerProvider(testProvider2)
providerManager.registerProvider(testProvider3)
providerManager.registerProvider(testProvider4)
providerManager.registerProvider(testProvider5)
})
it('returns providers in the correct order for the given scope chain and labels', () => {
let { defaultProvider } = providerManager
let providers = providerManager.applicableProviders(['workspace-center'], '.source.other')
expect(providers).toHaveLength(2)
expect(providers[0]).toEqual(testProvider3)
expect(providers[1]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(testProvider1)
expect(providers[1]).toEqual(testProvider3)
expect(providers[2]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js .comment')
expect(providers).toHaveLength(4)
expect(providers[0]).toEqual(testProvider4)
expect(providers[1]).toEqual(testProvider1)
expect(providers[2]).toEqual(testProvider3)
expect(providers[3]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js .variable.js')
expect(providers).toHaveLength(4)
expect(providers[0]).toEqual(testProvider2)
expect(providers[1]).toEqual(testProvider1)
expect(providers[2]).toEqual(testProvider3)
expect(providers[3]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js .other.js')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(testProvider1)
expect(providers[1]).toEqual(testProvider3)
expect(providers[2]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['label1'], '.source.js .comment')
expect(providers).toHaveLength(2)
expect(providers[0]).toEqual(testProvider4)
expect(providers[1]).toEqual(testProvider5)
providers = providerManager.applicableProviders(['label1', 'workspace-center'], '.source.js .comment')
expect(providers).toHaveLength(5)
})
it('does not return providers if the scopeChain exactly matches a global blacklist item', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(4)
atom.config.set('autocomplete-plus.scopeBlacklist', ['.source.js .comment'])
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(0)
})
it('does not return providers if the scopeChain matches a global blacklist item with a wildcard', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(4)
atom.config.set('autocomplete-plus.scopeBlacklist', ['.source.js *'])
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(0)
})
it('does not return providers if the scopeChain matches a global blacklist item with a wildcard one level of depth below the current scope', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(4)
atom.config.set('autocomplete-plus.scopeBlacklist', ['.source.js *'])
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment .other')).toHaveLength(0)
})
it('does return providers if the scopeChain does not match a global blacklist item with a wildcard', () => {
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(4)
atom.config.set('autocomplete-plus.scopeBlacklist', ['.source.coffee *'])
expect(providerManager.applicableProviders(['workspace-center'], '.source.js .comment')).toHaveLength(4)
})
it('filters a provider if the scopeChain matches a provider blacklist item', () => {
let { defaultProvider } = providerManager
let providers = providerManager.applicableProviders(['workspace-center'], '.source.js .variable.js .other.js')
expect(providers).toHaveLength(4)
expect(providers[0]).toEqual(testProvider2)
expect(providers[1]).toEqual(testProvider1)
expect(providers[2]).toEqual(testProvider3)
expect(providers[3]).toEqual(defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js .variable.js .comment2.js')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(testProvider1)
expect(providers[1]).toEqual(testProvider3)
expect(providers[2]).toEqual(defaultProvider)
})
it('filters a provider if the scopeChain matches a provider providerblacklist item', () => {
let providers = providerManager.applicableProviders(['workspace-center'], '.source.js .variable.js .other.js')
expect(providers).toHaveLength(4)
expect(providers[0]).toEqual(testProvider2)
expect(providers[1]).toEqual(testProvider1)
expect(providers[2]).toEqual(testProvider3)
expect(providers[3]).toEqual(providerManager.defaultProvider)
providers = providerManager.applicableProviders(['workspace-center'], '.source.js .variable.js .comment3.js')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(testProvider2)
expect(providers[1]).toEqual(testProvider1)
expect(providers[2]).toEqual(testProvider3)
})
})
describe('when inclusion priorities are used', () => {
let [accessoryProvider1, accessoryProvider2, verySpecificProvider, mainProvider, defaultProvider] = []
beforeEach(() => {
atom.config.set('autocomplete-plus.enableBuiltinProvider', true)
providerManager = new ProviderManager()
providerManager.initialize()
defaultProvider = providerManager.defaultProvider
accessoryProvider1 = {
scopeSelector: '*',
inclusionPriority: 2,
getSuggestions (options) {},
dispose () {}
}
accessoryProvider2 = {
scopeSelector: '.source.js',
inclusionPriority: 2,
excludeLowerPriority: false,
getSuggestions (options) {},
dispose () {}
}
verySpecificProvider = {
scopeSelector: '.source.js .comment',
inclusionPriority: 2,
excludeLowerPriority: true,
getSuggestions (options) {},
dispose () {}
}
mainProvider = {
scopeSelector: '.source.js',
inclusionPriority: 1,
excludeLowerPriority: true,
getSuggestions (options) {},
dispose () {}
}
providerManager.registerProvider(accessoryProvider1)
providerManager.registerProvider(accessoryProvider2)
providerManager.registerProvider(verySpecificProvider)
providerManager.registerProvider(mainProvider)
})
it('returns the default provider and higher when nothing with a higher proirity is excluding the lower', () => {
let providers = providerManager.applicableProviders(['workspace-center'], '.source.coffee')
expect(providers).toHaveLength(2)
expect(providers[0]).toEqual(accessoryProvider1)
expect(providers[1]).toEqual(defaultProvider)
})
it('exclude the lower priority provider, the default, when one with a higher proirity excludes the lower', () => {
let providers = providerManager.applicableProviders(['workspace-center'], '.source.js')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(accessoryProvider2)
expect(providers[1]).toEqual(mainProvider)
expect(providers[2]).toEqual(accessoryProvider1)
})
it('excludes the all lower priority providers when multiple providers of lower priority', () => {
let providers = providerManager.applicableProviders(['workspace-center'], '.source.js .comment')
expect(providers).toHaveLength(3)
expect(providers[0]).toEqual(verySpecificProvider)
expect(providers[1]).toEqual(accessoryProvider2)
expect(providers[2]).toEqual(accessoryProvider1)
})
})
describe('when suggestionPriorities are the same', () => {
let [provider1, provider2, provider3] = []
beforeEach(() => {
atom.config.set('autocomplete-plus.enableBuiltinProvider', true)
providerManager = new ProviderManager()
providerManager.initialize()
provider1 = {
scopeSelector: '*',
suggestionPriority: 2,
getSuggestions (options) {},
dispose () {}
}
provider2 = {
scopeSelector: '.source.js',
suggestionPriority: 3,
getSuggestions (options) {},
dispose () {}
}
provider3 = {
scopeSelector: '.source.js .comment',
suggestionPriority: 2,
getSuggestions (options) {},
dispose () {}
}
providerManager.registerProvider(provider1)
providerManager.registerProvider(provider2)
providerManager.registerProvider(provider3)
})
it('sorts by specificity', () => {
let providers = providerManager.applicableProviders(['workspace-center'], '.source.js .comment')
expect(providers).toHaveLength(4)
expect(providers[0]).toEqual(provider2)
expect(providers[1]).toEqual(provider3)
expect(providers[2]).toEqual(provider1)
})
})
})
var hasDisposable = (compositeDisposable, disposable) => {
if (compositeDisposable && compositeDisposable.disposables && compositeDisposable.disposables.has) {
return compositeDisposable.disposables.has(disposable)
}
if (compositeDisposable && compositeDisposable.disposables && compositeDisposable.disposables.indexOf) {
return compositeDisposable.disposables.indexOf(disposable) > -1
}
return false
}

View File

@ -0,0 +1,95 @@
/* eslint-env jasmine */
beforeEach(() => {
spyOn(atom.views, 'readDocument').andCallFake(fn => fn())
spyOn(atom.views, 'updateDocument').andCallFake(fn => fn())
atom.config.set('autocomplete-plus.minimumWordLength', 1)
atom.config.set('autocomplete-plus.suggestionListFollows', 'Word')
atom.config.set('autocomplete-plus.useCoreMovementCommands', true)
atom.config.set('autocomplete-plus.includeCompletionsFromAllBuffers', false)
})
function waitForAutocomplete (editor) {
const editorView = atom.views.getView(editor)
return conditionPromise(
() => editorView.querySelectorAll('.autocomplete-plus li').length > 0
)
}
function waitForAutocompleteToDisappear (editor) {
const editorView = atom.views.getView(editor)
return conditionPromise(
() => editorView.querySelectorAll('.autocomplete-plus li').length === 0
)
}
let triggerAutocompletion = (editor, moveCursor = true, char = 'f') => {
if (moveCursor) {
editor.moveToBottom()
editor.moveToBeginningOfLine()
}
editor.insertText(char)
}
async function waitForDeferredSuggestions (editorView, totalSuggestions) {
await conditionPromise(
() => editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list .suggestion-list-scroller')
)
const scroller = editorView.querySelector('.autocomplete-plus autocomplete-suggestion-list .suggestion-list-scroller')
scroller.scrollTo(0, 100)
scroller.scrollTo(0, 0)
await conditionPromise(
() => editorView.querySelectorAll('.autocomplete-plus li').length === totalSuggestions
)
}
let buildIMECompositionEvent = (event, {data, target} = {}) => {
event = new CustomEvent(event, {bubbles: true})
event.data = data
Object.defineProperty(event, 'target', {get () { return target }})
return event
}
let buildTextInputEvent = ({data, target}) => {
let event = new CustomEvent('textInput', {bubbles: true})
event.data = data
Object.defineProperty(event, 'target', {get () { return target }})
return event
}
async function conditionPromise (condition) {
const startTime = Date.now()
while (true) {
await timeoutPromise(100)
if (await condition()) {
return
}
if (Date.now() - startTime > 5000) {
throw new Error('Timed out waiting on condition')
}
}
}
function timeoutPromise (timeout) {
return new Promise(function (resolve) {
setTimeout(resolve, timeout)
})
}
module.exports = {
conditionPromise,
timeoutPromise,
triggerAutocompletion,
waitForAutocomplete,
waitForAutocompleteToDisappear,
buildIMECompositionEvent,
buildTextInputEvent,
waitForDeferredSuggestions
}

View File

@ -0,0 +1,440 @@
/* eslint-env jasmine */
const { conditionPromise } = require('./spec-helper')
const path = require('path')
let suggestionsForPrefix = async (provider, editor, prefix, options) => {
let bufferPosition = editor.getCursorBufferPosition()
let scopeDescriptor = editor.getLastCursor().getScopeDescriptor()
let suggestions = provider.getSuggestions({editor, bufferPosition, prefix, scopeDescriptor})
if (options && options.raw) {
return suggestions
} else {
if (suggestions) {
return (await suggestions).map(sug => sug.text)
} else {
return []
}
}
}
describe('SubsequenceProvider', () => {
let [editor, mainModule, autocompleteManager, provider] = []
beforeEach(async () => {
atom.workspace.project.setPaths([path.join(__dirname, 'fixtures')]);
jasmine.useRealClock()
// Set to live completion
atom.config.set('autocomplete-plus.enableAutoActivation', true)
atom.config.set('autocomplete-plus.defaultProvider', 'Subsequence')
// Set the completion delay
atom.config.set('autocomplete-plus.autoActivationDelay', 100)
let workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
editor = await atom.workspace.open('sample.js')
await atom.packages.activatePackage('language-javascript')
mainModule = (await atom.packages.activatePackage('autocomplete-plus')).mainModule
await conditionPromise(() =>
mainModule.autocompleteManager && mainModule.autocompleteManager.ready
)
autocompleteManager = mainModule.autocompleteManager
provider = autocompleteManager.providerManager.defaultProvider
})
it('runs a completion ', async () => {
const suggestions = await suggestionsForPrefix(provider, editor, 'quick')
expect(suggestions).toContain('quicksort')
})
it('adds words to the symbol list after they have been written', async () => {
let suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).not.toContain('aNewFunction')
editor.insertText('function aNewFunction(){};')
editor.insertText(' ')
suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).toContain('aNewFunction')
})
it('adds words after they have been added to a scope that is not a direct match for the selector', async () => {
let suggestions = await suggestionsForPrefix(provider, editor, 'some')
expect(suggestions).not.toContain('somestring')
editor.insertText('abc = "somestring"')
editor.insertText(' ')
suggestions = await suggestionsForPrefix(provider, editor, 'some')
expect(suggestions).toContain('somestring')
})
it('removes words from the symbol list when they do not exist in the buffer', async () => {
editor.moveToBottom()
editor.moveToBeginningOfLine()
let suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).not.toContain('aNewFunction')
editor.insertText('function aNewFunction(){};')
editor.moveToEndOfLine()
suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).toContain('aNewFunction')
editor.setCursorBufferPosition([13, 21])
editor.backspace()
editor.moveToTop()
suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).toContain('aNewFunctio')
expect(suggestions).not.toContain('aNewFunction')
})
it('does not return the prefix as a suggestion', async () => {
atom.config.set('editor.nonWordCharacters', '-')
atom.config.set('autocomplete-plus.extraWordCharacters', '-')
editor.moveToBottom()
editor.insertText('--qu')
let suggestions = await suggestionsForPrefix(provider, editor, '--qu')
expect(suggestions).not.toContain('--qu')
})
it('does not return the word under the cursors when are multiple cursors', async () => {
editor.moveToBottom()
editor.setText('\n\n\n')
editor.setCursorBufferPosition([0, 0])
editor.addCursorAtBufferPosition([1, 0])
editor.addCursorAtBufferPosition([2, 0])
editor.insertText('omg')
const suggestions = await suggestionsForPrefix(provider, editor, 'omg')
expect(suggestions).not.toContain('omg')
})
it('does not output suggestions from the other buffer', async () => {
await atom.packages.activatePackage('language-coffee-script')
const coffeeEditor = await atom.workspace.open('sample.coffee')
const suggestions = await suggestionsForPrefix(provider, coffeeEditor, 'item')
expect(suggestions).toHaveLength(0)
})
describe('search range', () => {
it('includes the full range when the buffer is smaller than the max range', () => {
const range = provider.clampedRange(500, 25, 100)
expect(range.start.row).toBeLessThan(0)
expect(range.end.row).toBeGreaterThan(100)
})
it('returns the expected result when cursor is close to end of large buffer', () => {
const range = provider.clampedRange(100, 450, 500)
expect(range.start.row).toBeLessThan(350)
})
it('returns the expected result when cursor is close to beginning of large buffer', () => {
const range = provider.clampedRange(100, 50, 500)
expect(range.end.row).toBeGreaterThan(100)
})
})
describe('when autocomplete-plus.minimumWordLength is > 1', () => {
beforeEach(() => atom.config.set('autocomplete-plus.minimumWordLength', 3))
it('only returns results when the prefix is at least the min word length', async () => {
editor.insertText('function aNewFunction(){};')
const results = await Promise.all([
'',
'a',
'an',
'ane',
'anew'
].map(text => suggestionsForPrefix(provider, editor, text)))
expect(results[0]).not.toContain('aNewFunction')
expect(results[1]).not.toContain('aNewFunction')
expect(results[2]).not.toContain('aNewFunction')
expect(results[3]).toContain('aNewFunction')
expect(results[4]).toContain('aNewFunction')
})
})
describe('when autocomplete-plus.minimumWordLength is 0', () => {
beforeEach(() => atom.config.set('autocomplete-plus.minimumWordLength', 0))
it('only returns results when the prefix is at least the min word length', async () => {
editor.insertText('function aNewFunction(){};')
const testResultPairs = [
['', false],
['a', true],
['an', true],
['ane', true],
['anew', true]
]
const results = await Promise.all(
testResultPairs.map(t => suggestionsForPrefix(provider, editor, t[0]))
)
results.forEach((result, idx) => {
if (testResultPairs[idx][1]) {
expect(result).toContain('aNewFunction')
} else {
expect(result).not.toContain('aNewFunction')
}
})
})
})
describe("when the editor's path changes", () => {
it('continues to track changes on the new path', async () => {
let buffer = editor.getBuffer()
expect(provider.watchedBuffers.get(buffer)).toBe(editor)
let suggestions = await suggestionsForPrefix(provider, editor, 'qu')
expect(suggestions).toContain('quicksort')
buffer.setPath('cats.js')
expect(provider.watchedBuffers.get(buffer)).toBe(editor)
editor.moveToBottom()
editor.moveToBeginningOfLine()
const results = await Promise.all([
suggestionsForPrefix(provider, editor, 'qu'),
suggestionsForPrefix(provider, editor, 'anew')
])
expect(results[0]).toContain('quicksort')
expect(results[1]).not.toContain('aNewFunction')
editor.insertText('function aNewFunction(){};')
suggestions = await suggestionsForPrefix(provider, editor, 'anew')
expect(suggestions).toContain('aNewFunction')
})
})
describe('when editor.nonWordCharacters changes', () => {
it('includes characters that are included in the `autocomplete-plus.extraWordCharacters` setting or not excluded in the `editor.nonWordCharacters` setting', async () => {
const scopeSelector = editor.getLastCursor().getScopeDescriptor().getScopeChain()
editor.insertText('good$noodles good-beef ')
atom.config.set('editor.nonWordCharacters', '$-', {scopeSelector})
let sugs = await suggestionsForPrefix(provider, editor, 'good')
expect(sugs).not.toContain('good$noodles')
expect(sugs).not.toContain('good-beef')
atom.config.set('autocomplete-plus.extraWordCharacters', '-', {scopeSelector})
sugs = await suggestionsForPrefix(provider, editor, 'good')
expect(sugs).toContain('good-beef')
expect(sugs).not.toContain('good$noodles')
atom.config.set('editor.nonWordCharacters', '-', {scopeSelector})
sugs = await suggestionsForPrefix(provider, editor, 'good')
expect(sugs).toContain('good-beef')
expect(sugs).toContain('good$noodles')
})
})
describe('when includeCompletionsFromAllBuffers is enabled', () => {
beforeEach(async () => {
atom.config.set('autocomplete-plus.includeCompletionsFromAllBuffers', true)
await atom.packages.activatePackage('language-coffee-script')
editor = await atom.workspace.open('sample.coffee')
})
afterEach(() => {
atom.config.set('autocomplete-plus.includeCompletionsFromAllBuffers', false)
})
it('outputs unique suggestions', async () => {
editor.setCursorBufferPosition([7, 0])
const suggestions = await suggestionsForPrefix(provider, editor, 'qu')
expect(suggestions).toHaveLength(2)
})
it('outputs suggestions from the other buffer', async () => {
editor.setCursorBufferPosition([7, 0])
const suggestions = await suggestionsForPrefix(provider, editor, 'item')
expect(suggestions[0]).toBe('items')
})
})
// TODO: commenting this out because I'm not sure what it's trying to test
// just remove it? (This was brought over from the symbol provider spec.)
// describe('when the autocomplete.symbols changes between scopes', () => {
// beforeEach(() => {
// editor.setText(`// in-a-comment
// inVar = "in-a-string"`
// )
// waitForBufferToStopChanging()
//
// let commentConfig = {
// incomment: {
// selector: '.comment'
// }
// }
//
// let stringConfig = {
// instring: {
// selector: '.string'
// }
// }
//
// atom.config.set('autocomplete.symbols', commentConfig, {scopeSelector: '.source.js .comment'})
// atom.config.set('autocomplete.symbols', stringConfig, {scopeSelector: '.source.js .string'})
// })
//
// it('uses the config for the scope under the cursor', () => {
// // Using the comment config
// editor.setCursorBufferPosition([0, 2])
//
// waitsForPromise(() =>
// suggestionsForPrefix(provider, editor, 'in', {raw: true}).then(sugs => {
// expect(sugs).toHaveLength(1)
// expect(sugs[0].text).toBe('in-a-comment')
// expect(sugs[0].type).toBe('incomment')
//
// // Using the string config
// editor.setCursorBufferPosition([1, 20])
// editor.insertText(' ')
// waitForBufferToStopChanging()
//
// return suggestionsForPrefix(provider, editor, 'in', {raw: true})
// }).then(sugs => {
// expect(sugs).toHaveLength(1)
// expect(sugs[0].text).toBe('in-a-string')
// expect(sugs[0].type).toBe('instring')
//
// editor.setCursorBufferPosition([1, Infinity])
// editor.insertText(' ')
// waitForBufferToStopChanging()
//
// return suggestionsForPrefix(provider, editor, 'in', {raw: true})
// }).then(sugs => {
// expect(sugs).toHaveLength(3)
// expect(sugs[0].text).toBe('inVar')
// expect(sugs[0].type).toBe('')
// })
// )
// })
// })
//
// describe('when the config contains a list of suggestion strings', () => {
// beforeEach(() => {
// editor.setText('// abcomment')
// waitForBufferToStopChanging()
//
// let commentConfig = {
// comment: { selector: '.comment' },
// builtin: {
// suggestions: ['abcd', 'abcde', 'abcdef']
// }
// }
//
// atom.config.set('autocomplete.symbols', commentConfig, {scopeSelector: '.source.js .comment'})
// })
//
// it('adds the suggestions to the results', () => {
// // Using the comment config
// editor.setCursorBufferPosition([0, 2])
//
// waitsForPromise(() =>
// suggestionsForPrefix(provider, editor, 'ab', {raw: true}).then(suggestions => {
// expect(suggestions).toHaveLength(4)
// expect(suggestions[0].text).toBe('abcomment')
// expect(suggestions[0].type).toBe('comment')
// expect(suggestions[1].text).toBe('abcdef')
// expect(suggestions[1].type).toBe('builtin')
// })
// )
// })
// })
describe('when the symbols config contains a list of suggestion objects', () => {
beforeEach(() => {
editor.setText('// abcomment')
let commentConfig = {
comment: { selector: '.comment' },
builtin: {
suggestions: [
{nope: 'nope1', rightLabel: 'will not be added to the suggestions'},
{text: 'abcd', rightLabel: 'one', type: 'function'},
[]
]
}
}
atom.config.set('autocomplete.symbols', commentConfig, {scopeSelector: '.source.js .comment'})
})
it('adds the suggestion objects to the results', async () => {
// Using the comment config
editor.setCursorBufferPosition([0, 2])
const suggestions = await suggestionsForPrefix(provider, editor, 'ab', {raw: true})
expect(suggestions).toHaveLength(2)
expect(suggestions[0].text).toBe('abcd')
expect(suggestions[0].type).toBe('function')
expect(suggestions[0].rightLabel).toBe('one')
expect(suggestions[1].text).toBe('abcomment')
expect(suggestions[1].type).toBe('comment')
})
})
describe('when the legacy completions array is used', () => {
beforeEach(() => {
editor.setText('// abcomment')
atom.config.set('editor.completions', ['abcd', 'abcde', 'abcdef'], {scopeSelector: '.source.js .comment'})
})
it('uses the config for the scope under the cursor', async () => {
// Using the comment config
editor.setCursorBufferPosition([0, 2])
const suggestions = await suggestionsForPrefix(provider, editor, 'ab', {raw: true})
expect(suggestions).toHaveLength(4)
expect(suggestions[0].text).toBe('abcd')
expect(suggestions[0].type).toBe('builtin')
expect(suggestions[3].text).toBe('abcomment')
expect(suggestions[3].type).toBe('')
})
})
it('adds words to the wordlist with unicode characters', async () => {
atom.config.set('autocomplete-plus.enableExtendedUnicodeSupport', true)
let suggestions = await suggestionsForPrefix(provider, editor, 'somē', {raw: true})
expect(suggestions).toHaveLength(0)
editor.insertText('somēthingNew')
editor.insertText(' ')
suggestions = await suggestionsForPrefix(provider, editor, 'somē', {raw: true})
expect(suggestions).toHaveLength(1)
})
it('does not throw errors when findWordsWithSubsequence jobs are cancelled due to rapid buffer changes', async () => {
editor.setText('')
const promises = []
for (let i = 0; i < 50; i++) {
editor.insertText('x')
promises.push(suggestionsForPrefix(provider, editor, editor.getText(), {raw: true}))
}
await Promise.all(promises)
})
})

View File

@ -0,0 +1,438 @@
/* eslint-env jasmine */
/* eslint-disable no-template-curly-in-string */
const SuggestionListElement = require('../lib/suggestion-list-element')
const fragmentToHtml = fragment => {
const el = document.createElement('span')
el.appendChild(fragment.cloneNode(true))
return el.innerHTML
}
describe('Suggestion List Element', () => {
let [suggestionListElement] = []
beforeEach(() => { suggestionListElement = new SuggestionListElement() })
afterEach(() => {
if (suggestionListElement) {
suggestionListElement.dispose()
}
suggestionListElement = null
})
describe('renderItem', () => {
beforeEach(() => jasmine.attachToDOM(suggestionListElement.element))
it('HTML escapes displayText', () => {
let suggestion = {text: 'Animal<Cat>'}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal&lt;Cat&gt;')
suggestion = {text: 'Animal<Cat>', displayText: 'Animal<Cat>'}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal&lt;Cat&gt;')
suggestion = {snippet: 'Animal<Cat>', displayText: 'Animal<Cat>'}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal&lt;Cat&gt;')
})
it('HTML escapes snippets', () => {
let suggestion = {snippet: 'Animal<Cat>(${1:omg<wow>}, ${2:ok<yeah>})'}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal&lt;Cat&gt;')
expect(suggestionListElement.selectedLi.innerHTML).toContain('omg&lt;wow&gt;')
expect(suggestionListElement.selectedLi.innerHTML).toContain('ok&lt;yeah&gt;')
suggestion = {
snippet: 'Animal<Cat>(${1:omg<wow>}, ${2:ok<yeah>})',
displayText: 'Animal<Cat>(omg<wow>, ok<yeah>)'
}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal&lt;Cat&gt;')
expect(suggestionListElement.selectedLi.innerHTML).toContain('omg&lt;wow&gt;')
expect(suggestionListElement.selectedLi.innerHTML).toContain('ok&lt;yeah&gt;')
})
it('HTML escapes labels', () => {
let suggestion = {text: 'something', leftLabel: 'Animal<Cat>', rightLabel: 'Animal<Dog>'}
suggestionListElement.renderItem(suggestion)
expect(suggestionListElement.selectedLi.querySelector('.left-label').innerHTML).toContain('Animal&lt;Cat&gt;')
return expect(suggestionListElement.selectedLi.querySelector('.right-label').innerHTML).toContain('Animal&lt;Dog&gt;')
})
})
describe('itemChanged', () => {
beforeEach(() => jasmine.attachToDOM(suggestionListElement.element))
it('updates the list item', async () => {
jasmine.useRealClock()
const suggestion = {text: 'foo'}
const newSuggestion = {text: 'foo', description: 'foobar', rightLabel: 'foo'}
suggestionListElement.model = {items: [newSuggestion]}
suggestionListElement.selectedIndex = 0
suggestionListElement.renderItem(suggestion, 0)
expect(suggestionListElement.element.querySelector('.right-label').innerText).toBe('')
suggestionListElement.itemChanged({suggestion: newSuggestion, index: 0})
expect(suggestionListElement.element.querySelector('.right-label').innerText)
.toBe('foo')
expect(suggestionListElement.element.querySelector('.suggestion-description-content').innerText)
.toBe('foobar')
})
})
describe('getDisplayHTML', () => {
it('uses displayText over text or snippet', () => {
let text = 'abcd()'
let snippet
let displayText = 'acd'
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, displayText, replacementPrefix)
expect(fragmentToHtml(html)).toBe('<span class="character-match">a</span>cd')
})
it('handles the empty string in the text field', () => {
let text = ''
let snippet
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('')
})
it('handles the empty string in the snippet field', () => {
let text
let snippet = ''
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('')
})
it('handles an empty prefix', () => {
let text
let snippet = 'abc'
let replacementPrefix = ''
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('abc')
})
it('outputs correct html when there are no snippets in the snippet field', () => {
let text = ''
let snippet = 'abc(d, e)f'
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('<span class="character-match">a</span>bc(d, e)f')
})
it('outputs correct html when there are not character matches', () => {
let text = ''
let snippet = 'abc(d, e)f'
let replacementPrefix = 'omg'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('abc(d, e)f')
})
it('outputs correct html when the text field is used', () => {
let text = 'abc(d, e)f'
let snippet
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('<span class="character-match">a</span>bc(d, e)f')
})
it('replaces a snippet with no escaped right braces', () => {
let text = ''
let snippet = 'abc(${1:d}, ${2:e})f'
let replacementPrefix = 'a'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('<span class="character-match">a</span>bc(<span class="snippet-completion">d</span>, <span class="snippet-completion">e</span>)f')
})
it('replaces a snippet with no escaped right braces', () => {
let text = ''
let snippet = 'text(${1:ab}, ${2:cd})'
let replacementPrefix = 'ta'
let html = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(html)).toBe('<span class="character-match">t</span>ext(<span class="snippet-completion"><span class="character-match">a</span>b</span>, <span class="snippet-completion">cd</span>)')
})
it('replaces a snippet with escaped right braces', () => {
let text = ''
let snippet = 'abc(${1:d}, ${2:e})f ${3:interface{\\}}'
let replacementPrefix = 'a'
let display = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(display)).toBe('<span class="character-match">a</span>bc(<span class="snippet-completion">d</span>, <span class="snippet-completion">e</span>)f <span class="snippet-completion">interface{}</span>')
})
it('replaces a snippet with escaped multiple right braces', () => {
let text = ''
let snippet = 'abc(${1:d}, ${2:something{ok\\}}, ${3:e})f ${4:interface{\\}}'
let replacementPrefix = 'a'
let display = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(display)).toBe('<span class="character-match">a</span>bc(<span class="snippet-completion">d</span>, <span class="snippet-completion">something{ok}</span>, <span class="snippet-completion">e</span>)f <span class="snippet-completion">interface{}</span>')
})
it('replaces a snippet with elements that have no text', () => {
let text = ''
let snippet = 'abc(${1:d}, ${2:e})f${3}'
let replacementPrefix = 'a'
let display = suggestionListElement.getDisplayFragment(text, snippet, null, replacementPrefix)
expect(fragmentToHtml(display)).toBe('<span class="character-match">a</span>bc(<span class="snippet-completion">d</span>, <span class="snippet-completion">e</span>)f')
})
})
describe('findCharacterMatches', () => {
let assertMatches = (text, replacementPrefix, truthyIndices) => {
text = suggestionListElement.removeEmptySnippets(text)
let snippets = suggestionListElement.snippetParser.findSnippets(text)
text = suggestionListElement.removeSnippetsFromText(snippets, text)
let matches = suggestionListElement.findCharacterMatchIndices(text, replacementPrefix)
for (var i = 0; i <= text.length; i++) {
if (truthyIndices.indexOf(i) !== -1) {
expect(matches[i]).toBeTruthy()
} else {
let m = matches
if (m) {
m = m[i]
}
expect(m).toBeFalsy()
}
}
}
it('finds matches when no snippets exist', () => {
assertMatches('hello', '', [])
assertMatches('hello', 'h', [0])
assertMatches('hello', 'hl', [0, 2])
assertMatches('hello', 'hlo', [0, 2, 4])
})
it('finds matches when snippets exist', () => {
assertMatches('${0:hello}', '', [])
assertMatches('${0:hello}', 'h', [0])
assertMatches('${0:hello}', 'hl', [0, 2])
assertMatches('${0:hello}', 'hlo', [0, 2, 4])
assertMatches('${0:hello}world', '', [])
assertMatches('${0:hello}world', 'h', [0])
assertMatches('${0:hello}world', 'hw', [0, 5])
assertMatches('${0:hello}world', 'hlw', [0, 2, 5])
assertMatches('hello${0:world}', '', [])
assertMatches('hello${0:world}', 'h', [0])
assertMatches('hello${0:world}', 'hw', [0, 5])
assertMatches('hello${0:world}', 'hlw', [0, 2, 5])
})
})
describe('removeEmptySnippets', () => {
it('removes an empty snippet group', () => {
expect(suggestionListElement.removeEmptySnippets('$0')).toBe('')
expect(suggestionListElement.removeEmptySnippets('$1000')).toBe('')
})
it('removes an empty snippet group with surrounding text', () => {
expect(suggestionListElement.removeEmptySnippets('hello$0')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('$0hello')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('hello$0hello')).toBe('hellohello')
expect(suggestionListElement.removeEmptySnippets('hello$1000hello')).toBe('hellohello')
})
it('removes an empty snippet group with braces', () => {
expect(suggestionListElement.removeEmptySnippets('${0}')).toBe('')
expect(suggestionListElement.removeEmptySnippets('${1000}')).toBe('')
})
it('removes an empty snippet group with braces with surrounding text', () => {
expect(suggestionListElement.removeEmptySnippets('hello${0}')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('${0}hello')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('hello${0}hello')).toBe('hellohello')
expect(suggestionListElement.removeEmptySnippets('hello${1000}hello')).toBe('hellohello')
})
it('removes an empty snippet group with braces and a colon', () => {
expect(suggestionListElement.removeEmptySnippets('${0:}')).toBe('')
expect(suggestionListElement.removeEmptySnippets('${1000:}')).toBe('')
})
it('removes an empty snippet group with braces and a colon with surrounding text', () => {
expect(suggestionListElement.removeEmptySnippets('hello${0:}')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('${0:}hello')).toBe('hello')
expect(suggestionListElement.removeEmptySnippets('hello${0:}hello')).toBe('hellohello')
expect(suggestionListElement.removeEmptySnippets('hello${1000:}hello')).toBe('hellohello')
})
})
describe('moveSelectionUp', () => {
it('decreases the selected index when the current index is greater than zero', () => {
spyOn(suggestionListElement, 'setSelectedIndex')
suggestionListElement.selectedIndex = 1
suggestionListElement.moveSelectionUp()
expect(suggestionListElement.setSelectedIndex).toHaveBeenCalledWith(0)
})
it('dismisses the suggestion list if the current selection is at the start of the list and moveToCancel is true', () => {
const model = {
activeEditor: {
moveUp () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveUp')
spyOn(model, 'cancel')
suggestionListElement.model = model
suggestionListElement.selectedIndex = 0
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionUp()
expect(model.activeEditor.moveUp).toHaveBeenCalledWith(1)
expect(model.cancel).toHaveBeenCalled()
})
it('cycles to the last element in the suggestion list when the current selection is at the start of the list', () => {
spyOn(suggestionListElement, 'visibleItems').andReturn(['a', 'b', 'c', 'd', 'e'])
spyOn(suggestionListElement, 'setSelectedIndex')
suggestionListElement.moveSelectionUp()
expect(suggestionListElement.setSelectedIndex).toHaveBeenCalledWith(4)
})
})
describe('moveSelectionDown', () => {
it('increases the selected index if the current selection is not at the end of the list', () => {
spyOn(suggestionListElement, 'visibleItems').andReturn(['a', 'b', 'c', 'd', 'e'])
spyOn(suggestionListElement, 'setSelectedIndex')
suggestionListElement.selectedIndex = 3
suggestionListElement.moveSelectionDown()
expect(suggestionListElement.setSelectedIndex).toHaveBeenCalledWith(4)
})
it('dismisses the suggestion list if the current selection is at the end of the list and moveToCancel is true', () => {
const model = {
activeEditor: {
moveDown () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveDown')
spyOn(model, 'cancel')
spyOn(suggestionListElement, 'visibleItems').andReturn(['a', 'b', 'c', 'd', 'e'])
suggestionListElement.model = model
suggestionListElement.selectedIndex = 4
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionDown()
expect(model.activeEditor.moveDown).toHaveBeenCalledWith(1)
expect(model.cancel).toHaveBeenCalled()
})
it('cycles to the first element in the suggestion list when the current suggestion is at the end of the list', () => {
spyOn(suggestionListElement, 'visibleItems').andReturn(['a', 'b', 'c', 'd', 'e'])
spyOn(suggestionListElement, 'setSelectedIndex')
suggestionListElement.selectedIndex = 4
suggestionListElement.moveSelectionDown()
expect(suggestionListElement.setSelectedIndex).toHaveBeenCalledWith(0)
})
})
describe('moveSelectionPageUp', () => {
it('dismisses the list if moveToCancel is true', () => {
const model = {
activeEditor: {
getScreenLineCount: () => 42,
moveUp () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveUp')
spyOn(model, 'cancel')
suggestionListElement.model = model
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionPageUp()
expect(model.activeEditor.moveUp).toHaveBeenCalledWith(42)
expect(model.cancel).toHaveBeenCalled()
})
})
describe('moveSelectionPageDown', () => {
it('dismisses the list if moveToCancel is true', () => {
const model = {
activeEditor: {
getScreenLineCount: () => 42,
moveDown () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveDown')
spyOn(model, 'cancel')
spyOn(suggestionListElement, 'visibleItems').andReturn(['a'])
suggestionListElement.model = model
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionPageDown()
expect(model.activeEditor.moveDown).toHaveBeenCalledWith(42)
expect(model.cancel).toHaveBeenCalled()
})
})
describe('moveSelectionToTop', () => {
it('dismisses the list if moveToCancel is true', () => {
const model = {
activeEditor: {
moveToTop () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveToTop')
spyOn(model, 'cancel')
suggestionListElement.model = model
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionToTop()
expect(model.activeEditor.moveToTop).toHaveBeenCalled()
expect(model.cancel).toHaveBeenCalled()
})
})
describe('moveSelectionToBottom', () => {
it('dismisses the list if moveToCancel is true', () => {
const model = {
activeEditor: {
moveToBottom () {}
},
cancel () {}
}
spyOn(model.activeEditor, 'moveToBottom')
spyOn(model, 'cancel')
spyOn(suggestionListElement, 'visibleItems').andReturn(['a'])
suggestionListElement.model = model
suggestionListElement.moveToCancel = true
suggestionListElement.moveSelectionToBottom()
expect(model.activeEditor.moveToBottom).toHaveBeenCalled()
expect(model.cancel).toHaveBeenCalled()
})
})
})

View File

@ -0,0 +1,232 @@
// TODO: remove this fallback when 193 is out for a while
.attr-syntax-color() { @syntax-color-attribute: #888; } .attr-syntax-color();
@import "ui-variables";
@import "syntax-variables";
@font-size-small: .9em;
@type-icon-font-size: 1em;
@type-letter-icon-font-size: @font-size-small;
@row-line-height: 2em;
@item-padding: .75em;
@item-side-padding: .6em;
atom-overlay.autocomplete-plus {
z-index: 12; // higher than docks
}
autocomplete-suggestion-list.select-list.popover-list {
width: auto !important; // TODO: Can be removed once the inline style is gone
display: inline-block;
min-width: 243px; // Try and fail until the specs pass
padding: 0;
overflow: hidden;
color: @text-color;
.suggestion-list-scroller {
overflow-y: auto;
}
.suggestion-description {
padding: 5px 0;
padding-left: @item-side-padding;
padding-right: @item-side-padding;
min-height: @row-line-height;
line-height: 1.3;
background: darken(@overlay-background-color, 4%);
border-radius: 0 0 @component-border-radius @component-border-radius;
// let the other sibling decide how large the container should be
width: min-content;
min-width: 100%;
}
.suggestion-description-content {
font-size: @font-size + 1px;
font-family: @font-family;
max-height: 33vh;
display: block;
overflow-y: auto;
white-space: pre-wrap;
}
.suggestion-description-more-link {
font-size: @font-size + 1px;
font-family: @font-family;
color: @text-color-info;
}
input {
position: absolute;
opacity: 0.00;
}
ol.list-group {
margin-top: 0;
display: table;
width: 100%;
margin-right: 10px; // Needed to prevent horizontal scrolling when right label is too long
li {
display: table-row;
color: @text-color;
line-height: @row-line-height;
> span {
display: table-cell;
vertical-align: middle;
}
&:after {
clear: both;
display: block;
content: ' ';
}
&.selected {
.word {
color: fadeout(@text-color-selected, 20%);
}
.character-match {
color: @text-color-selected;
}
.left-label, .right-label, .snippet-completion {
color: fadeout(@text-color-selected, 50%);
}
}
}
}
}
autocomplete-suggestion-list {
.icon-container {
width: @row-line-height;
padding-left: 0;
padding-right: @item-padding;
&:empty {
width: @item-side-padding;
padding: 0;
}
}
.icon {
display: block;
width: @row-line-height;
line-height: @row-line-height;
text-align: center;
font-style: normal;
font-weight: bold;
color: @text-color-subtle;
background: fadeout(@text-color, 90%);
&:empty { display: none; }
> *::before {
font-size: @type-icon-font-size;
width: @type-icon-font-size;
height: @type-icon-font-size;
}
.icon-letter {
// I'm failing to vert center these letters. So shift them up...
position: relative;
top: -.1em;
font-size: @type-letter-icon-font-size;
line-height: @type-letter-icon-font-size;
}
}
.left-label {
text-align: right;
padding-right: @item-padding;
vertical-align: middle;
font-size: @font-size-small;
color: @text-color-subtle;
&:empty {
padding-right: 0;
}
}
.right-label {
padding-right: @item-padding;
font-size: @font-size-small;
color: @text-color-subtle;
&:empty {
padding-right: 0;
}
}
.word-container {
padding-right: @item-padding;
}
.word {
float: left;
}
.character-match {
font-weight: bold;
color: @text-color-highlight;
}
.snippet-completion {
color: @text-color-subtle;
}
// Here we set the max width of the popup
.left-label {
max-width: 20vw;
}
.word {
max-width: 35vw;
}
.right-label {
max-width: 25vw;
}
.word, .left-label, .right-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.make-type-icon(attribute, @syntax-color-attribute);
.make-type-icon(builtin, @syntax-color-keyword);
.make-type-icon(class, @syntax-color-class);
.make-type-icon(constant, @syntax-color-constant);
.make-type-icon(function, @syntax-color-function);
.make-type-icon(import, @syntax-color-import);
.make-type-icon(keyword, @syntax-color-keyword);
.make-type-icon(method, @syntax-color-method);
.make-type-icon(module, @syntax-color-import);
.make-type-icon(mixin, @syntax-color-class);
.make-type-icon(package, @syntax-color-import);
.make-type-icon(property, @syntax-color-property);
.make-type-icon(require, @syntax-color-import);
.make-type-icon(snippet, @syntax-color-snippet);
.make-type-icon(tag, @syntax-color-tag);
.make-type-icon(type, @syntax-color-class);
.make-type-icon(value, @syntax-color-value);
.make-type-icon(variable, @syntax-color-variable);
.make-type-icon(selector, @syntax-color-attribute);
.make-type-icon(pseudo-selector, @syntax-color-attribute);
}
.make-type-icon(@type, @color) {
.icon.@{type} {
color: @color;
background: fadeout(@color, 80%)
}
}
.make-type-icon(@type, @color)
when (hsvvalue(@color) - hsvvalue(@overlay-background-color) > -20)
and (hsvvalue(@color) - hsvvalue(@overlay-background-color) < 20) {
.icon.@{type} {
@corrected-color: contrast(@overlay-background-color, darken(@color, 30%), lighten(@color, 30%));
color: @corrected-color;
background: fadeout(@corrected-color, 80%);
}
}

View File

@ -0,0 +1,7 @@
# autocomplete+ snippet suggestions package
Adds snippets to autocomplete+ suggestions
## Features
* Adds user snippets and language snippets to the autocomplete+ suggestions list

View File

@ -0,0 +1,26 @@
module.exports = {
provider: null,
activate() {},
deactivate() {
this.provider = null
},
provide() {
if (this.provider == null) {
const SnippetsProvider = require('./snippets-provider')
this.provider = new SnippetsProvider()
if (this.snippets != null) {
this.provider.setSnippetsSource(this.snippets)
}
}
return this.provider
},
consumeSnippets(snippets) {
this.snippets = snippets
return (this.provider != null ? this.provider.setSnippetsSource(this.snippets) : undefined)
}
}

View File

@ -0,0 +1,61 @@
module.exports =
class SnippetsProvider {
constructor() {
this.selector = '*'
this.inclusionPriority = 1
this.suggestionPriority = 2
this.filterSuggestions = true
this.showIcon = true
this.snippetsSource = {
snippetsForScopes(scopeDescriptor) {
return atom.config.get('snippets', {scope: scopeDescriptor})
}
}
}
setSnippetsSource(snippetsSource) {
if (typeof (snippetsSource != null ? snippetsSource.snippetsForScopes : undefined) === "function") {
return this.snippetsSource = snippetsSource
}
}
getSuggestions({scopeDescriptor, prefix}) {
if (!(prefix != null ? prefix.length : undefined)) { return }
const scopeSnippets = this.snippetsSource.snippetsForScopes(scopeDescriptor)
return this.findSuggestionsForPrefix(scopeSnippets, prefix)
}
findSuggestionsForPrefix(snippets, prefix) {
if (snippets == null) { return [] }
const suggestions = []
for (let snippetPrefix in snippets) {
const snippet = snippets[snippetPrefix]
if (!snippet || !snippetPrefix || !prefix || !firstCharsEqual(snippetPrefix, prefix)) { continue }
suggestions.push({
iconHTML: this.showIcon ? undefined : false,
type: 'snippet',
text: snippet.prefix,
replacementPrefix: prefix,
rightLabel: snippet.name,
rightLabelHTML: snippet.rightLabelHTML,
leftLabel: snippet.leftLabel,
leftLabelHTML: snippet.leftLabelHTML,
description: snippet.description,
descriptionMoreURL: snippet.descriptionMoreURL
})
}
suggestions.sort(ascendingPrefixComparator)
return suggestions
}
onDidInsertSuggestion({editor}) {
return atom.commands.dispatch(atom.views.getView(editor), 'snippets:expand')
}
}
const ascendingPrefixComparator = (a, b) => a.text.localeCompare(b.text)
const firstCharsEqual = (str1, str2) => str1[0].toLowerCase() === str2[0].toLowerCase()

View File

@ -0,0 +1,16 @@
{
"name": "autocomplete-snippets",
"version": "1.12.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "autocomplete-snippets",
"version": "1.12.1",
"license": "MIT",
"engines": {
"atom": ">=0.174.0 <2.0.0"
}
}
}
}

View File

@ -0,0 +1,25 @@
{
"name": "autocomplete-snippets",
"main": "./lib/autocomplete-snippets",
"version": "1.12.1",
"description": "Adds snippets to autocomplete+ suggestions",
"repository": "https://github.com/pulsar-edit/pulsar",
"license": "MIT",
"engines": {
"atom": ">=0.174.0 <2.0.0"
},
"providedServices": {
"autocomplete.provider": {
"versions": {
"2.0.0": "provide"
}
}
},
"consumedServices": {
"snippets": {
"versions": {
"0.1.0": "consumeSnippets"
}
}
}
}

View File

@ -0,0 +1,106 @@
describe('AutocompleteSnippets', () => {
let [completionDelay, editor, editorView] = []
beforeEach(() => {
atom.config.set('autocomplete-plus.enableAutoActivation', true)
completionDelay = 100
atom.config.set('autocomplete-plus.autoActivationDelay', completionDelay)
completionDelay += 100 // Rendering delay
const workspaceElement = atom.views.getView(atom.workspace)
jasmine.attachToDOM(workspaceElement)
let autocompleteSnippetsMainModule = null
let snippetsMainModule = null
const autocompleteManager = null
waitsForPromise(() =>
Promise.all([
atom.workspace.open('sample.js').then((e) => {
editor = e
editorView = atom.views.getView(editor)
}),
atom.packages.activatePackage('language-javascript'),
atom.packages.activatePackage('autocomplete-snippets').then(({mainModule}) => autocompleteSnippetsMainModule = mainModule),
atom.packages.activatePackage('autocomplete-plus'),
atom.packages.activatePackage('snippets').then(({mainModule}) => {
snippetsMainModule = mainModule
snippetsMainModule.loaded = false
})
])
)
waitsFor('snippets provider to be registered', 1000, () => autocompleteSnippetsMainModule.provider != null)
waitsFor('all snippets to load', 3000, () => snippetsMainModule.loaded)
})
describe('when autocomplete-plus is enabled', () => {
it('shows autocompletions when there are snippets available', () => {
runs(() => {
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
editor.moveToBottom()
editor.insertText('D')
editor.insertText('o')
advanceClock(completionDelay)
})
waitsFor('autocomplete view to appear', 1000, () => editorView.querySelector('.autocomplete-plus span.word'))
runs(() => {
expect(editorView.querySelector('.autocomplete-plus span.word')).toHaveText('do')
expect(editorView.querySelector('.autocomplete-plus span.right-label')).toHaveText('do')
})
})
it("expands the snippet on confirm", () => {
runs(() => {
expect(editorView.querySelector('.autocomplete-plus')).not.toExist()
editor.moveToBottom()
editor.insertText('D')
editor.insertText('o')
advanceClock(completionDelay)
})
waitsFor('autocomplete view to appear', 1000, () => editorView.querySelector('.autocomplete-plus span.word'))
runs(() => {
atom.commands.dispatch(editorView, 'autocomplete-plus:confirm')
expect(editor.getText()).toContain('} while (true)')
})
})
})
describe('when showing suggestions', () =>
it('sorts them in alphabetical order', () => {
const unorderedPrefixes = [
"",
"dop",
"do",
"dad",
"d"
]
const snippets = {}
for (let x of Array.from(unorderedPrefixes)) {
snippets[x] = {prefix: x, name: "", description: "", descriptionMoreURL: ""}
}
const SnippetsProvider = require('../lib/snippets-provider')
const sp = new SnippetsProvider()
sp.setSnippetsSource({snippetsForScopes(scope) {
return snippets
}})
const suggestions = sp.getSuggestions({scopeDescriptor: "", prefix: "d"})
const suggestionsText = suggestions.map(x => x.text)
expect(suggestionsText).toEqual(["d", "dad", "do", "dop"])
})
)
})

View File

@ -0,0 +1 @@
console.log("ohai");

Some files were not shown because too many files have changed in this diff Show More