@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
378 lines (341 loc) • 13.3 kB
text/typescript
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import URI from './uri';
import * as fuzzy from 'fuzzy';
import { Event } from './event';
import { KeySequence } from './keys';
import { CancellationToken } from './cancellation';
import { URI as Uri } from 'vscode-uri';
export const quickPickServicePath = '/services/quickPick';
export const QuickPickService = Symbol('QuickPickService');
export interface QuickPickService {
show<T extends QuickPickItem>(items: Array<T | QuickPickSeparator>, options?: QuickPickOptions<T>): Promise<T | undefined>;
setItems<T extends QuickPickItem>(items: Array<T>): void;
hide(): void
readonly onDidHide: Event<void>;
readonly onDidAccept: Event<void>;
readonly onDidChangeValue: Event<{ quickPick: QuickPick<QuickPickItem>, filter: string }>;
readonly onDidChangeActive: Event<{ quickPick: QuickPick<QuickPickItem>, activeItems: Array<QuickPickItem> }>;
readonly onDidChangeSelection: Event<{ quickPick: QuickPick<QuickPickItem>, selectedItems: Array<QuickPickItem> }>;
readonly onDidTriggerButton: Event<QuickInputButtonHandle>;
}
export interface Match {
start: number;
end: number;
}
export interface QuickPickItemHighlights {
label?: Match[];
description?: Match[];
detail?: Match[];
}
export interface QuickPickItem {
type?: 'item';
id?: string;
label: string;
meta?: string;
ariaLabel?: string;
description?: string;
detail?: string;
keySequence?: KeySequence;
iconPath?: URI | Uri | { light?: URI | Uri; dark: URI | Uri } | { id: string };
iconClasses?: string[];
alwaysShow?: boolean;
highlights?: QuickPickItemHighlights;
buttons?: readonly QuickInputButton[];
execute?: () => void;
}
export interface QuickPickSeparator {
type: 'separator';
label?: string;
}
export type QuickPickItemOrSeparator = QuickPickItem | QuickPickSeparator;
export type QuickPickInput<T = QuickPickItem> = T | QuickPickSeparator;
export namespace QuickPickItem {
export function is(item: QuickPickSeparator | QuickPickItem): item is QuickPickItem {
// if it's not a separator, it's an item
return item.type !== 'separator';
}
}
export interface QuickPickSeparator {
type: 'separator';
label?: string;
}
export namespace QuickPickSeparator {
export function is(item: QuickPickItemOrSeparator): item is QuickPickSeparator {
return item.type === 'separator';
}
}
export type QuickPicks = (QuickPickSeparator | QuickPickItem)[];
export interface QuickPickValue<V> extends QuickPickItem {
value: V
}
export interface QuickInputButton {
iconPath?: URI | Uri | { light?: URI | Uri; dark: URI | Uri } | { id: string };
iconClass?: string;
tooltip?: string;
/**
* Whether the button should be visible even when the user is not hovering.
*/
alwaysVisible?: boolean;
}
export interface NormalizedQuickInputButton extends QuickInputButton {
iconPath?: { light?: Uri, dark: Uri };
}
export namespace QuickInputButton {
export function normalize(button: undefined): undefined;
export function normalize(button: QuickInputButton): NormalizedQuickInputButton;
export function normalize(button?: QuickInputButton): NormalizedQuickInputButton | undefined {
if (!button) {
return button;
}
let iconPath: NormalizedQuickInputButton['iconPath'] = undefined;
if (button.iconPath instanceof URI) {
iconPath = { dark: button.iconPath['codeUri'] };
} else if (button.iconPath && 'dark' in button.iconPath) {
const dark = Uri.isUri(button.iconPath.dark) ? button.iconPath.dark : button.iconPath.dark['codeUri'];
const light = Uri.isUri(button.iconPath.light) ? button.iconPath.light : button.iconPath.light?.['codeUri'];
iconPath = { dark, light };
}
return {
...button,
iconPath,
};
}
}
export interface QuickInputButtonHandle extends QuickInputButton {
index: number; // index of where they are in buttons array if QuickInputButton or -1 if QuickInputButtons.Back
}
export enum QuickInputHideReason {
/**
* Focus was moved away from the input, but the user may not have explicitly closed it.
*/
Blur = 1,
/**
* An explicit close gesture, like striking the Escape key
*/
Gesture = 2,
/**
* Any other reason
*/
Other = 3,
}
export interface QuickInput {
readonly onDidHide: Event<{ reason: QuickInputHideReason }>;
readonly onDispose: Event<void>;
title: string | undefined;
description: string | undefined;
step: number | undefined;
totalSteps: number | undefined;
enabled: boolean;
contextKey: string | undefined;
busy: boolean;
ignoreFocusOut: boolean;
show(): void;
hide(): void;
dispose(): void;
}
export interface InputBox extends QuickInput {
value: string | undefined;
valueSelection: Readonly<[number, number]> | undefined;
placeholder: string | undefined;
password: boolean;
readonly onDidChangeValue: Event<string>;
readonly onDidAccept: Event<void>;
buttons: ReadonlyArray<QuickInputButton>;
readonly onDidTriggerButton: Event<QuickInputButton>;
prompt: string | undefined;
validationMessage: string | undefined;
}
export interface QuickPick<T extends QuickPickItemOrSeparator> extends QuickInput {
value: string;
placeholder: string | undefined;
items: ReadonlyArray<T | QuickPickSeparator>;
activeItems: ReadonlyArray<T>;
selectedItems: ReadonlyArray<T>;
canSelectMany: boolean;
matchOnDescription: boolean;
matchOnDetail: boolean;
keepScrollPosition: boolean;
readonly onDidAccept: Event<{ inBackground: boolean } | undefined>;
readonly onDidChangeValue: Event<string>;
readonly onDidTriggerButton: Event<QuickInputButton>;
readonly onDidTriggerItemButton: Event<QuickPickItemButtonEvent<T>>;
readonly onDidChangeActive: Event<T[]>;
readonly onDidChangeSelection: Event<T[]>;
}
export interface PickOptions<T extends QuickPickItem> {
title?: string;
placeHolder?: string;
matchOnDescription?: boolean;
matchOnDetail?: boolean;
matchOnLabel?: boolean;
autoFocusOnList?: boolean;
ignoreFocusLost?: boolean;
canPickMany?: boolean;
contextKey?: string;
activeItem?: Promise<T> | T;
onDidFocus?: (entry: T) => void;
}
export interface InputOptions {
title?: string;
value?: string;
valueSelection?: [number, number];
prompt?: string;
placeHolder?: string;
password?: boolean;
ignoreFocusLost?: boolean;
validateInput?(input: string): Promise<string | { content: string; severity: number; } | null | undefined> | undefined;
}
export interface QuickPickItemButtonEvent<T extends QuickPickItemOrSeparator> {
button: QuickInputButton;
item: T;
}
export interface QuickPickItemButtonContext<T extends QuickPickItemOrSeparator> extends QuickPickItemButtonEvent<T> {
removeItem(): void;
}
export interface QuickPickOptions<T extends QuickPickItemOrSeparator> {
busy?: boolean;
enabled?: boolean;
title?: string;
description?: string;
value?: string;
filterValue?: (value: string) => string;
ariaLabel?: string;
buttons?: Array<QuickInputButton>;
placeholder?: string;
canAcceptInBackground?: boolean;
customButton?: boolean;
customLabel?: string;
customHover?: string;
canSelectMany?: boolean;
matchOnDescription?: boolean;
matchOnDetail?: boolean;
matchOnLabel?: boolean;
sortByLabel?: boolean;
keepScrollPosition?: boolean;
autoFocusOnList?: boolean;
ignoreFocusOut?: boolean;
valueSelection?: Readonly<[number, number]>;
validationMessage?: string;
hideInput?: boolean;
hideCheckAll?: boolean;
runIfSingle?: boolean
contextKey?: string;
activeItem?: T,
step?: number;
totalSteps?: number;
onDidAccept?: () => void,
onDidChangeActive?: (quickPick: QuickPick<T>, activeItems: Array<T>) => void,
onDidChangeSelection?: (quickPick: QuickPick<T>, selectedItems: Array<T>) => void,
onDidChangeValue?: (quickPick: QuickPick<T>, filter: string) => void,
onDidCustom?: () => void,
onDidHide?: () => void;
onDidTriggerButton?: (button: QuickInputButton) => void,
onDidTriggerItemButton?: (ItemButtonEvent: QuickPickItemButtonContext<T>) => void
}
export const quickInputServicePath = '/services/quickInput';
export const QuickInputService = Symbol('QuickInputService');
export interface QuickInputService {
readonly backButton: QuickInputButton;
readonly onShow: Event<void>;
readonly onHide: Event<void>;
open(filter: string): void;
createInputBox(): InputBox;
input(options?: InputOptions, token?: CancellationToken): Promise<string | undefined>;
pick<T extends QuickPickItem, O extends PickOptions<T>>(picks: Promise<QuickPickInput<T>[]> | QuickPickInput<T>[], options?: O, token?: CancellationToken):
Promise<(O extends { canPickMany: true } ? T[] : T) | undefined>;
showQuickPick<T extends QuickPickItem>(items: Array<T | QuickPickSeparator>, options?: QuickPickOptions<T>): Promise<T | undefined>;
hide(): void;
/**
* Provides raw access to the quick pick controller.
*/
createQuickPick<T extends QuickPickItem>(): QuickPick<T>;
}
/**
* Filter the list of quick pick items based on the provided filter.
* Items are filtered based on if:
* - their `label` satisfies the filter using `fuzzy`.
* - their `description` satisfies the filter using `fuzzy`.
* - their `detail` satisfies the filter using `fuzzy`.
* Filtered items are also updated to display proper highlights based on how they were filtered.
* @param items the list of quick pick items.
* @param filter the filter to search for.
* @returns the list of quick pick items that satisfy the filter.
*/
export function filterItems(items: QuickPickItemOrSeparator[], filter: string): QuickPickItemOrSeparator[] {
filter = filter.trim().toLowerCase();
if (filter.length === 0) {
for (const item of items) {
if (item.type !== 'separator') {
item.highlights = undefined; // reset highlights from previous filtering.
}
}
return items;
}
const filteredItems: QuickPickItemOrSeparator[] = [];
for (const item of items) {
if (item.type === 'separator') {
filteredItems.push(item);
} else if (
fuzzy.test(filter, item.label) ||
(item.description && fuzzy.test(filter, item.description)) ||
(item.detail && fuzzy.test(filter, item.detail))
) {
item.highlights = {
label: findMatches(item.label, filter),
description: item.description ? findMatches(item.description, filter) : undefined,
detail: item.detail ? findMatches(item.detail, filter) : undefined
};
filteredItems.push(item);
}
}
return filteredItems;
}
/**
* Find match highlights when testing a word against a pattern.
* @param word the word to test.
* @param pattern the word to match against.
* @returns the list of highlights if present.
*/
export function findMatches(word: string, pattern: string): Array<{ start: number, end: number }> | undefined {
word = word.toLocaleLowerCase();
pattern = pattern.toLocaleLowerCase();
if (pattern.trim().length === 0) {
return undefined;
}
const delimiter = '\u0000'; // null byte that shouldn't appear in the input and is used to denote matches.
const matchResult = fuzzy.match(pattern.replace(/\u0000/gu, ''), word, { pre: delimiter, post: delimiter });
if (!matchResult) {
return undefined;
}
const match = matchResult.rendered;
const highlights: { start: number, end: number }[] = [];
let lastIndex = 0;
/** We need to account for the extra markers by removing them from the range */
let offset = 0;
while (true) {
const start = match.indexOf(delimiter, lastIndex);
if (start === -1) { break; }
const end = match.indexOf(delimiter, start + 1);
if (end === -1) { break; }
highlights.push({
start: start - offset++,
end: end - offset++
});
lastIndex = end + 1;
}
return highlights.length > 0 ? highlights : undefined;
}