/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as nls from 'vs/nls'; import { TPromise } from 'vs/base/common/winjs.base'; import * as types from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { ITree, IActionProvider } from 'vs/base/parts/tree/browser/tree'; import { IconLabel, IIconLabelValueOptions } from 'vs/base/browser/ui/iconLabel/iconLabel'; import { IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode } from 'vs/base/parts/quickopen/common/quickOpen'; import { Action, IAction, IActionRunner } from 'vs/base/common/actions'; import { compareAnything } from 'vs/base/common/comparers'; import { ActionBar, IActionItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel'; import * as DOM from 'vs/base/browser/dom'; import { IQuickOpenStyles } from 'vs/base/parts/quickopen/browser/quickOpenWidget'; import { KeybindingLabel } from 'vs/base/browser/ui/keybindingLabel/keybindingLabel'; import { OS } from 'vs/base/common/platform'; import { ResolvedKeybinding } from 'vs/base/common/keyCodes'; import { IItemAccessor } from 'vs/base/parts/quickopen/common/quickOpenScorer'; export interface IContext { event: any; quickNavigateConfiguration: IQuickNavigateConfiguration; } export interface IHighlight { start: number; end: number; } let IDS = 0; export class QuickOpenItemAccessorClass implements IItemAccessor { getItemLabel(entry: QuickOpenEntry): string { return entry.getLabel(); } getItemDescription(entry: QuickOpenEntry): string { return entry.getDescription(); } getItemPath(entry: QuickOpenEntry): string { const resource = entry.getResource(); return resource ? resource.fsPath : void 0; } } export const QuickOpenItemAccessor = new QuickOpenItemAccessorClass(); export class QuickOpenEntry { private id: string; private labelHighlights: IHighlight[]; private descriptionHighlights: IHighlight[]; private detailHighlights: IHighlight[]; private hidden: boolean; constructor(highlights: IHighlight[] = []) { this.id = (IDS++).toString(); this.labelHighlights = highlights; this.descriptionHighlights = []; } /** * A unique identifier for the entry */ getId(): string { return this.id; } /** * The label of the entry to identify it from others in the list */ getLabel(): string { return null; } /** * The options for the label to use for this entry */ getLabelOptions(): IIconLabelValueOptions { return null; } /** * The label of the entry to use when a screen reader wants to read about the entry */ getAriaLabel(): string { return [this.getLabel(), this.getDescription(), this.getDetail()] .filter(s => !!s) .join(', '); } /** * Detail information about the entry that is optional and can be shown below the label */ getDetail(): string { return null; } /** * The icon of the entry to identify it from others in the list */ getIcon(): string { return null; } /** * A secondary description that is optional and can be shown right to the label */ getDescription(): string { return null; } /** * A tooltip to show when hovering over the entry. */ getTooltip(): string { return null; } /** * A tooltip to show when hovering over the description portion of the entry. */ getDescriptionTooltip(): string { return null; } /** * An optional keybinding to show for an entry. */ getKeybinding(): ResolvedKeybinding { return null; } /** * A resource for this entry. Resource URIs can be used to compare different kinds of entries and group * them together. */ getResource(): URI { return null; } /** * Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer. */ isHidden(): boolean { return this.hidden; } /** * Allows to reuse the same model while filtering. Hidden entries will not show up in the viewer. */ setHidden(hidden: boolean): void { this.hidden = hidden; } /** * Allows to set highlight ranges that should show up for the entry label and optionally description if set. */ setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void { this.labelHighlights = labelHighlights; this.descriptionHighlights = descriptionHighlights; this.detailHighlights = detailHighlights; } /** * Allows to return highlight ranges that should show up for the entry label and description. */ getHighlights(): [IHighlight[] /* Label */, IHighlight[] /* Description */, IHighlight[] /* Detail */] { return [this.labelHighlights, this.descriptionHighlights, this.detailHighlights]; } /** * Called when the entry is selected for opening. Returns a boolean value indicating if an action was performed or not. * The mode parameter gives an indication if the element is previewed (using arrow keys) or opened. * * The context parameter provides additional context information how the run was triggered. */ run(mode: Mode, context: IContext): boolean { return false; } /** * Determines if this quick open entry should merge with the editor history in quick open. If set to true * and the resource of this entry is the same as the resource for an editor history, it will not show up * because it is considered to be a duplicate of an editor history. */ mergeWithEditorHistory(): boolean { return false; } } export class QuickOpenEntryGroup extends QuickOpenEntry { private entry: QuickOpenEntry; private groupLabel: string; private withBorder: boolean; constructor(entry?: QuickOpenEntry, groupLabel?: string, withBorder?: boolean) { super(); this.entry = entry; this.groupLabel = groupLabel; this.withBorder = withBorder; } /** * The label of the group or null if none. */ getGroupLabel(): string { return this.groupLabel; } setGroupLabel(groupLabel: string): void { this.groupLabel = groupLabel; } /** * Whether to show a border on top of the group entry or not. */ showBorder(): boolean { return this.withBorder; } setShowBorder(showBorder: boolean): void { this.withBorder = showBorder; } getLabel(): string { return this.entry ? this.entry.getLabel() : super.getLabel(); } getLabelOptions(): IIconLabelValueOptions { return this.entry ? this.entry.getLabelOptions() : super.getLabelOptions(); } getAriaLabel(): string { return this.entry ? this.entry.getAriaLabel() : super.getAriaLabel(); } getDetail(): string { return this.entry ? this.entry.getDetail() : super.getDetail(); } getResource(): URI { return this.entry ? this.entry.getResource() : super.getResource(); } getIcon(): string { return this.entry ? this.entry.getIcon() : super.getIcon(); } getDescription(): string { return this.entry ? this.entry.getDescription() : super.getDescription(); } getEntry(): QuickOpenEntry { return this.entry; } getHighlights(): [IHighlight[], IHighlight[], IHighlight[]] { return this.entry ? this.entry.getHighlights() : super.getHighlights(); } isHidden(): boolean { return this.entry ? this.entry.isHidden() : super.isHidden(); } setHighlights(labelHighlights: IHighlight[], descriptionHighlights?: IHighlight[], detailHighlights?: IHighlight[]): void { this.entry ? this.entry.setHighlights(labelHighlights, descriptionHighlights, detailHighlights) : super.setHighlights(labelHighlights, descriptionHighlights, detailHighlights); } setHidden(hidden: boolean): void { this.entry ? this.entry.setHidden(hidden) : super.setHidden(hidden); } run(mode: Mode, context: IContext): boolean { return this.entry ? this.entry.run(mode, context) : super.run(mode, context); } } class NoActionProvider implements IActionProvider { hasActions(tree: ITree, element: any): boolean { return false; } getActions(tree: ITree, element: any): TPromise { return TPromise.as(null); } hasSecondaryActions(tree: ITree, element: any): boolean { return false; } getSecondaryActions(tree: ITree, element: any): TPromise { return TPromise.as(null); } getActionItem(tree: ITree, element: any, action: Action): IActionItem { return null; } } export interface IQuickOpenEntryTemplateData { container: HTMLElement; entry: HTMLElement; icon: HTMLSpanElement; label: IconLabel; detail: HighlightedLabel; keybinding: KeybindingLabel; actionBar: ActionBar; } export interface IQuickOpenEntryGroupTemplateData extends IQuickOpenEntryTemplateData { group: HTMLDivElement; } const templateEntry = 'quickOpenEntry'; const templateEntryGroup = 'quickOpenEntryGroup'; class Renderer implements IRenderer { private actionProvider: IActionProvider; private actionRunner: IActionRunner; constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner: IActionRunner | null = null) { this.actionProvider = actionProvider; this.actionRunner = actionRunner; } getHeight(entry: QuickOpenEntry): number { if (entry.getDetail()) { return 44; } return 22; } getTemplateId(entry: QuickOpenEntry): string { if (entry instanceof QuickOpenEntryGroup) { return templateEntryGroup; } return templateEntry; } renderTemplate(templateId: string, container: HTMLElement, styles: IQuickOpenStyles): IQuickOpenEntryGroupTemplateData { const entryContainer = document.createElement('div'); DOM.addClass(entryContainer, 'sub-content'); container.appendChild(entryContainer); // Entry const row1 = DOM.$('.quick-open-row'); const row2 = DOM.$('.quick-open-row'); const entry = DOM.$('.quick-open-entry', null, row1, row2); entryContainer.appendChild(entry); // Icon const icon = document.createElement('span'); row1.appendChild(icon); // Label const label = new IconLabel(row1, { supportHighlights: true, supportDescriptionHighlights: true }); // Keybinding const keybindingContainer = document.createElement('span'); row1.appendChild(keybindingContainer); DOM.addClass(keybindingContainer, 'quick-open-entry-keybinding'); const keybinding = new KeybindingLabel(keybindingContainer, OS); // Detail const detailContainer = document.createElement('div'); row2.appendChild(detailContainer); DOM.addClass(detailContainer, 'quick-open-entry-meta'); const detail = new HighlightedLabel(detailContainer, true); // Entry Group let group: HTMLDivElement; if (templateId === templateEntryGroup) { group = document.createElement('div'); DOM.addClass(group, 'results-group'); container.appendChild(group); } // Actions DOM.addClass(container, 'actions'); const actionBarContainer = document.createElement('div'); DOM.addClass(actionBarContainer, 'primary-action-bar'); container.appendChild(actionBarContainer); const actionBar = new ActionBar(actionBarContainer, { actionRunner: this.actionRunner }); return { container, entry, icon, label, detail, keybinding, group, actionBar }; } renderElement(entry: QuickOpenEntry, templateId: string, data: IQuickOpenEntryGroupTemplateData, styles: IQuickOpenStyles): void { // Action Bar if (this.actionProvider.hasActions(null, entry)) { DOM.addClass(data.container, 'has-actions'); } else { DOM.removeClass(data.container, 'has-actions'); } data.actionBar.context = entry; // make sure the context is the current element this.actionProvider.getActions(null, entry).then((actions) => { if (data.actionBar.isEmpty() && actions && actions.length > 0) { data.actionBar.push(actions, { icon: true, label: false }); } else if (!data.actionBar.isEmpty() && (!actions || actions.length === 0)) { data.actionBar.clear(); } }); // Entry group class if (entry instanceof QuickOpenEntryGroup && entry.getGroupLabel()) { DOM.addClass(data.container, 'has-group-label'); } else { DOM.removeClass(data.container, 'has-group-label'); } // Entry group if (entry instanceof QuickOpenEntryGroup) { const group = entry; const groupData = data; // Border if (group.showBorder()) { DOM.addClass(groupData.container, 'results-group-separator'); groupData.container.style.borderTopColor = styles.pickerGroupBorder.toString(); } else { DOM.removeClass(groupData.container, 'results-group-separator'); groupData.container.style.borderTopColor = null; } // Group Label const groupLabel = group.getGroupLabel() || ''; groupData.group.textContent = groupLabel; groupData.group.style.color = styles.pickerGroupForeground.toString(); } // Normal Entry if (entry instanceof QuickOpenEntry) { const [labelHighlights, descriptionHighlights, detailHighlights] = entry.getHighlights(); // Icon const iconClass = entry.getIcon() ? ('quick-open-entry-icon ' + entry.getIcon()) : ''; data.icon.className = iconClass; // Label const options: IIconLabelValueOptions = entry.getLabelOptions() || Object.create(null); options.matches = labelHighlights || []; options.title = entry.getTooltip(); options.descriptionTitle = entry.getDescriptionTooltip() || entry.getDescription(); // tooltip over description because it could overflow options.descriptionMatches = descriptionHighlights || []; data.label.setValue(entry.getLabel(), entry.getDescription(), options); // Meta data.detail.set(entry.getDetail(), detailHighlights); // Keybinding data.keybinding.set(entry.getKeybinding(), null); } } disposeTemplate(templateId: string, templateData: IQuickOpenEntryGroupTemplateData): void { const data = templateData as IQuickOpenEntryGroupTemplateData; data.actionBar.dispose(); data.actionBar = null; data.container = null; data.entry = null; data.keybinding.dispose(); data.keybinding = null; data.detail.dispose(); data.detail = null; data.group = null; data.icon = null; data.label.dispose(); data.label = null; } } export class QuickOpenModel implements IModel, IDataSource, IFilter, IRunner, IAccessiblityProvider { private _entries: QuickOpenEntry[]; private _dataSource: IDataSource; private _renderer: IRenderer; private _filter: IFilter; private _runner: IRunner; private _accessibilityProvider: IAccessiblityProvider; constructor(entries: QuickOpenEntry[] = [], actionProvider: IActionProvider = new NoActionProvider()) { this._entries = entries; this._dataSource = this; this._renderer = new Renderer(actionProvider); this._filter = this; this._runner = this; this._accessibilityProvider = this; } get entries() { return this._entries; } get dataSource() { return this._dataSource; } get renderer() { return this._renderer; } get filter() { return this._filter; } get runner() { return this._runner; } get accessibilityProvider() { return this._accessibilityProvider; } set entries(entries: QuickOpenEntry[]) { this._entries = entries; } /** * Adds entries that should show up in the quick open viewer. */ addEntries(entries: QuickOpenEntry[]): void { if (types.isArray(entries)) { this._entries = this._entries.concat(entries); } } /** * Set the entries that should show up in the quick open viewer. */ setEntries(entries: QuickOpenEntry[]): void { if (types.isArray(entries)) { this._entries = entries; } } /** * Get the entries that should show up in the quick open viewer. * * @visibleOnly optional parameter to only return visible entries */ getEntries(visibleOnly?: boolean): QuickOpenEntry[] { if (visibleOnly) { return this._entries.filter((e) => !e.isHidden()); } return this._entries; } getId(entry: QuickOpenEntry): string { return entry.getId(); } getLabel(entry: QuickOpenEntry): string { return entry.getLabel(); } getAriaLabel(entry: QuickOpenEntry): string { const ariaLabel = entry.getAriaLabel(); if (ariaLabel) { return nls.localize('quickOpenAriaLabelEntry', "{0}, picker", entry.getAriaLabel()); } return nls.localize('quickOpenAriaLabel', "picker"); } isVisible(entry: QuickOpenEntry): boolean { return !entry.isHidden(); } run(entry: QuickOpenEntry, mode: Mode, context: IContext): boolean { return entry.run(mode, context); } } /** * A good default sort implementation for quick open entries respecting highlight information * as well as associated resources. */ export function compareEntries(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string): number { // Give matches with label highlights higher priority over // those with only description highlights const labelHighlightsA = elementA.getHighlights()[0] || []; const labelHighlightsB = elementB.getHighlights()[0] || []; if (labelHighlightsA.length && !labelHighlightsB.length) { return -1; } if (!labelHighlightsA.length && labelHighlightsB.length) { return 1; } // Fallback to the full path if labels are identical and we have associated resources let nameA = elementA.getLabel(); let nameB = elementB.getLabel(); if (nameA === nameB) { const resourceA = elementA.getResource(); const resourceB = elementB.getResource(); if (resourceA && resourceB) { nameA = resourceA.fsPath; nameB = resourceB.fsPath; } } return compareAnything(nameA, nameB, lookFor); }