diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts index 31fe9e34810e8f0767dcea54f1b8ae0f03c8ea90..7211b8f397cce51c7b908222140ed175ef146bb5 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/quickInput.test.ts @@ -9,6 +9,13 @@ import * as assert from 'assert'; import { window, commands } from 'vscode'; import { closeAllEditors } from '../utils'; +interface QuickPickExpected { + events: string[]; + activeItems: string[][]; + selectionItems: string[][]; + acceptedItems: string[][]; +} + suite('window namespace tests', function () { suite('QuickInput tests', function () { @@ -20,59 +27,87 @@ suite('window namespace tests', function () { _done(err); }; - const expectedEvents = ['active', 'active', 'selection', 'accept', 'hide']; - const expectedActiveItems = [['eins'], ['zwei']]; - const expectedSelectionItems = [['zwei']]; + const quickPick = createQuickPick({ + events: ['active', 'active', 'selection', 'accept', 'hide'], + activeItems: [['eins'], ['zwei']], + selectionItems: [['zwei']], + acceptedItems: [['zwei']], + }, done); + quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.show(); - const quickPick = window.createQuickPick(); - quickPick.onDidChangeActive(items => { - try { - assert.equal('active', expectedEvents.shift()); - const expected = expectedActiveItems.shift(); - assert.deepEqual(items.map(item => item.label), expected); - assert.deepEqual(quickPick.activeItems.map(item => item.label), expected); - } catch (err) { - done(err); - } - }); - quickPick.onDidChangeSelection(items => { - try { - assert.equal('selection', expectedEvents.shift()); - const expected = expectedSelectionItems.shift(); - assert.deepEqual(items.map(item => item.label), expected); - assert.deepEqual(quickPick.selectedItems.map(item => item.label), expected); - } catch (err) { - done(err); - } - }); - quickPick.onDidAccept(() => { - try { - assert.equal('accept', expectedEvents.shift()); - const expected = ['zwei']; - assert.deepEqual(quickPick.activeItems.map(item => item.label), expected); - assert.deepEqual(quickPick.selectedItems.map(item => item.label), expected); - quickPick.dispose(); - } catch (err) { - done(err); - } - }); - quickPick.onDidHide(() => { - try { - assert.equal('hide', expectedEvents.shift()); - done(); - } catch (err) { - done(err); - } - }); + (async () => { + await commands.executeCommand('workbench.action.quickOpenSelectNext'); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + })() + .catch(err => done(err)); + }); + test('createQuickPick, focus second', function (_done) { + let done = (err?: any) => { + done = () => {}; + _done(err); + }; + + const quickPick = createQuickPick({ + events: ['active', 'selection', 'accept', 'hide'], + activeItems: [['zwei']], + selectionItems: [['zwei']], + acceptedItems: [['zwei']], + }, done); quickPick.items = ['eins', 'zwei', 'drei'].map(label => ({ label })); + quickPick.activeItems = [quickPick.items[1]]; quickPick.show(); (async () => { - await commands.executeCommand('workbench.action.quickOpenSelectNext'); await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); })() .catch(err => done(err)); }); }); }); + +function createQuickPick(expected: QuickPickExpected, done: (err?: any) => void) { + const quickPick = window.createQuickPick(); + quickPick.onDidChangeActive(items => { + try { + assert.equal('active', expected.events.shift()); + const expectedItems = expected.activeItems.shift(); + assert.deepEqual(items.map(item => item.label), expectedItems); + assert.deepEqual(quickPick.activeItems.map(item => item.label), expectedItems); + } catch (err) { + done(err); + } + }); + quickPick.onDidChangeSelection(items => { + try { + assert.equal('selection', expected.events.shift()); + const expectedItems = expected.selectionItems.shift(); + assert.deepEqual(items.map(item => item.label), expectedItems); + assert.deepEqual(quickPick.selectedItems.map(item => item.label), expectedItems); + } catch (err) { + done(err); + } + }); + quickPick.onDidAccept(() => { + try { + assert.equal('accept', expected.events.shift()); + const expectedItems = expected.acceptedItems.shift(); + assert.deepEqual(quickPick.activeItems.map(item => item.label), expectedItems); + assert.deepEqual(quickPick.selectedItems.map(item => item.label), expectedItems); + quickPick.dispose(); + } catch (err) { + done(err); + } + }); + quickPick.onDidHide(() => { + try { + assert.equal('hide', expected.events.shift()); + done(); + } catch (err) { + done(err); + } + }); + + return quickPick; +} \ No newline at end of file diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts index 449cc78536a93b9099f134f3ee7aca8e219c9f3c..87734062b8a006d56828bb3b1fb576cbba43420d 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/window.test.ts @@ -440,6 +440,18 @@ suite('window namespace tests', () => { assert.deepStrictEqual(await picks, ['eins', 'zwei']); }); + test('showQuickPick, keep selection (Microsoft/vscode-azure-account#67)', async function () { + const picks = window.showQuickPick([ + { label: 'eins' }, + { label: 'zwei', picked: true }, + { label: 'drei', picked: true } + ], { + canPickMany: true + }); + await commands.executeCommand('workbench.action.acceptSelectedQuickOpenItem'); + assert.deepStrictEqual((await picks)!.map(pick => pick.label), ['zwei', 'drei']); + }); + test('showQuickPick, undefined on cancel', function () { const source = new CancellationTokenSource(); const p = window.showQuickPick(['eins', 'zwei', 'drei'], undefined, source.token); diff --git a/src/vs/platform/quickinput/common/quickInput.ts b/src/vs/platform/quickinput/common/quickInput.ts index ae0f47d225fdf70d4e1307d66fe910803ad065a7..20da81e28b9d6fcc3d8ec214e22092d2247a33d1 100644 --- a/src/vs/platform/quickinput/common/quickInput.ts +++ b/src/vs/platform/quickinput/common/quickInput.ts @@ -131,11 +131,11 @@ export interface IQuickPick extends IQuickInput { matchOnDetail: boolean; - readonly activeItems: ReadonlyArray; + activeItems: ReadonlyArray; readonly onDidChangeActive: Event; - readonly selectedItems: ReadonlyArray; + selectedItems: ReadonlyArray; readonly onDidChangeSelection: Event; } diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 7076a28a820dbc3810e92101ae77497185adb149..acd0beaabe07b7a0391c46e125cfb0d886a22148 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -581,11 +581,11 @@ declare module 'vscode' { matchOnDetail: boolean; - readonly activeItems: ReadonlyArray; + activeItems: ReadonlyArray; readonly onDidChangeActive: Event; - readonly selectedItems: ReadonlyArray; + selectedItems: ReadonlyArray; readonly onDidChangeSelection: Event; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts index 1afbe3b936ed163f2130f9625f4223de20ecdb8b..5afe23566d3b9050d92021d9a1479f3c7ea58240 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadQuickOpen.ts @@ -12,6 +12,11 @@ import { ExtHostContext, MainThreadQuickOpenShape, ExtHostQuickOpenShape, Transf import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import URI from 'vs/base/common/uri'; +interface QuickInputSession { + input: IQuickInput; + handlesToItems: Map; +} + @extHostNamedCustomer(MainContext.MainThreadQuickOpen) export class MainThreadQuickOpen implements MainThreadQuickOpenShape { @@ -114,7 +119,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { // ---- QuickInput - private sessions = new Map(); + private sessions = new Map(); $createOrUpdate(params: TransferQuickInput): TPromise { const sessionId = params.id; @@ -140,7 +145,10 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { input.onDidHide(() => { this._proxy.$onDidHide(sessionId); }); - session = input; + session = { + input, + handlesToItems: new Map() + }; } else { const input = this._quickInputService.createInputBox(); input.onDidAccept(() => { @@ -155,22 +163,36 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { input.onDidHide(() => { this._proxy.$onDidHide(sessionId); }); - session = input; + session = { + input, + handlesToItems: new Map() + }; } this.sessions.set(sessionId, session); } + const { input, handlesToItems } = session; for (const param in params) { if (param === 'id' || param === 'type') { continue; } if (param === 'visible') { if (params.visible) { - session.show(); + input.show(); } else { - session.hide(); + input.hide(); } + } else if (param === 'items') { + handlesToItems.clear(); + params[param].forEach(item => { + handlesToItems.set(item.handle, item); + }); + input[param] = params[param]; + } else if (param === 'activeItems' || param === 'selectedItems') { + input[param] = params[param] + .filter(handle => handlesToItems.has(handle)) + .map(handle => handlesToItems.get(handle)); } else if (param === 'buttons') { - session[param] = params.buttons.map(button => { + input[param] = params.buttons.map(button => { if (button.handle === -1) { return this._quickInputService.backButton; } @@ -185,7 +207,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { }; }); } else { - session[param] = params[param]; + input[param] = params[param]; } } return TPromise.as(undefined); @@ -194,7 +216,7 @@ export class MainThreadQuickOpen implements MainThreadQuickOpenShape { $dispose(sessionId: number): TPromise { const session = this.sessions.get(sessionId); if (session) { - session.dispose(); + session.input.dispose(); this.sessions.delete(sessionId); } return TPromise.as(undefined); diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 5dd9e5517bc8eae7b82f1ba166556b4ac041bef9..b037ad5bc34362637d5f6b06404729947d641a2f 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -380,6 +380,10 @@ export interface TransferQuickPick extends BaseTransferQuickInput { items?: TransferQuickPickItems[]; + activeItems?: number[]; + + selectedItems?: number[]; + canSelectMany?: boolean; ignoreFocusOut?: boolean; diff --git a/src/vs/workbench/api/node/extHostQuickOpen.ts b/src/vs/workbench/api/node/extHostQuickOpen.ts index b7fd0b53aa09c21fe8e1a2c8b4510efb4e2e48b7..1673e4b4876cf4d9d52cd93c715d2a1026e5e2bd 100644 --- a/src/vs/workbench/api/node/extHostQuickOpen.ts +++ b/src/vs/workbench/api/node/extHostQuickOpen.ts @@ -455,6 +455,7 @@ class ExtHostQuickPick extends ExtHostQuickInput implements QuickPick { private _items: QuickPickItem[] = []; private _handlesToItems = new Map(); + private _itemsToHandles = new Map(); private _canSelectMany = false; private _matchOnDescription = true; private _matchOnDetail = true; @@ -479,8 +480,10 @@ class ExtHostQuickPick extends ExtHostQuickInput implements QuickPick { set items(items: QuickPickItem[]) { this._items = items; this._handlesToItems.clear(); + this._itemsToHandles.clear(); items.forEach((item, i) => { this._handlesToItems.set(i, item); + this._itemsToHandles.set(item, i); }); this.update({ items: items.map((item, i) => ({ @@ -524,12 +527,22 @@ class ExtHostQuickPick extends ExtHostQuickInput implements QuickPick { return this._activeItems; } + set activeItems(activeItems: QuickPickItem[]) { + this._activeItems = activeItems.filter(item => this._itemsToHandles.has(item)); + this.update({ activeItems: this._activeItems.map(item => this._itemsToHandles.get(item)) }); + } + onDidChangeActive = this._onDidChangeActiveEmitter.event; get selectedItems() { return this._selectedItems; } + set selectedItems(selectedItems: QuickPickItem[]) { + this._selectedItems = selectedItems.filter(item => this._itemsToHandles.has(item)); + this.update({ selectedItems: this._selectedItems.map(item => this._itemsToHandles.get(item)) }); + } + onDidChangeSelection = this._onDidChangeSelectionEmitter.event; _fireDidChangeActive(handles: number[]) { diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index 83529563f38de24407da4f106a4213df1d901bf2..5d22a8c62ccec04a82197ff659978f532a26c462 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -266,8 +266,10 @@ class QuickPick extends QuickInput implements IQuickPick { private _matchOnDescription = true; private _matchOnDetail = true; private _activeItems: IQuickPickItem[] = []; + private activeItemsUpdated = false; private onDidChangeActiveEmitter = new Emitter(); private _selectedItems: IQuickPickItem[] = []; + private selectedItemsUpdated = false; private onDidChangeSelectionEmitter = new Emitter(); private quickNavigate = false; @@ -344,12 +346,24 @@ class QuickPick extends QuickInput implements IQuickPick { return this._activeItems; } + set activeItems(activeItems: IQuickPickItem[]) { + this._activeItems = activeItems; + this.activeItemsUpdated = true; + this.update(); + } + onDidChangeActive = this.onDidChangeActiveEmitter.event; get selectedItems() { return this._selectedItems; } + set selectedItems(selectedItems: IQuickPickItem[]) { + this._selectedItems = selectedItems; + this.selectedItemsUpdated = true; + this.update(); + } + onDidChangeSelection = this.onDidChangeSelectionEmitter.event; show() { @@ -390,6 +404,9 @@ class QuickPick extends QuickInput implements IQuickPick { this.onDidAcceptEmitter.fire(); }), this.ui.list.onDidChangeFocus(focusedItems => { + if (this.activeItemsUpdated) { + return; // Expect another event. + } // Drop initial event. if (!focusedItems.length && !this._activeItems.length) { return; @@ -433,6 +450,7 @@ class QuickPick extends QuickInput implements IQuickPick { this.ui.inputBox.placeholder = (this.placeholder || ''); } if (this.itemsUpdated) { + this.itemsUpdated = false; this.ui.list.setElements(this.items); this.ui.list.filter(this.ui.inputBox.value); this.ui.checkAll.checked = this.ui.list.getAllVisibleChecked(); @@ -440,7 +458,6 @@ class QuickPick extends QuickInput implements IQuickPick { if (!this.canSelectMany) { this.ui.list.focus('First'); } - this.itemsUpdated = false; } if (this.ui.container.classList.contains('show-checkboxes') !== this.canSelectMany) { if (this.canSelectMany) { @@ -449,6 +466,18 @@ class QuickPick extends QuickInput implements IQuickPick { this.ui.list.focus('First'); } } + if (this.activeItemsUpdated) { + this.activeItemsUpdated = false; + this.ui.list.setFocusedElements(this.activeItems); + } + if (this.selectedItemsUpdated) { + this.selectedItemsUpdated = false; + if (this.canSelectMany) { + this.ui.list.setCheckedElements(this.selectedItems); + } else { + this.ui.list.setSelectedElements(this.selectedItems); + } + } this.ui.ignoreFocusOut = this.ignoreFocusOut; this.ui.list.matchOnDescription = this.matchOnDescription; this.ui.list.matchOnDetail = this.matchOnDetail; @@ -878,6 +907,9 @@ export class QuickInputService extends Component implements IQuickInputService { picks.then(items => { input.busy = false; input.items = items; + if (input.canSelectMany) { + input.selectedItems = items.filter(item => item.picked); + } }); input.show(); picks.then(null, err => { diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts index 96551bd6c5cc6072b7ce4274b1c42476535bba9e..6d5c5416319eb66f92387e955ad4c198abc8fc14 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInputList.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInputList.ts @@ -149,6 +149,7 @@ export class QuickInputList { private container: HTMLElement; private list: WorkbenchList; private elements: ListElement[] = []; + private elementsToIndexes = new Map(); matchOnDescription = false; matchOnDetail = false; private _onChangedAllVisibleChecked = new Emitter(); @@ -269,9 +270,14 @@ export class QuickInputList { this.elements = elements.map((item, index) => new ListElement({ index, item, - checked: !!item.picked + checked: false })); this.elementDisposables.push(...this.elements.map(element => element.onChecked(() => this.fireCheckedEvents()))); + + this.elementsToIndexes = this.elements.reduce((map, element, index) => { + map.set(element.item, index); + return map; + }, new Map()); this.list.splice(0, this.list.length, this.elements); this.list.setFocus([]); } @@ -281,16 +287,44 @@ export class QuickInputList { .map(e => e.item); } + setFocusedElements(items: IQuickPickItem[]) { + this.list.setFocus(items + .filter(item => this.elementsToIndexes.has(item)) + .map(item => this.elementsToIndexes.get(item))); + } + getSelectedElements() { return this.list.getSelectedElements() .map(e => e.item); } + setSelectedElements(items: IQuickPickItem[]) { + this.list.setSelection(items + .filter(item => this.elementsToIndexes.has(item)) + .map(item => this.elementsToIndexes.get(item))); + } + getCheckedElements() { return this.elements.filter(e => e.checked) .map(e => e.item); } + setCheckedElements(items: IQuickPickItem[]) { + try { + this._fireCheckedEvents = false; + const checked = new Set(); + for (const item of items) { + checked.add(item); + } + for (const element of this.elements) { + element.checked = checked.has(element.item); + } + } finally { + this._fireCheckedEvents = true; + this.fireCheckedEvents(); + } + } + set enabled(value: boolean) { this.list.getHTMLElement().style.pointerEvents = value ? null : 'none'; } @@ -368,6 +402,10 @@ export class QuickInputList { return compareEntries(a, b, normalizedSearchValue); }); + this.elementsToIndexes = shownElements.reduce((map, element, index) => { + map.set(element.item, index); + return map; + }, new Map()); this.list.splice(0, this.list.length, shownElements); this.list.setFocus([]); this.list.layout();