mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-08 09:38:26 -05:00
613 lines
18 KiB
TypeScript
613 lines
18 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import * as nls from 'vs/nls';
|
|
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, IEntryRunContext } from 'vs/base/parts/quickopen/common/quickOpen';
|
|
import { IAction, IActionRunner } from 'vs/base/common/actions';
|
|
import { compareAnything } from 'vs/base/common/comparers';
|
|
import { ActionBar } 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';
|
|
import { coalesce } from 'vs/base/common/arrays';
|
|
|
|
export interface IContext {
|
|
event: any;
|
|
quickNavigateConfiguration: IQuickNavigateConfiguration;
|
|
}
|
|
|
|
export interface IHighlight {
|
|
start: number;
|
|
end: number;
|
|
}
|
|
|
|
let IDS = 0;
|
|
|
|
export class QuickOpenItemAccessorClass implements IItemAccessor<QuickOpenEntry> {
|
|
|
|
getItemLabel(entry: QuickOpenEntry): string | null {
|
|
return entry.getLabel();
|
|
}
|
|
|
|
getItemDescription(entry: QuickOpenEntry): string | null {
|
|
return entry.getDescription();
|
|
}
|
|
|
|
getItemPath(entry: QuickOpenEntry): string | undefined {
|
|
const resource = entry.getResource();
|
|
|
|
return resource ? resource.fsPath : undefined;
|
|
}
|
|
}
|
|
|
|
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 | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The options for the label to use for this entry
|
|
*/
|
|
getLabelOptions(): IIconLabelValueOptions | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The label of the entry to use when a screen reader wants to read about the entry
|
|
*/
|
|
getAriaLabel(): string {
|
|
return coalesce([this.getLabel(), this.getDescription(), this.getDetail()])
|
|
.join(', ');
|
|
}
|
|
|
|
/**
|
|
* Detail information about the entry that is optional and can be shown below the label
|
|
*/
|
|
getDetail(): string | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* The icon of the entry to identify it from others in the list
|
|
*/
|
|
getIcon(): string | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A secondary description that is optional and can be shown right to the label
|
|
*/
|
|
getDescription(): string | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A tooltip to show when hovering over the entry.
|
|
*/
|
|
getTooltip(): string | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A tooltip to show when hovering over the description portion of the entry.
|
|
*/
|
|
getDescriptionTooltip(): string | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* An optional keybinding to show for an entry.
|
|
*/
|
|
getKeybinding(): ResolvedKeybinding | null {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* A resource for this entry. Resource URIs can be used to compare different kinds of entries and group
|
|
* them together.
|
|
*/
|
|
getResource(): URI | null {
|
|
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[] | undefined /* Description */, IHighlight[] | undefined /* 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: IEntryRunContext): 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 | undefined {
|
|
return this.groupLabel;
|
|
}
|
|
|
|
setGroupLabel(groupLabel: string | undefined): 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 | null {
|
|
return this.entry ? this.entry.getLabel() : super.getLabel();
|
|
}
|
|
|
|
getLabelOptions(): IIconLabelValueOptions | null {
|
|
return this.entry ? this.entry.getLabelOptions() : super.getLabelOptions();
|
|
}
|
|
|
|
getAriaLabel(): string {
|
|
return this.entry ? this.entry.getAriaLabel() : super.getAriaLabel();
|
|
}
|
|
|
|
getDetail(): string | null {
|
|
return this.entry ? this.entry.getDetail() : super.getDetail();
|
|
}
|
|
|
|
getResource(): URI | null {
|
|
return this.entry ? this.entry.getResource() : super.getResource();
|
|
}
|
|
|
|
getIcon(): string | null {
|
|
return this.entry ? this.entry.getIcon() : super.getIcon();
|
|
}
|
|
|
|
getDescription(): string | null {
|
|
return this.entry ? this.entry.getDescription() : super.getDescription();
|
|
}
|
|
|
|
getEntry(): QuickOpenEntry | undefined {
|
|
return this.entry;
|
|
}
|
|
|
|
getHighlights(): [IHighlight[], IHighlight[] | undefined, IHighlight[] | undefined] {
|
|
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: IEntryRunContext): 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): IAction[] | null {
|
|
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<QuickOpenEntry> {
|
|
|
|
private actionProvider: IActionProvider;
|
|
private actionRunner?: IActionRunner;
|
|
|
|
constructor(actionProvider: IActionProvider = new NoActionProvider(), actionRunner?: IActionRunner) {
|
|
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', undefined, 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 | undefined;
|
|
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
|
|
|
|
const actions = this.actionProvider.getActions(null, entry);
|
|
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 = <QuickOpenEntryGroup>entry;
|
|
const groupData = data;
|
|
|
|
// Border
|
|
if (group.showBorder()) {
|
|
DOM.addClass(groupData.container, 'results-group-separator');
|
|
if (styles.pickerGroupBorder) {
|
|
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() || '';
|
|
if (groupData.group) {
|
|
groupData.group.textContent = groupLabel;
|
|
if (styles.pickerGroupForeground) {
|
|
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 = types.withNullAsUndefined(entry.getTooltip());
|
|
options.descriptionTitle = entry.getDescriptionTooltip() || types.withNullAsUndefined(entry.getDescription()); // tooltip over description because it could overflow
|
|
options.descriptionMatches = descriptionHighlights || [];
|
|
data.label.setLabel(types.withNullAsUndefined(entry.getLabel()), types.withNullAsUndefined(entry.getDescription()), options);
|
|
|
|
// Meta
|
|
data.detail.set(types.withNullAsUndefined(entry.getDetail()), detailHighlights);
|
|
|
|
// Keybinding
|
|
data.keybinding.set(entry.getKeybinding()!);
|
|
}
|
|
}
|
|
|
|
disposeTemplate(templateId: string, templateData: IQuickOpenEntryGroupTemplateData): void {
|
|
templateData.actionBar.dispose();
|
|
templateData.actionBar = null!;
|
|
templateData.container = null!;
|
|
templateData.entry = null!;
|
|
templateData.keybinding = null!;
|
|
templateData.detail = null!;
|
|
templateData.group = null!;
|
|
templateData.icon = null!;
|
|
templateData.label.dispose();
|
|
templateData.label = null!;
|
|
}
|
|
}
|
|
|
|
export class QuickOpenModel implements
|
|
IModel<QuickOpenEntry>,
|
|
IDataSource<QuickOpenEntry>,
|
|
IFilter<QuickOpenEntry>,
|
|
IRunner<QuickOpenEntry>,
|
|
IAccessiblityProvider<QuickOpenEntry>
|
|
{
|
|
private _entries: QuickOpenEntry[];
|
|
private _dataSource: IDataSource<QuickOpenEntry>;
|
|
private _renderer: IRenderer<QuickOpenEntry>;
|
|
private _filter: IFilter<QuickOpenEntry>;
|
|
private _runner: IRunner<QuickOpenEntry>;
|
|
private _accessibilityProvider: IAccessiblityProvider<QuickOpenEntry>;
|
|
|
|
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 | null {
|
|
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: IEntryRunContext): 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);
|
|
}
|