diff --git a/lib/command-palette-package.js b/lib/command-palette-package.js index d4332b9..6b9a584 100644 --- a/lib/command-palette-package.js +++ b/lib/command-palette-package.js @@ -4,8 +4,8 @@ import {CompositeDisposable} from 'atom' import CommandPaletteView from './command-palette-view' class CommandPalettePackage { - activate () { - this.commandPaletteView = new CommandPaletteView() + activate (state) { + this.commandPaletteView = new CommandPaletteView(state) this.disposables = new CompositeDisposable() this.disposables.add(atom.commands.add('atom-workspace', 'command-palette:toggle', () => { this.commandPaletteView.toggle() @@ -16,9 +16,16 @@ class CommandPalettePackage { this.disposables.add(atom.config.observe('command-palette.preserveLastSearch', (newValue) => { this.commandPaletteView.update({preserveLastSearch: newValue}) })) + this.disposables.add(atom.config.observe('command-palette.initialOrderingOfItems', (newValue) => { + this.commandPaletteView.update({initialOrderingOfItems: newValue}) + })) return this.commandPaletteView.show() } + serialize() { + return this.commandPaletteView.serialize() + } + async deactivate () { this.disposables.dispose() await this.commandPaletteView.destroy() diff --git a/lib/command-palette-view.js b/lib/command-palette-view.js index 1526f72..69152d4 100644 --- a/lib/command-palette-view.js +++ b/lib/command-palette-view.js @@ -6,7 +6,8 @@ import fuzzaldrin from 'fuzzaldrin' import fuzzaldrinPlus from 'fuzzaldrin-plus' export default class CommandPaletteView { - constructor () { + constructor (state = {}) { + this.itemLaunches = state.itemLaunches || {} this.keyBindingsForActiveElement = [] this.commandsForActiveElement = [] this.selectListView = new SelectListView({ @@ -69,6 +70,14 @@ export default class CommandPaletteView { }, didConfirmSelection: (keyBinding) => { this.hide() + const elementName = keyBinding.name + const launchedAt = new Date().getTime() + if(this.itemLaunches.hasOwnProperty(elementName)) { + this.itemLaunches[elementName].push(launchedAt) + } + else { + this.itemLaunches[elementName] = [launchedAt] + } const event = new CustomEvent(keyBinding.name, {bubbles: true, cancelable: true}) this.activeElement.dispatchEvent(event) }, @@ -130,6 +139,10 @@ export default class CommandPaletteView { if (props.hasOwnProperty('useAlternateScoring')) { this.useAlternateScoring = props.useAlternateScoring } + + if (props.hasOwnProperty('initialOrderingOfItems')) { + this.initialOrderingOfItems = props.initialOrderingOfItems + } } get fuzz () { @@ -171,9 +184,39 @@ export default class CommandPaletteView { } } + serialize = () => ({ + itemLaunches: this.itemLaunches + }) + filter = (items, query) => { if (query.length === 0) { - return items + if (Object.keys(this.itemLaunches).length === 0) return items; + if (this.initialOrderingOfItems === 'alphabetic') return items; + + const scoredItems = [] + const unscoredItems = [] + + for (const item of items) { + const launchDates = this.itemLaunches[item.name] || [] + let score; + if (this.initialOrderingOfItems === 'frequency') { + score = launchDates.length + } + else if(this.initialOrderingOfItems === 'recent') { + score = launchDates[launchDates.length-1] + } + + if(score) { + scoredItems.push({item, score}) + } + else { + unscoredItems.push(item) + } + } + return scoredItems + .sort((a, b) => b.score - a.score) + .map(i => i.item) + .concat(unscoredItems) } const scoredItems = [] diff --git a/package.json b/package.json index 1a95a87..efe2177 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,16 @@ "type": "boolean", "default": false, "description": "Preserve the last search when reopening the command palette." + }, + "initialOrderingOfItems": { + "type": "string", + "default": "frequency", + "enum": [ + {"value": "frequency", "description": "By frequency of use"}, + {"value": "recent", "description": "By most recently used"}, + {"value": "alphabetic", "description": "Alphabetical order"} + ] + } } } diff --git a/test/command-palette-view.test.js b/test/command-palette-view.test.js index 2e50a8e..b1c36ce 100644 --- a/test/command-palette-view.test.js +++ b/test/command-palette-view.test.js @@ -22,6 +22,200 @@ describe('CommandPaletteView', () => { workspaceElement.remove() }) + describe('initial sorting', () => { + let commandPalette + const fakeCommands = [ + 'one', + 'two', + 'three', + ].map(command => `command-palette-test:${command}`) + + beforeEach(async () => { + commandPalette = new CommandPaletteView() + for (let i=0; i {}) + + const numTimesToLaunch = fakeCommands.length - i + //console.log(`Launching ${command.displayName} ${numTimesToLaunch} times`) + + for (j=0; j { + beforeEach(async () => { + await commandPalette.update({initialOrderingOfItems: 'frequency'}) + }) + + it('orders the scored items correctly', async () => { + await commandPalette.show() + await commandPalette.selectListView.refs.queryEditor.setText('') + await commandPalette.selectListView.update() + + fakeCommands.forEach(command => { + const selectedItem = commandPalette.selectListView.getSelectedItem().name + assert.equal(selectedItem, command) + commandPalette.selectListView.selectNext() + }) + }) + + it('orders the rest of the palette items alphabetically', async () => { + await commandPalette.show() + await commandPalette.selectListView.refs.queryEditor.setText('') + await commandPalette.selectListView.update() + + commandPalette.selectListView.selectFirst() + const firstItem = commandPalette.selectListView.getSelectedItem().name + + // skip scored items + for(let i=0; i { + const serializedState = commandPalette.serialize(); + const newCommandPalette = new CommandPaletteView(serializedState); + + await newCommandPalette.update({initialOrderingOfItems: 'frequency'}) + await newCommandPalette.show() + await newCommandPalette.selectListView.refs.queryEditor.setText('') + await newCommandPalette.selectListView.update() + + fakeCommands.forEach(command => { + const selectedItem = newCommandPalette.selectListView.getSelectedItem().name + assert.equal(selectedItem, command) + newCommandPalette.selectListView.selectNext() + }) + }) + }) + + describe('when initially sorting by recentness', () => { + beforeEach(async () => { + await commandPalette.update({initialOrderingOfItems: 'recent'}) + }) + + it('orders the scored items correctly', async () => { + await commandPalette.show() + await commandPalette.selectListView.refs.queryEditor.setText('') + await commandPalette.selectListView.update() + + fakeCommands.reverse().forEach(command => { + const selectedItem = commandPalette.selectListView.getSelectedItem().name + assert.equal(selectedItem, command) + commandPalette.selectListView.selectNext() + }) + }) + + it('orders the rest of the palette items alphabetically', async () => { + await commandPalette.show() + await commandPalette.selectListView.refs.queryEditor.setText('') + await commandPalette.selectListView.update() + + commandPalette.selectListView.selectFirst() + const firstItem = commandPalette.selectListView.getSelectedItem().name + + // skip scored items + for(let i=0; i { + const serializedState = commandPalette.serialize(); + const newCommandPalette = new CommandPaletteView(serializedState); + + await newCommandPalette.update({initialOrderingOfItems: 'recent'}) + await newCommandPalette.show() + await newCommandPalette.selectListView.refs.queryEditor.setText('') + await newCommandPalette.selectListView.update() + + fakeCommands.reverse().forEach(command => { + const selectedItem = newCommandPalette.selectListView.getSelectedItem().name + assert.equal(selectedItem, command) + newCommandPalette.selectListView.selectNext() + }) + }) + }) + + describe('when initially sorting alphabetically', () => { + beforeEach(async () => { + await commandPalette.update({initialOrderingOfItems: 'alphabetic'}) + }) + + it('orders the palette items correctly', async () => { + await commandPalette.show() + await commandPalette.selectListView.refs.queryEditor.setText('') + await commandPalette.selectListView.update() + + commandPalette.selectListView.selectFirst() + const firstItem = commandPalette.selectListView.getSelectedItem().name + + // compare pairwise items + let currentItem, previousItem + do { + previousItem = commandPalette.selectListView.getSelectedItem().name + commandPalette.selectListView.selectNext() + currentItem = commandPalette.selectListView.getSelectedItem().name + if(currentItem == firstItem) break; + assert.equal(previousItem.localeCompare(currentItem), -1) + } + while (previousItem != firstItem) + }) + + it('remembers the ordering between launches', async () => { + const serializedState = commandPalette.serialize(); + const newCommandPalette = new CommandPaletteView(serializedState); + + await newCommandPalette.update({initialOrderingOfItems: 'recent'}) + await newCommandPalette.show() + await newCommandPalette.selectListView.refs.queryEditor.setText('') + await newCommandPalette.selectListView.update() + + commandPalette.selectListView.selectFirst() + const firstItem = commandPalette.selectListView.getSelectedItem().name + + // compare pairwise items + let currentItem, previousItem + do { + previousItem = commandPalette.selectListView.getSelectedItem().name + commandPalette.selectListView.selectNext() + currentItem = commandPalette.selectListView.getSelectedItem().name + if(currentItem == firstItem) break; + assert.equal(previousItem.localeCompare(currentItem), -1) + } + while (previousItem != firstItem) + }) + }) + }) + describe('toggle', () => { describe('when an element is focused', () => { it('shows a list of all valid command descriptions, names, and keybindings for the previously focused element', async () => {