diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.css b/src/vs/workbench/browser/parts/quickinput/quickInput.css index b8e411d30cf36..6b8fcbd826dc4 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.css +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.css @@ -12,6 +12,10 @@ margin-left: -300px; } +.quick-input-box { + margin: 6px; +} + .quick-input-actions { padding: 3px; } diff --git a/src/vs/workbench/browser/parts/quickinput/quickInput.ts b/src/vs/workbench/browser/parts/quickinput/quickInput.ts index dd1b1817aa254..c6d52227e3bc5 100644 --- a/src/vs/workbench/browser/parts/quickinput/quickInput.ts +++ b/src/vs/workbench/browser/parts/quickinput/quickInput.ts @@ -10,10 +10,7 @@ import { Component } from 'vs/workbench/common/component'; import { IQuickInputService } from 'vs/platform/quickInput/common/quickInput'; import { IPartService } from 'vs/workbench/services/part/common/partService'; import { Dimension } from 'vs/base/browser/builder'; -import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; import * as dom from 'vs/base/browser/dom'; -import { dispose, IDisposable } from 'vs/base/common/lifecycle'; -import { WorkbenchList } from 'vs/platform/list/browser/listService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { registerThemingParticipant, ITheme, ICssStyleCollector, IThemeService } from 'vs/platform/theme/common/themeService'; import { buttonBackground, buttonForeground, contrastBorder, buttonHoverBackground, widgetShadow } from 'vs/platform/theme/common/colorRegistry'; @@ -21,70 +18,11 @@ import { SIDE_BAR_BACKGROUND, SIDE_BAR_FOREGROUND } from 'vs/workbench/common/th import { IPickOpenEntry, IPickOptions } from 'vs/platform/quickOpen/common/quickOpen'; import { TPromise } from 'vs/base/common/winjs.base'; import { CancellationToken } from 'vs/base/common/cancellation'; +import { QuickInputCheckboxList } from './quickInputCheckboxList'; +import { QuickInputBox } from './quickInputBox'; const $ = dom.$; -export interface ISelectedElement { - item: object; - label: string; - selected: boolean; -} - -interface ISelectedElementTemplateData { - element: HTMLElement; - name: HTMLElement; - checkbox: HTMLInputElement; - context: ISelectedElement; - toDispose: IDisposable[]; -} - -class SelectedElementRenderer implements IRenderer { - - static readonly ID = 'selectedelement'; - - get templateId() { - return SelectedElementRenderer.ID; - } - - renderTemplate(container: HTMLElement): ISelectedElementTemplateData { - const data: ISelectedElementTemplateData = Object.create(null); - data.element = dom.append(container, $('.selected_element')); - - data.checkbox = $('input'); - data.checkbox.type = 'checkbox'; - data.toDispose = []; - data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => data.context.selected = !data.context.selected)); - - dom.append(data.element, data.checkbox); - - data.name = dom.append(data.element, $('span.label')); - - return data; - } - - renderElement(element: ISelectedElement, index: number, data: ISelectedElementTemplateData): void { - data.context = element; - data.name.textContent = element.label; - data.element.title = data.name.textContent; - data.checkbox.checked = element.selected; - } - - disposeTemplate(templateData: ISelectedElementTemplateData): void { - dispose(templateData.toDispose); - } -} - -class SelectedElementDelegate implements IDelegate { - - getHeight(element: ISelectedElement): number { - return 22; - } - - getTemplateId(element: ISelectedElement): string { - return SelectedElementRenderer.ID; - } -} - export class QuickInputService extends Component implements IQuickInputService { public _serviceBrand: any; @@ -95,9 +33,9 @@ export class QuickInputService extends Component implements IQuickInputService { private layoutDimensions: Dimension; private container: HTMLElement; - private list: WorkbenchList; + private inputBox: QuickInputBox; + private checkboxList: QuickInputCheckboxList; - private elements: ISelectedElement[] = []; private resolve: (value?: object[] | Thenable) => void; constructor( @@ -117,12 +55,13 @@ export class QuickInputService extends Component implements IQuickInputService { this.container = dom.append(workbench, $('.quick-input-widget')); this.container.style.display = 'none'; - const listContainer = dom.append(this.container, $('.quick-input-list')); - const delegate = new SelectedElementDelegate(); - this.list = this.instantiationService.createInstance(WorkbenchList, listContainer, delegate, [new SelectedElementRenderer()], { - identityProvider: element => element.label, - multipleSelectionSupport: false - }) as WorkbenchList; + this.inputBox = new QuickInputBox(this.container); + this.inputBox.style(this.themeService.getTheme()); + this.inputBox.onInput(value => { + this.checkboxList.filter(value); + }); + + this.checkboxList = this.instantiationService.createInstance(QuickInputCheckboxList, this.container); const buttonContainer = dom.append(this.container, $('.quick-input-actions')); const cancel = dom.append(buttonContainer, $('button')); @@ -144,7 +83,7 @@ export class QuickInputService extends Component implements IQuickInputService { private close(ok: boolean) { if (ok) { - this.resolve(this.elements.filter(e => e.selected).map(e => e.item)); + this.resolve(this.checkboxList.getSelectedElements()); } else { this.resolve(); } @@ -154,18 +93,13 @@ export class QuickInputService extends Component implements IQuickInputService { async pick(picks: TPromise, options?: IPickOptions, token?: CancellationToken): TPromise { this.create(); + this.inputBox.setPlaceholder(options.placeHolder || ''); // TODO: Progress indication. - this.elements = (await picks).map(item => ({ - item, - label: item.label, - selected: !!item.selected - })); - this.list.splice(0, this.list.length, this.elements); + this.checkboxList.setElements(await picks); this.container.style.display = null; this.updateLayout(); - this.list.focusFirst(); - this.list.domFocus(); + this.inputBox.setFocus(); return new TPromise(resolve => this.resolve = resolve); } @@ -185,9 +119,14 @@ export class QuickInputService extends Component implements IQuickInputService { style.width = width + 'px'; style.marginLeft = '-' + (width / 2) + 'px'; - this.list.layout(); + this.inputBox.layout(); + this.checkboxList.layout(); } } + + protected updateStyles() { + this.inputBox.style(this.themeService.getTheme()); + } } registerThemingParticipant((theme: ITheme, collector: ICssStyleCollector) => { diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts new file mode 100644 index 0000000000000..e3e99bfbb40b8 --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInputBox.ts @@ -0,0 +1,63 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./quickInput'; +import * as dom from 'vs/base/browser/dom'; +import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox'; +import * as nls from 'vs/nls'; +import { inputBackground, inputForeground, inputBorder } from 'vs/platform/theme/common/colorRegistry'; +import { ITheme } from 'vs/platform/theme/common/themeService'; +import { IDisposable } from 'vs/base/common/lifecycle'; + +const $ = dom.$; + +const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickInputBoxAriaLabel', "Type to narrow down results."); + +export class QuickInputBox { + + public container: HTMLElement; + private inputBox: InputBox; + + constructor( + private parent: HTMLElement + ) { + this.container = dom.append(this.parent, $('.quick-input-box')); + this.inputBox = new InputBox(this.container, null, { + ariaLabel: DEFAULT_INPUT_ARIA_LABEL + }); + + // ARIA + const inputElement = this.inputBox.inputElement; + inputElement.setAttribute('role', 'combobox'); + inputElement.setAttribute('aria-haspopup', 'false'); + inputElement.setAttribute('aria-autocomplete', 'list'); + } + + onInput(handler: (event: string) => void): IDisposable { + return this.inputBox.onDidChange(handler); + } + + setPlaceholder(placeholder: string) { + this.inputBox.setPlaceHolder(placeholder); + } + + setFocus(): void { + this.inputBox.focus(); + } + + layout(): void { + this.inputBox.layout(); + } + + style(theme: ITheme) { + this.inputBox.style({ + inputForeground: theme.getColor(inputForeground), + inputBackground: theme.getColor(inputBackground), + inputBorder: theme.getColor(inputBorder) + }); + } +} diff --git a/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts new file mode 100644 index 0000000000000..d973dbf32928e --- /dev/null +++ b/src/vs/workbench/browser/parts/quickinput/quickInputCheckboxList.ts @@ -0,0 +1,194 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import 'vs/css!./quickInput'; +import { IDelegate, IRenderer } from 'vs/base/browser/ui/list/list'; +import * as dom from 'vs/base/browser/dom'; +import { dispose, IDisposable } from 'vs/base/common/lifecycle'; +import { WorkbenchList } from 'vs/platform/list/browser/listService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IPickOpenEntry } from 'vs/platform/quickOpen/common/quickOpen'; +import { IMatch } from 'vs/base/common/filters'; +import { matchesFuzzyOcticonAware, parseOcticons } from 'vs/base/common/octicon'; +import { compareAnything } from 'vs/base/common/comparers'; + +const $ = dom.$; + +export interface ISelectedElement { + index: number; + item: object; + label: string; + shouldAlwaysShow?: boolean; + hidden?: boolean; + selected?: boolean; + labelHighlights?: IMatch[]; + descriptionHighlights?: IMatch[]; + detailHighlights?: IMatch[]; +} + +interface ISelectedElementTemplateData { + element: HTMLElement; + name: HTMLElement; + checkbox: HTMLInputElement; + context: ISelectedElement; + toDispose: IDisposable[]; +} + +class SelectedElementRenderer implements IRenderer { + + static readonly ID = 'selectedelement'; + + get templateId() { + return SelectedElementRenderer.ID; + } + + renderTemplate(container: HTMLElement): ISelectedElementTemplateData { + const data: ISelectedElementTemplateData = Object.create(null); + data.element = dom.append(container, $('.selected_element')); + + data.checkbox = $('input'); + data.checkbox.type = 'checkbox'; + data.toDispose = []; + data.toDispose.push(dom.addStandardDisposableListener(data.checkbox, 'change', (e) => data.context.selected = !data.context.selected)); + + dom.append(data.element, data.checkbox); + + data.name = dom.append(data.element, $('span.label')); + + return data; + } + + renderElement(element: ISelectedElement, index: number, data: ISelectedElementTemplateData): void { + data.context = element; + data.name.textContent = element.label; + data.element.title = data.name.textContent; + data.checkbox.checked = element.selected; + } + + disposeTemplate(templateData: ISelectedElementTemplateData): void { + dispose(templateData.toDispose); + } +} + +class SelectedElementDelegate implements IDelegate { + + getHeight(element: ISelectedElement): number { + return 22; + } + + getTemplateId(element: ISelectedElement): string { + return SelectedElementRenderer.ID; + } +} + +export class QuickInputCheckboxList { + + container: HTMLElement; + private list: WorkbenchList; + private elements: ISelectedElement[] = []; + + constructor( + private parent: HTMLElement, + @IInstantiationService private instantiationService: IInstantiationService + ) { + this.container = dom.append(this.parent, $('.quick-input-checkbox-list')); + const delegate = new SelectedElementDelegate(); + this.list = this.instantiationService.createInstance(WorkbenchList, this.container, delegate, [new SelectedElementRenderer()], { + identityProvider: element => element.label, + multipleSelectionSupport: false + }) as WorkbenchList; + } + + setElements(elements: IPickOpenEntry[]): void { + this.elements = elements.map((item, index) => ({ + index, + item, + label: item.label, + selected: !!item.selected + })); + this.list.splice(0, this.list.length, this.elements); + } + + getSelectedElements() { + return this.elements.filter(e => e.selected) + .map(e => e.item); + } + + setFocus(): void { + this.list.focusFirst(); + this.list.domFocus(); + } + + layout(): void { + this.list.layout(); + } + + filter(query: string) { + query = query.trim(); + + // Reset filtering + if (!query) { + this.elements.forEach(element => { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = false; + }); + } + + // Filter by value (since we support octicons, use octicon aware fuzzy matching) + else { + this.elements.forEach(element => { + const labelHighlights = matchesFuzzyOcticonAware(query, parseOcticons(element.label)); + const descriptionHighlights = undefined; // TODO matchesFuzzyOcticonAware(query, parseOcticons(element.description)); + const detailHighlights = undefined; // TODO matchesFuzzyOcticonAware(query, parseOcticons(element.detail)); + + if (element.shouldAlwaysShow || labelHighlights || descriptionHighlights || detailHighlights) { + element.labelHighlights = labelHighlights; + element.descriptionHighlights = descriptionHighlights; + element.detailHighlights = detailHighlights; + element.hidden = false; + } else { + element.labelHighlights = undefined; + element.descriptionHighlights = undefined; + element.detailHighlights = undefined; + element.hidden = true; + } + }); + } + + // Sort by value + const normalizedSearchValue = query.toLowerCase(); + this.elements.sort((a, b) => { + if (!query) { + return a.index - b.index; // restore natural order + } + return compareEntries(a, b, normalizedSearchValue); + }); + + this.list.splice(0, this.list.length, this.elements.filter(element => !element.hidden)); + this.list.layout(); + if (query) { + this.list.focusFirst(); + } + } +} + +function compareEntries(elementA: ISelectedElement, elementB: ISelectedElement, lookFor: string): number { + + const labelHighlightsA = elementA.labelHighlights || []; + const labelHighlightsB = elementB.labelHighlights || []; + if (labelHighlightsA.length && !labelHighlightsB.length) { + return -1; + } + + if (!labelHighlightsA.length && labelHighlightsB.length) { + return 1; + } + + return compareAnything(elementA.label, elementB.label, lookFor); +} \ No newline at end of file