From 57b196ceb91a9a07e5723f469ef16d3db86115e0 Mon Sep 17 00:00:00 2001 From: confused-Techie Date: Sun, 12 Mar 2023 15:06:42 -0700 Subject: [PATCH] Bundle `keybinding-resolver` --- packages/keybinding-resolver/.gitignore | 15 + packages/keybinding-resolver/README.md | 10 + .../keymaps/keybinding-resolver.cson | 8 + .../lib/keybinding-resolver-view.js | 266 ++++++++++++++++++ packages/keybinding-resolver/lib/main.js | 33 +++ .../menus/keybinding-resolver.cson | 11 + .../keybinding-resolver/package-lock.json | 232 +++++++++++++++ packages/keybinding-resolver/package.json | 19 ++ .../spec/async-spec-helpers.js | 40 +++ .../spec/keybinding-resolver-view-spec.js | 180 ++++++++++++ .../styles/keybinding-resolver.less | 64 +++++ 11 files changed, 878 insertions(+) create mode 100644 packages/keybinding-resolver/.gitignore create mode 100644 packages/keybinding-resolver/README.md create mode 100644 packages/keybinding-resolver/keymaps/keybinding-resolver.cson create mode 100644 packages/keybinding-resolver/lib/keybinding-resolver-view.js create mode 100644 packages/keybinding-resolver/lib/main.js create mode 100644 packages/keybinding-resolver/menus/keybinding-resolver.cson create mode 100644 packages/keybinding-resolver/package-lock.json create mode 100644 packages/keybinding-resolver/package.json create mode 100644 packages/keybinding-resolver/spec/async-spec-helpers.js create mode 100644 packages/keybinding-resolver/spec/keybinding-resolver-view-spec.js create mode 100644 packages/keybinding-resolver/styles/keybinding-resolver.less diff --git a/packages/keybinding-resolver/.gitignore b/packages/keybinding-resolver/.gitignore new file mode 100644 index 000000000..a72b52ebe --- /dev/null +++ b/packages/keybinding-resolver/.gitignore @@ -0,0 +1,15 @@ +lib-cov +*.seed +*.log +*.csv +*.dat +*.out +*.pid +*.gz + +pids +logs +results + +npm-debug.log +node_modules diff --git a/packages/keybinding-resolver/README.md b/packages/keybinding-resolver/README.md new file mode 100644 index 000000000..275458cf1 --- /dev/null +++ b/packages/keybinding-resolver/README.md @@ -0,0 +1,10 @@ +# Keybinding Resolver package + +Shows what commands a keybinding resolves to. + +You can open and close the resolver using Cmd+. (macOS) or Ctrl+. (Linux and Windows). + +Please note the clipboard icon which can be selected to copy the given keybinding +directive so that you can easily paste it into your keymap files. + +![](https://user-images.githubusercontent.com/4137660/44482876-8de73a80-a617-11e8-8bd5-24023c96b39e.png) diff --git a/packages/keybinding-resolver/keymaps/keybinding-resolver.cson b/packages/keybinding-resolver/keymaps/keybinding-resolver.cson new file mode 100644 index 000000000..e84e7496a --- /dev/null +++ b/packages/keybinding-resolver/keymaps/keybinding-resolver.cson @@ -0,0 +1,8 @@ +'.platform-darwin': + 'cmd-.': 'key-binding-resolver:toggle' + +'.platform-win32': + 'ctrl-.': 'key-binding-resolver:toggle' + +'.platform-linux': + 'ctrl-.': 'key-binding-resolver:toggle' diff --git a/packages/keybinding-resolver/lib/keybinding-resolver-view.js b/packages/keybinding-resolver/lib/keybinding-resolver-view.js new file mode 100644 index 000000000..7c5dc16da --- /dev/null +++ b/packages/keybinding-resolver/lib/keybinding-resolver-view.js @@ -0,0 +1,266 @@ +/** @babel */ +/** @jsx etch.dom */ + +import fs from 'fs-plus' +import etch from 'etch' +import {CompositeDisposable} from 'atom' +import path from 'path' + +export default class KeyBindingResolverView { + constructor () { + this.keystrokes = null + this.usedKeyBinding = null + this.unusedKeyBindings = [] + this.unmatchedKeyBindings = [] + this.partiallyMatchedBindings = [] + this.attached = false + this.disposables = new CompositeDisposable() + this.keybindingDisposables = new CompositeDisposable() + + this.disposables.add(atom.workspace.getBottomDock().observeActivePaneItem(item => { + if (item === this) { + this.attach() + } else { + this.detach() + } + })) + + this.disposables.add(atom.workspace.getBottomDock().observeVisible(visible => { + if (visible) { + if (atom.workspace.getBottomDock().getActivePaneItem() === this) this.attach() + } else { + this.detach() + } + })) + + etch.initialize(this) + } + + getTitle () { + return 'Key Binding Resolver' + } + + getIconName () { + return 'keyboard' + } + + getDefaultLocation () { + return 'bottom' + } + + getAllowedLocations () { + // TODO: Support left and right possibly + return ['bottom'] + } + + getURI () { + return 'atom://keybinding-resolver' + } + + serialize () { + return { + deserializer: 'keybinding-resolver/KeyBindingResolverView' + } + } + + destroy () { + this.disposables.dispose() + this.detach() + return etch.destroy(this) + } + + attach () { + if (this.attached) return + + this.attached = true + this.keybindingDisposables = new CompositeDisposable() + this.keybindingDisposables.add(atom.keymaps.onDidMatchBinding(({keystrokes, binding, keyboardEventTarget, eventType}) => { + if (eventType === 'keyup' && binding == null) { + return + } + + const unusedKeyBindings = atom.keymaps + .findKeyBindings({keystrokes, target: keyboardEventTarget}) + .filter((b) => b !== binding) + + const unmatchedKeyBindings = atom.keymaps + .findKeyBindings({keystrokes}) + .filter((b) => b !== binding && !unusedKeyBindings.includes(b)) + + this.update({usedKeyBinding: binding, unusedKeyBindings, unmatchedKeyBindings, keystrokes}) + })) + + this.keybindingDisposables.add(atom.keymaps.onDidPartiallyMatchBindings(({keystrokes, partiallyMatchedBindings}) => { + this.update({keystrokes, partiallyMatchedBindings}) + })) + + this.keybindingDisposables.add(atom.keymaps.onDidFailToMatchBinding(({keystrokes, keyboardEventTarget, eventType}) => { + if (eventType === 'keyup') { + return + } + + const unusedKeyBindings = atom.keymaps.findKeyBindings({keystrokes, target: keyboardEventTarget}) + const unmatchedKeyBindings = atom.keymaps + .findKeyBindings({keystrokes}) + .filter((b) => !unusedKeyBindings.includes(b)) + + this.update({unusedKeyBindings, unmatchedKeyBindings, keystrokes}) + })) + } + + detach () { + if (!this.attached) return + + this.attached = false + this.keybindingDisposables.dispose() + this.keybindingDisposables = null + } + + update (props) { + this.keystrokes = props.keystrokes + this.usedKeyBinding = props.usedKeyBinding + this.unusedKeyBindings = props.unusedKeyBindings || [] + this.unmatchedKeyBindings = props.unmatchedKeyBindings || [] + this.partiallyMatchedBindings = props.partiallyMatchedBindings || [] + return etch.update(this) + } + + render () { + return ( +
+
{this.renderKeystrokes()}
+
{this.renderKeyBindings()}
+
+ ) + } + + renderKeystrokes () { + if (this.keystrokes) { + if (this.partiallyMatchedBindings.length > 0) { + return {this.keystrokes} (partial) + } else { + return {this.keystrokes} + } + } else { + return Press any key + } + } + + renderKeyBindings () { + if (this.partiallyMatchedBindings.length > 0) { + return ( + + + {this.partiallyMatchedBindings.map((binding) => ( + + + + + + + + ))} + +
this.copyKeybinding(binding)}>{binding.command}{binding.keystrokes}{binding.selector} this.openKeybindingFile(binding.source)}>{binding.source}
+ ) + } else { + let usedKeyBinding = '' + if (this.usedKeyBinding) { + usedKeyBinding = ( + + this.copyKeybinding(this.usedKeyBinding)}> + {this.usedKeyBinding.command} + {this.usedKeyBinding.selector} + this.openKeybindingFile(this.usedKeyBinding.source)}>{this.usedKeyBinding.source} + + ) + } + return ( + + + {usedKeyBinding} + {this.unusedKeyBindings.map((binding) => ( + + + + + + + ))} + {this.unmatchedKeyBindings.map((binding) => ( + + + + + + + ))} + +
this.copyKeybinding(binding)}>{binding.command}{binding.selector} this.openKeybindingFile(binding.source)}>{binding.source}
this.copyKeybinding(binding)}>{binding.command}{binding.selector} this.openKeybindingFile(binding.source)}>{binding.source}
+ ) + } + } + + isInAsarArchive (pathToCheck) { + const {resourcePath} = atom.getLoadSettings() + return pathToCheck.startsWith(`${resourcePath}${path.sep}`) && path.extname(resourcePath) === '.asar' + } + + extractBundledKeymap (bundledKeymapPath) { + const metadata = require(path.join(atom.getLoadSettings().resourcePath, 'package.json')) + const bundledKeymaps = metadata ? metadata._atomKeymaps : {} + const keymapName = path.basename(bundledKeymapPath) + const extractedKeymapPath = path.join(require('temp').mkdirSync('atom-bundled-keymap-'), keymapName) + fs.writeFileSync( + extractedKeymapPath, + JSON.stringify(bundledKeymaps[keymapName] || {}, null, 2) + ) + return extractedKeymapPath + } + + extractBundledPackageKeymap (keymapRelativePath) { + const packageName = keymapRelativePath.split(path.sep)[1] + const keymapName = path.basename(keymapRelativePath) + const metadata = atom.packages.packagesCache[packageName] || {} + const keymaps = metadata.keymaps || {} + const extractedKeymapPath = path.join(require('temp').mkdirSync('atom-bundled-keymap-'), keymapName) + fs.writeFileSync( + extractedKeymapPath, + JSON.stringify(keymaps[keymapRelativePath] || {}, null, 2) + ) + return extractedKeymapPath + } + + openKeybindingFile (keymapPath) { + if (this.isInAsarArchive(keymapPath)) { + keymapPath = this.extractBundledKeymap(keymapPath) + } else if (keymapPath.startsWith('core:node_modules')) { + keymapPath = this.extractBundledPackageKeymap(keymapPath.replace('core:', '')) + } else if (keymapPath.startsWith('core:')) { + keymapPath = this.extractBundledKeymap(keymapPath.replace('core:', '')) + } + + atom.workspace.open(keymapPath) + } + + copyKeybinding (binding) { + let content + const keymapExtension = path.extname(atom.keymaps.getUserKeymapPath()) + let escapedKeystrokes = binding.keystrokes.replace(/\\/g, '\\\\') // Escape backslashes + if (keymapExtension === '.cson') { + content = `\ +'${binding.selector}': + '${escapedKeystrokes}': '${binding.command}' +` + } else { + content = `\ +"${binding.selector}": { + "${escapedKeystrokes}": "${binding.command}" +} +` + } + + atom.notifications.addInfo('Keybinding Copied') + return atom.clipboard.write(content) + } +} diff --git a/packages/keybinding-resolver/lib/main.js b/packages/keybinding-resolver/lib/main.js new file mode 100644 index 000000000..954c6762c --- /dev/null +++ b/packages/keybinding-resolver/lib/main.js @@ -0,0 +1,33 @@ +const {CompositeDisposable} = require('atom') + +const KeyBindingResolverView = require('./keybinding-resolver-view') + +const KEYBINDING_RESOLVER_URI = 'atom://keybinding-resolver' + +module.exports = { + activate () { + this.subscriptions = new CompositeDisposable() + + this.subscriptions.add(atom.workspace.addOpener(uri => { + if (uri === KEYBINDING_RESOLVER_URI) { + return new KeyBindingResolverView() + } + })) + + this.subscriptions.add(atom.commands.add('atom-workspace', { + 'key-binding-resolver:toggle': () => this.toggle() + })) + }, + + deactivate () { + this.subscriptions.dispose() + }, + + toggle () { + atom.workspace.toggle(KEYBINDING_RESOLVER_URI) + }, + + deserializeKeyBindingResolverView (serialized) { + return new KeyBindingResolverView() + } +} diff --git a/packages/keybinding-resolver/menus/keybinding-resolver.cson b/packages/keybinding-resolver/menus/keybinding-resolver.cson new file mode 100644 index 000000000..e7ed9df45 --- /dev/null +++ b/packages/keybinding-resolver/menus/keybinding-resolver.cson @@ -0,0 +1,11 @@ +'menu': [ + { + 'label': 'Packages' + 'submenu': [ + 'label': 'Keybinding Resolver' + 'submenu': [ + { 'label': 'Toggle', 'command': 'key-binding-resolver:toggle' } + ] + ] + } +] diff --git a/packages/keybinding-resolver/package-lock.json b/packages/keybinding-resolver/package-lock.json new file mode 100644 index 000000000..6ac9111c5 --- /dev/null +++ b/packages/keybinding-resolver/package-lock.json @@ -0,0 +1,232 @@ +{ + "name": "keybinding-resolver", + "version": "0.39.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "keybinding-resolver", + "version": "0.39.1", + "license": "MIT", + "dependencies": { + "etch": "0.9.0", + "fs-plus": "^3.0.0", + "temp": "^0.9.0" + }, + "engines": { + "atom": ">=1.17.0" + } + }, + "node_modules/async": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz", + "integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo=" + }, + "node_modules/balanced-match": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "integrity": "sha1-yz8+PHMtwPAe5wtAPzAuYddwmDg=" + }, + "node_modules/brace-expansion": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "integrity": "sha1-cZfX6qm4fmSDkOph/GbIRCdCDfk=", + "dependencies": { + "balanced-match": "^0.4.1", + "concat-map": "0.0.1" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "node_modules/etch": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/etch/-/etch-0.9.0.tgz", + "integrity": "sha1-CSJpiPLO4GkL3yCMyyXkFNXfrV8=" + }, + "node_modules/fs-plus": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fs-plus/-/fs-plus-3.0.2.tgz", + "integrity": "sha1-a19Sp3EolMTd6f2PgfqMYN8EHz0=", + "dependencies": { + "async": "^1.5.2", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2", + "underscore-plus": "1.x" + } + }, + "node_modules/fs-plus/node_modules/rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dependencies": { + "glob": "^7.0.5" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/glob": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.1.tgz", + "integrity": "sha1-gFIR3wT6rxxjo2ADBs31reULLsg=", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.2", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" + }, + "node_modules/minimatch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "integrity": "sha1-Kk5AkLlrLbBqnX3wEFWmKnfJt3Q=", + "dependencies": { + "brace-expansion": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mkdirp": { + "version": "0.5.1", + "resolved": "http://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dependencies": { + "minimist": "0.0.8" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp/node_modules/minimist": { + "version": "0.0.8", + "resolved": "http://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=" + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dependencies": { + "wrappy": "1" + } + }, + "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": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/temp": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.0.tgz", + "integrity": "sha512-YfUhPQCJoNQE5N+FJQcdPz63O3x3sdT4Xju69Gj4iZe0lBKOtnAMi0SLj9xKhGkcGhsxThvTJ/usxtFPo438zQ==", + "engines": [ + "node >=4.0.0" + ], + "dependencies": { + "rimraf": "~2.6.2" + } + }, + "node_modules/temp/node_modules/balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" + }, + "node_modules/temp/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/temp/node_modules/glob": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.4.tgz", + "integrity": "sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + } + }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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/underscore": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.8.3.tgz", + "integrity": "sha1-Tz+1OxBuYJf8+ctBCfKl6b36UCI=" + }, + "node_modules/underscore-plus": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/underscore-plus/-/underscore-plus-1.6.8.tgz", + "integrity": "sha512-88PrCeMKeAAC1L4xjSiiZ3Fg6kZOYrLpLGVPPeqKq/662DfQe/KTSKdSR/Q/tucKNnfW2MNAUGSCkDf8HmXC5Q==", + "dependencies": { + "underscore": "~1.8.3" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + } + } +} diff --git a/packages/keybinding-resolver/package.json b/packages/keybinding-resolver/package.json new file mode 100644 index 000000000..1a2a28670 --- /dev/null +++ b/packages/keybinding-resolver/package.json @@ -0,0 +1,19 @@ +{ + "name": "keybinding-resolver", + "main": "./lib/main", + "version": "0.39.1", + "description": "Show what commands a keybinding resolves to", + "license": "MIT", + "repository": "https://github.com/pulsar-edit/keybinding-resolver", + "engines": { + "atom": ">=1.17.0" + }, + "deserializers": { + "keybinding-resolver/KeyBindingResolverView": "deserializeKeyBindingResolverView" + }, + "dependencies": { + "etch": "0.9.0", + "fs-plus": "^3.0.0", + "temp": "^0.9.0" + } +} diff --git a/packages/keybinding-resolver/spec/async-spec-helpers.js b/packages/keybinding-resolver/spec/async-spec-helpers.js new file mode 100644 index 000000000..c2548adc7 --- /dev/null +++ b/packages/keybinding-resolver/spec/async-spec-helpers.js @@ -0,0 +1,40 @@ +/** @babel */ + +export function beforeEach (fn) { + global.beforeEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +export function afterEach (fn) { + global.afterEach(function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) +} + +['it', 'fit', 'ffit', 'fffit'].forEach(function (name) { + module.exports[name] = function (description, fn) { + global[name](description, function () { + const result = fn() + if (result instanceof Promise) { + waitsForPromise(() => result) + } + }) + } +}) + +function waitsForPromise (fn) { + const promise = fn() + global.waitsFor('spec promise to resolve', function (done) { + promise.then(done, function (error) { + jasmine.getEnv().currentSpec.fail(error) + done() + }) + }) +} diff --git a/packages/keybinding-resolver/spec/keybinding-resolver-view-spec.js b/packages/keybinding-resolver/spec/keybinding-resolver-view-spec.js new file mode 100644 index 000000000..fc7bd94d2 --- /dev/null +++ b/packages/keybinding-resolver/spec/keybinding-resolver-view-spec.js @@ -0,0 +1,180 @@ +const {it, fit, ffit, beforeEach} = require('./async-spec-helpers') // eslint-disable-line no-unused-vars +const etch = require('etch') + +describe('KeyBindingResolverView', () => { + let workspaceElement, bottomDockElement + + beforeEach(async () => { + workspaceElement = atom.views.getView(atom.workspace) + bottomDockElement = atom.views.getView(atom.workspace.getBottomDock()) + await atom.packages.activatePackage('keybinding-resolver') + jasmine.attachToDOM(workspaceElement); + }) + + describe('when the key-binding-resolver:toggle event is triggered', () => { + it('toggles the view', async () => { + expect(atom.workspace.getBottomDock().isVisible()).toBe(false) + expect(bottomDockElement.querySelector('.key-binding-resolver')).not.toExist() + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + expect(atom.workspace.getBottomDock().isVisible()).toBe(true) + expect(bottomDockElement.querySelector('.key-binding-resolver')).toExist() + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + expect(atom.workspace.getBottomDock().isVisible()).toBe(false) + expect(bottomDockElement.querySelector('.key-binding-resolver')).toExist() + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + expect(atom.workspace.getBottomDock().isVisible()).toBe(true) + expect(bottomDockElement.querySelector('.key-binding-resolver')).toExist() + }) + + it('focuses the view if it is not visible instead of destroying it', async () => { + expect(atom.workspace.getBottomDock().isVisible()).toBe(false) + expect(bottomDockElement.querySelector('.key-binding-resolver')).not.toExist() + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + expect(atom.workspace.getBottomDock().isVisible()).toBe(true) + expect(bottomDockElement.querySelector('.key-binding-resolver')).toExist() + + atom.workspace.getBottomDock().hide() + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + + expect(atom.workspace.getBottomDock().isVisible()).toBe(true) + expect(bottomDockElement.querySelector('.key-binding-resolver')).toExist() + }) + }) + + describe('capturing keybinding events', () => { + it('captures events when the keybinding resolver is visible', async () => { + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + const keybindingResolverView = atom.workspace.getBottomDock().getActivePaneItem() + expect(keybindingResolverView.keybindingDisposables).not.toBe(null) + + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + }) + + it('does not capture events when the keybinding resolver is not the active pane item', async () => { + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + const keybindingResolverView = atom.workspace.getBottomDock().getActivePaneItem() + expect(keybindingResolverView.keybindingDisposables).not.toBe(null) + + atom.workspace.getBottomDock().getActivePane().splitRight() + expect(keybindingResolverView.keybindingDisposables).toBe(null) + + atom.workspace.getBottomDock().getActivePane().destroy() + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + }) + + it('does not capture events when the dock the keybinding resolver is in is not visible', async () => { + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + const keybindingResolverView = atom.workspace.getBottomDock().getActivePaneItem() + expect(keybindingResolverView.keybindingDisposables).not.toBe(null) + + atom.workspace.getBottomDock().hide() + expect(keybindingResolverView.keybindingDisposables).toBe(null) + + atom.workspace.getBottomDock().show() + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + }) + }) + + describe('when a keydown event occurs', () => { + it('displays all commands for the keydown event but does not clear for the keyup when there is no keyup binding', async () => { + atom.keymaps.add('name', { + '.workspace': { + 'x': 'match-1' + } + }) + atom.keymaps.add('name', { + '.workspace': { + 'x': 'match-2' + } + }) + atom.keymaps.add('name', { + '.never-again': { + 'x': 'unmatch-2' + } + }) + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(1) + + // It should not render the keyup event data because there is no match + spyOn(etch.getScheduler(), 'updateDocument').andCallThrough() + document.dispatchEvent(atom.keymaps.constructor.buildKeyupEvent('x', {target: bottomDockElement})) + expect(etch.getScheduler().updateDocument).not.toHaveBeenCalled() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(1) + }) + + it('displays all commands for the keydown event but does not clear for the keyup when there is no keyup binding', async () => { + atom.keymaps.add('name', { + '.workspace': { + 'x': 'match-1' + } + }) + atom.keymaps.add('name', { + '.workspace': { + 'x ^x': 'match-2' + } + }) + atom.keymaps.add('name', { + '.workspace': { + 'a ^a': 'match-3' + } + }) + atom.keymaps.add('name', { + '.never-again': { + 'x': 'unmatch-2' + } + }) + + await atom.commands.dispatch(workspaceElement, 'key-binding-resolver:toggle') + + // Not partial because it dispatches the command for `x` immediately due to only having keyup events in remainder of partial match + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(0) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(1) + + // It should not render the keyup event data because there is no match + document.dispatchEvent(atom.keymaps.constructor.buildKeyupEvent('x', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('x ^x') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(0) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(0) + + document.dispatchEvent(atom.keymaps.constructor.buildKeydownEvent('a', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('a (partial)') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(0) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(0) + + document.dispatchEvent(atom.keymaps.constructor.buildKeyupEvent('a', {target: bottomDockElement})) + await etch.getScheduler().getNextUpdatePromise() + expect(bottomDockElement.querySelector('.key-binding-resolver .keystroke').textContent).toBe('a ^a') + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .used')).toHaveLength(1) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unused')).toHaveLength(0) + expect(bottomDockElement.querySelectorAll('.key-binding-resolver .unmatched')).toHaveLength(0) + }) + }) +}) diff --git a/packages/keybinding-resolver/styles/keybinding-resolver.less b/packages/keybinding-resolver/styles/keybinding-resolver.less new file mode 100644 index 000000000..3637be5aa --- /dev/null +++ b/packages/keybinding-resolver/styles/keybinding-resolver.less @@ -0,0 +1,64 @@ +@import "ui-variables"; +@import "octicon-mixins"; + +.key-binding-resolver { + overflow: auto; + + .panel-heading { + position: sticky; + top: 0; + z-index: 1; + } + + .panel-body { + padding: 0 @component-padding; + } + + table { + + tr:not(:last-child) { + border-bottom: 1px solid @base-border-color; + } + + .used { + color: @text-color-success; + } + + .unused { + color: @text-color; + } + + .unmatched { + color: @text-color-subtle; + } + + // move icon so text is aligned when wrapped + .command { + padding-left: @component-icon-size; + &:before { + position: absolute; + margin-left: -@component-icon-size; + } + } + + .used .command, + .unused .command { + .octicon(check); + } + + .unmatched .command { + .octicon(x); + } + + .command, + .selector, + .source { + min-width: 10em; + word-break: break-word; // wrap long path names + } + + .source { + cursor: pointer; + } + } +}