mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-11 19:17:09 +03:00
Merge branch 'clojure-highlight-with-new-improvements' into feature/electron-20
This commit is contained in:
commit
cf2a81a971
@ -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
38
.github/workflows/documentation.yml
vendored
Normal 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
|
9
.github/workflows/editor-tests.yml
vendored
9
.github/workflows/editor-tests.yml
vendored
@ -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
|
||||
|
170
.github/workflows/package-tests-linux.yml
vendored
170
.github/workflows/package-tests-linux.yml
vendored
@ -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 }}/)"
|
||||
|
307
CHANGELOG.md
307
CHANGELOG.md
@ -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
|
||||
|
@ -1,5 +1,5 @@
|
||||
# VERSION: 0.2
|
||||
# DESCRIPTION: Image to build Atom
|
||||
# DESCRIPTION: Image to build Pulsar
|
||||
|
||||
FROM ubuntu:20.04
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 }--------------------------->
|
||||
|
0
docs/Pulsar-API-Documentation.md
Normal file
0
docs/Pulsar-API-Documentation.md
Normal file
0
docs/Source-Code-Documentation.md
Normal file
0
docs/Source-Code-Documentation.md
Normal 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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
25
hooks/README.md
Normal 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
|
59
hooks/manage_hooks-completion.bash
Normal file
59
hooks/manage_hooks-completion.bash
Normal 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
83
hooks/manage_hooks.sh
Executable 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
24
hooks/post-checkout
Executable 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
15
hooks/post-merge
Executable 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
56
hooks/update_editor.sh
Executable 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
|
92
package.json
92
package.json
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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';
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -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')) {
|
||||
|
1819
packages/about/package-lock.json
generated
1819
packages/about/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -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": {
|
||||
|
@ -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);
|
||||
|
@ -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');
|
||||
|
@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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();
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
||||
});
|
||||
|
||||
|
20
packages/archive-view/README.md
Normal file
20
packages/archive-view/README.md
Normal 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)
|
3
packages/archive-view/keymaps/archive-view.cson
Normal file
3
packages/archive-view/keymaps/archive-view.cson
Normal file
@ -0,0 +1,3 @@
|
||||
'.archive-editor':
|
||||
'k': 'core:move-up'
|
||||
'j': 'core:move-down'
|
208
packages/archive-view/lib/archive-editor-view.js
Normal file
208
packages/archive-view/lib/archive-editor-view.js
Normal 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()
|
||||
}
|
||||
}
|
74
packages/archive-view/lib/archive-editor.js
Normal file
74
packages/archive-view/lib/archive-editor.js
Normal 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
|
||||
}
|
||||
}
|
36
packages/archive-view/lib/default-file-icons.js
Normal file
36
packages/archive-view/lib/default-file-icons.js
Normal 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()
|
132
packages/archive-view/lib/directory-view.js
Normal file
132
packages/archive-view/lib/directory-view.js
Normal 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
|
||||
}
|
||||
}
|
112
packages/archive-view/lib/file-view.js
Normal file
112
packages/archive-view/lib/file-view.js
Normal 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')
|
||||
}
|
||||
}
|
72
packages/archive-view/lib/get-icon-services.js
Normal file
72
packages/archive-view/lib/get-icon-services.js
Normal 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
513
packages/archive-view/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
33
packages/archive-view/package.json
Normal file
33
packages/archive-view/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
BIN
packages/archive-view/resources/preview.png
Normal file
BIN
packages/archive-view/resources/preview.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 134 KiB |
46
packages/archive-view/spec/archive-editor-spec.js
Normal file
46
packages/archive-view/spec/archive-editor-spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
276
packages/archive-view/spec/archive-editor-view-spec.js
Normal file
276
packages/archive-view/spec/archive-editor-view-spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
103
packages/archive-view/spec/async-spec-helpers.js
Normal file
103
packages/archive-view/spec/async-spec-helpers.js
Normal 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
|
||||
}
|
BIN
packages/archive-view/spec/fixtures/file-icons.zip
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/file-icons.zip
vendored
Normal file
Binary file not shown.
1
packages/archive-view/spec/fixtures/invalid.zip
vendored
Normal file
1
packages/archive-view/spec/fixtures/invalid.zip
vendored
Normal file
@ -0,0 +1 @@
|
||||
invalid
|
BIN
packages/archive-view/spec/fixtures/multiple-entries.zip
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/multiple-entries.zip
vendored
Normal file
Binary file not shown.
BIN
packages/archive-view/spec/fixtures/nested.tar
vendored
Normal file
BIN
packages/archive-view/spec/fixtures/nested.tar
vendored
Normal file
Binary file not shown.
24
packages/archive-view/styles/archive-view.less
Normal file
24
packages/archive-view/styles/archive-view.less
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
packages/atom-dark-syntax/package-lock.json
generated
Normal file
16
packages/atom-dark-syntax/package-lock.json
generated
Normal 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
16
packages/atom-dark-ui/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
16
packages/atom-light-syntax/package-lock.json
generated
Normal file
16
packages/atom-light-syntax/package-lock.json
generated
Normal 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
16
packages/atom-light-ui/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
1
packages/autocomplete-plus/.coffeelintignore
Normal file
1
packages/autocomplete-plus/.coffeelintignore
Normal file
@ -0,0 +1 @@
|
||||
spec/fixtures
|
3
packages/autocomplete-plus/.gitignore
vendored
Normal file
3
packages/autocomplete-plus/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.DS_Store
|
||||
npm-debug.log
|
16
packages/autocomplete-plus/.pairs
Normal file
16
packages/autocomplete-plus/.pairs
Normal 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
|
109
packages/autocomplete-plus/README.md
Normal file
109
packages/autocomplete-plus/README.md
Normal 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']
|
||||
)
|
||||
}
|
||||
```
|
37
packages/autocomplete-plus/coffeelint.json
Normal file
37
packages/autocomplete-plus/coffeelint.json
Normal 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"
|
||||
}
|
||||
}
|
@ -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'
|
799
packages/autocomplete-plus/lib/autocomplete-manager.js
Normal file
799
packages/autocomplete-plus/lib/autocomplete-manager.js
Normal 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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
88
packages/autocomplete-plus/lib/main.js
Normal file
88
packages/autocomplete-plus/lib/main.js
Normal 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
|
||||
}
|
||||
}
|
4
packages/autocomplete-plus/lib/private-symbols.js
Normal file
4
packages/autocomplete-plus/lib/private-symbols.js
Normal file
@ -0,0 +1,4 @@
|
||||
'use babel'
|
||||
|
||||
const API_VERSION = Symbol('Private property: Semantic version of the service endpoint.')
|
||||
export {API_VERSION}
|
164
packages/autocomplete-plus/lib/provider-config.js
Normal file
164
packages/autocomplete-plus/lib/provider-config.js
Normal 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
|
||||
}
|
||||
}
|
303
packages/autocomplete-plus/lib/provider-manager.js
Normal file
303
packages/autocomplete-plus/lib/provider-manager.js
Normal 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}`)
|
||||
}
|
||||
}
|
69
packages/autocomplete-plus/lib/provider-metadata.js
Normal file
69
packages/autocomplete-plus/lib/provider-metadata.js
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
74
packages/autocomplete-plus/lib/scope-helpers.js
Normal file
74
packages/autocomplete-plus/lib/scope-helpers.js
Normal 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 }
|
99
packages/autocomplete-plus/lib/snippet-parser.js
Normal file
99
packages/autocomplete-plus/lib/snippet-parser.js
Normal 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)
|
265
packages/autocomplete-plus/lib/subsequence-provider.js
Normal file
265
packages/autocomplete-plus/lib/subsequence-provider.js
Normal 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
|
||||
}
|
697
packages/autocomplete-plus/lib/suggestion-list-element.js
Normal file
697
packages/autocomplete-plus/lib/suggestion-list-element.js
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
382
packages/autocomplete-plus/lib/suggestion-list.js
Normal file
382
packages/autocomplete-plus/lib/suggestion-list.js
Normal 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()
|
||||
}
|
||||
}
|
13
packages/autocomplete-plus/lib/type-helpers.js
Normal file
13
packages/autocomplete-plus/lib/type-helpers.js
Normal 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 }
|
4
packages/autocomplete-plus/lib/unicode-helpers.js
Normal file
4
packages/autocomplete-plus/lib/unicode-helpers.js
Normal 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 }
|
3054
packages/autocomplete-plus/package-lock.json
generated
Normal file
3054
packages/autocomplete-plus/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
258
packages/autocomplete-plus/package.json
Normal file
258
packages/autocomplete-plus/package.json
Normal 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/**"
|
||||
]
|
||||
}
|
||||
}
|
@ -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()
|
||||
})
|
||||
})
|
||||
})
|
@ -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
@ -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')
|
||||
})
|
||||
})
|
||||
})
|
4
packages/autocomplete-plus/spec/fixtures/blacklisted.md
vendored
Normal file
4
packages/autocomplete-plus/spec/fixtures/blacklisted.md
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
This is a simple markdown file. Autocompletion should not work here.
|
||||
====================================================================
|
||||
|
||||
![](http://i.lvme.me/xmeh35.jpg)
|
5
packages/autocomplete-plus/spec/fixtures/css.css
vendored
Normal file
5
packages/autocomplete-plus/spec/fixtures/css.css
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
body {
|
||||
font-size: 1234px;
|
||||
width: 110%;
|
||||
font-weight: bold !important;
|
||||
}
|
7
packages/autocomplete-plus/spec/fixtures/sample.coffee
vendored
Normal file
7
packages/autocomplete-plus/spec/fixtures/sample.coffee
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
SomeModule = require 'some-module'
|
||||
|
||||
class MyClass extends SomeModule
|
||||
constructor: ->
|
||||
|
||||
quicksort: ->
|
||||
# do quicksort here
|
13
packages/autocomplete-plus/spec/fixtures/sample.js
vendored
Normal file
13
packages/autocomplete-plus/spec/fixtures/sample.js
vendored
Normal 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));
|
||||
};
|
1
packages/autocomplete-plus/spec/fixtures/sample.txt
vendored
Normal file
1
packages/autocomplete-plus/spec/fixtures/sample.txt
vendored
Normal file
@ -0,0 +1 @@
|
||||
Some text.
|
15
packages/autocomplete-plus/spec/fixtures/samplelong.js
vendored
Normal file
15
packages/autocomplete-plus/spec/fixtures/samplelong.js
vendored
Normal 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));
|
||||
};
|
60
packages/autocomplete-plus/spec/main-spec.js
Normal file
60
packages/autocomplete-plus/spec/main-spec.js
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
340
packages/autocomplete-plus/spec/provider-api-legacy-spec.js
Normal file
340
packages/autocomplete-plus/spec/provider-api-legacy-spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
293
packages/autocomplete-plus/spec/provider-api-spec.js
Normal file
293
packages/autocomplete-plus/spec/provider-api-spec.js
Normal 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'}
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
555
packages/autocomplete-plus/spec/provider-manager-spec.js
Normal file
555
packages/autocomplete-plus/spec/provider-manager-spec.js
Normal 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
|
||||
}
|
95
packages/autocomplete-plus/spec/spec-helper.js
Normal file
95
packages/autocomplete-plus/spec/spec-helper.js
Normal 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
|
||||
}
|
440
packages/autocomplete-plus/spec/subsequence-provider-spec.js
Normal file
440
packages/autocomplete-plus/spec/subsequence-provider-spec.js
Normal 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)
|
||||
})
|
||||
})
|
438
packages/autocomplete-plus/spec/suggestion-list-element-spec.js
Normal file
438
packages/autocomplete-plus/spec/suggestion-list-element-spec.js
Normal 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<Cat>')
|
||||
|
||||
suggestion = {text: 'Animal<Cat>', displayText: 'Animal<Cat>'}
|
||||
suggestionListElement.renderItem(suggestion)
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal<Cat>')
|
||||
|
||||
suggestion = {snippet: 'Animal<Cat>', displayText: 'Animal<Cat>'}
|
||||
suggestionListElement.renderItem(suggestion)
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal<Cat>')
|
||||
})
|
||||
|
||||
it('HTML escapes snippets', () => {
|
||||
let suggestion = {snippet: 'Animal<Cat>(${1:omg<wow>}, ${2:ok<yeah>})'}
|
||||
suggestionListElement.renderItem(suggestion)
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('Animal<Cat>')
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('omg<wow>')
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('ok<yeah>')
|
||||
|
||||
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<Cat>')
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('omg<wow>')
|
||||
expect(suggestionListElement.selectedLi.innerHTML).toContain('ok<yeah>')
|
||||
})
|
||||
|
||||
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<Cat>')
|
||||
return expect(suggestionListElement.selectedLi.querySelector('.right-label').innerHTML).toContain('Animal<Dog>')
|
||||
})
|
||||
})
|
||||
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
232
packages/autocomplete-plus/styles/autocomplete.less
Normal file
232
packages/autocomplete-plus/styles/autocomplete.less
Normal 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%);
|
||||
}
|
||||
}
|
7
packages/autocomplete-snippets/README.md
Normal file
7
packages/autocomplete-snippets/README.md
Normal 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
|
26
packages/autocomplete-snippets/lib/autocomplete-snippets.js
Normal file
26
packages/autocomplete-snippets/lib/autocomplete-snippets.js
Normal 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)
|
||||
}
|
||||
}
|
61
packages/autocomplete-snippets/lib/snippets-provider.js
Normal file
61
packages/autocomplete-snippets/lib/snippets-provider.js
Normal 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()
|
16
packages/autocomplete-snippets/package-lock.json
generated
Normal file
16
packages/autocomplete-snippets/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
packages/autocomplete-snippets/package.json
Normal file
25
packages/autocomplete-snippets/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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"])
|
||||
})
|
||||
)
|
||||
})
|
1
packages/autocomplete-snippets/spec/fixtures/sample.js
vendored
Normal file
1
packages/autocomplete-snippets/spec/fixtures/sample.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
console.log("ohai");
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user