Skip to content

Commit 8316ba1

Browse files
authored
allow recent commands to be pinned (microsoft#158037)
1 parent c3b17a3 commit 8316ba1

File tree

14 files changed

+150
-34
lines changed

14 files changed

+150
-34
lines changed

src/vs/base/parts/quickinput/browser/quickInputList.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { ltrim } from 'vs/base/common/strings';
2525
import { withNullAsUndefined } from 'vs/base/common/types';
2626
import { IQuickInputOptions } from 'vs/base/parts/quickinput/browser/quickInput';
2727
import { getIconClass } from 'vs/base/parts/quickinput/browser/quickInputUtils';
28-
import { IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
28+
import { QuickPickItem, IQuickPickItem, IQuickPickItemButtonEvent, IQuickPickSeparator } from 'vs/base/parts/quickinput/common/quickInput';
2929
import 'vs/css!./media/quickInput';
3030
import { localize } from 'vs/nls';
3131

@@ -253,7 +253,7 @@ export class QuickInputList {
253253
readonly id: string;
254254
private container: HTMLElement;
255255
private list: List<ListElement>;
256-
private inputElements: Array<IQuickPickItem | IQuickPickSeparator> = [];
256+
private inputElements: Array<QuickPickItem> = [];
257257
private elements: ListElement[] = [];
258258
private elementsToIndexes = new Map<IQuickPickItem, number>();
259259
matchOnDescription = false;
@@ -436,7 +436,7 @@ export class QuickInputList {
436436
}
437437
}
438438

439-
setElements(inputElements: Array<IQuickPickItem | IQuickPickSeparator>): void {
439+
setElements(inputElements: Array<QuickPickItem>): void {
440440
this.elementDisposables = dispose(this.elementDisposables);
441441
const fireButtonTriggered = (event: IQuickPickItemButtonEvent<IQuickPickItem>) => this.fireButtonTriggered(event);
442442
this.inputElements = inputElements;

src/vs/base/parts/quickinput/common/quickInput.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export interface IQuickPickItemHighlights {
1818
detail?: IMatch[];
1919
}
2020

21+
export type QuickPickItem = IQuickPickSeparator | IQuickPickItem;
22+
2123
export interface IQuickPickItem {
2224
type?: 'item';
2325
id?: string;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { Codicon } from 'vs/base/common/codicons';
7+
import { localize } from 'vs/nls';
8+
import { IQuickPick, IQuickPickItem, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
9+
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
10+
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
11+
12+
const pinButtonClass = ThemeIcon.asClassName(Codicon.pin);
13+
const pinnedButtonClass = ThemeIcon.asClassName(Codicon.pinned);
14+
const buttonClasses = [pinButtonClass, pinnedButtonClass];
15+
/**
16+
* Initially, adds pin buttons to all @param quickPick items.
17+
* When pinned, a copy of the item will be moved to the end of the pinned list and any duplicate within the pinned list will
18+
* be removed if @param filterDupliates has been provided. Pin and pinned button events trigger updates to the underlying storage.
19+
* Shows the quickpick once formatted.
20+
*/
21+
export async function showWithPinnedItems(storageService: IStorageService, storageKey: string, quickPick: IQuickPick<IQuickPickItem>, filterDuplicates?: boolean): Promise<void> {
22+
quickPick.onDidTriggerItemButton(async buttonEvent => {
23+
const expectedButton = buttonEvent.button.iconClass && buttonClasses.includes(buttonEvent.button.iconClass);
24+
if (expectedButton) {
25+
quickPick.items = await _formatPinnedItems(storageKey, quickPick, storageService, buttonEvent.item, filterDuplicates);
26+
}
27+
});
28+
quickPick.onDidChangeValue(async value => {
29+
// don't show pinned items in the search results
30+
quickPick.items = value ? quickPick.items.filter(i => i.type !== 'separator' && !i.buttons?.find(b => b.iconClass === pinnedButtonClass)) : quickPick.items;
31+
});
32+
quickPick.items = await _formatPinnedItems(storageKey, quickPick, storageService, undefined, filterDuplicates);
33+
await quickPick.show();
34+
}
35+
36+
function _formatPinnedItems(storageKey: string, quickPick: IQuickPick<IQuickPickItem>, storageService: IStorageService, changedItem?: IQuickPickItem, filterDuplicates?: boolean): QuickPickItem[] {
37+
const formattedItems: QuickPickItem[] = [];
38+
let pinnedItems;
39+
if (changedItem) {
40+
pinnedItems = updatePinnedItems(storageKey, changedItem, storageService);
41+
} else {
42+
pinnedItems = getPinnedItems(storageKey, storageService);
43+
}
44+
if (pinnedItems.length) {
45+
formattedItems.push({ type: 'separator', label: localize("terminal.commands.pinned", 'Pinned') });
46+
}
47+
const pinnedIds = new Set();
48+
for (const itemToFind of pinnedItems) {
49+
const itemToPin = quickPick.items.find(item => itemsMatch(item, itemToFind));
50+
if (itemToPin) {
51+
const pinnedItemId = getItemIdentifier(itemToPin);
52+
const pinnedItem: IQuickPickItem = Object.assign({} as IQuickPickItem, itemToPin);
53+
if (!filterDuplicates || !pinnedIds.has(pinnedItemId)) {
54+
pinnedIds.add(pinnedItemId);
55+
updateButtons(pinnedItem, false);
56+
formattedItems.push(pinnedItem);
57+
}
58+
}
59+
}
60+
61+
for (const item of quickPick.items) {
62+
updateButtons(item, true);
63+
formattedItems.push(item);
64+
}
65+
return formattedItems;
66+
}
67+
68+
function getItemIdentifier(item: QuickPickItem): string {
69+
return item.type === 'separator' ? '' : item.id || `${item.label}${item.description}${item.detail}}`;
70+
}
71+
72+
function updateButtons(item: QuickPickItem, removePin: boolean): void {
73+
if (item.type === 'separator') {
74+
return;
75+
}
76+
// remove button classes before adding the new one
77+
item.buttons = item.buttons ? item.buttons?.filter(button => button.iconClass && !buttonClasses.includes(button.iconClass)) : [];
78+
item.buttons.unshift({
79+
iconClass: removePin ? pinButtonClass : pinnedButtonClass,
80+
tooltip: removePin ? localize('pinCommand', "Pin command") : localize('pinnedCommand', "Pinned command"),
81+
alwaysVisible: false
82+
});
83+
}
84+
85+
function itemsMatch(itemA: QuickPickItem, itemB: QuickPickItem): boolean {
86+
return getItemIdentifier(itemA) === getItemIdentifier(itemB);
87+
}
88+
89+
function updatePinnedItems(storageKey: string, changedItem: IQuickPickItem, storageService: IStorageService): IQuickPickItem[] {
90+
const removePin = changedItem.buttons?.find(b => b.iconClass === pinnedButtonClass);
91+
let items = getPinnedItems(storageKey, storageService);
92+
if (removePin) {
93+
items = items.filter(item => getItemIdentifier(item) !== getItemIdentifier(changedItem));
94+
} else {
95+
items.push(changedItem);
96+
}
97+
storageService.store(storageKey, JSON.stringify(items), StorageScope.WORKSPACE, StorageTarget.USER);
98+
return items;
99+
}
100+
101+
function getPinnedItems(storageKey: string, storageService: IStorageService): IQuickPickItem[] {
102+
const items = storageService.get(storageKey, StorageScope.WORKSPACE);
103+
return items ? JSON.parse(items) : [];
104+
}

src/vs/workbench/browser/actions/layoutActions.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { IsMacNativeContext } from 'vs/platform/contextkey/common/contextkeys';
1616
import { KeybindingsRegistry, KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
1717
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1818
import { IViewDescriptorService, IViewsService, ViewContainerLocation, IViewDescriptor, ViewContainerLocationToString } from 'vs/workbench/common/views';
19-
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
19+
import { QuickPickItem, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
2020
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
2121
import { IPaneCompositePartService } from 'vs/workbench/services/panecomposite/browser/panecomposite';
2222
import { ToggleAuxiliaryBarAction } from 'vs/workbench/browser/parts/auxiliarybar/auxiliaryBarActions';
@@ -665,8 +665,8 @@ registerAction2(class extends Action2 {
665665
} catch { }
666666
}
667667

668-
private getViewItems(viewDescriptorService: IViewDescriptorService, paneCompositePartService: IPaneCompositePartService): Array<IQuickPickItem | IQuickPickSeparator> {
669-
const results: Array<IQuickPickItem | IQuickPickSeparator> = [];
668+
private getViewItems(viewDescriptorService: IViewDescriptorService, paneCompositePartService: IPaneCompositePartService): Array<QuickPickItem> {
669+
const results: Array<QuickPickItem> = [];
670670

671671
const viewlets = paneCompositePartService.getVisiblePaneCompositeIds(ViewContainerLocation.Sidebar);
672672
viewlets.forEach(viewletId => {
@@ -1209,7 +1209,7 @@ registerAction2(class CustomizeLayoutAction extends Action2 {
12091209
});
12101210
}
12111211

1212-
getItems(contextKeyService: IContextKeyService): (IQuickPickItem | IQuickPickSeparator)[] {
1212+
getItems(contextKeyService: IContextKeyService): QuickPickItem[] {
12131213
const toQuickPickItem = (item: CustomizeLayoutItem): IQuickPickItem => {
12141214
const toggled = item.active.evaluate(contextKeyService.getContext(null));
12151215
let label = item.useButtons ?

src/vs/workbench/contrib/extensions/browser/extensionsActions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import { PICK_WORKSPACE_FOLDER_COMMAND_ID } from 'vs/workbench/browser/actions/w
3838
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
3939
import { IOpenerService } from 'vs/platform/opener/common/opener';
4040
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
41-
import { IQuickPickItem, IQuickInputService, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
41+
import { IQuickPickItem, IQuickInputService, IQuickPickSeparator, QuickPickItem } from 'vs/platform/quickinput/common/quickInput';
4242
import { CancellationToken } from 'vs/base/common/cancellation';
4343
import { alert } from 'vs/base/browser/ui/aria/aria';
4444
import { IWorkbenchThemeService, IWorkbenchTheme, IWorkbenchColorTheme, IWorkbenchFileIconTheme, IWorkbenchProductIconTheme } from 'vs/workbench/services/themes/common/workbenchThemeService';
@@ -1645,8 +1645,8 @@ function isThemeFromExtension(theme: IWorkbenchTheme, extension: IExtension | un
16451645
return !!(extension && theme.extensionData && ExtensionIdentifier.equals(theme.extensionData.extensionId, extension.identifier.id));
16461646
}
16471647

1648-
function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme, extension: IExtension | null | undefined, showCurrentTheme: boolean): (IQuickPickItem | IQuickPickSeparator)[] {
1649-
const picks: (IQuickPickItem | IQuickPickSeparator)[] = [];
1648+
function getQuickPickEntries(themes: IWorkbenchTheme[], currentTheme: IWorkbenchTheme, extension: IExtension | null | undefined, showCurrentTheme: boolean): QuickPickItem[] {
1649+
const picks: QuickPickItem[] = [];
16501650
for (const theme of themes) {
16511651
if (isThemeFromExtension(theme, extension) && !(showCurrentTheme && theme === currentTheme)) {
16521652
picks.push({ label: theme.label, id: theme.id });

src/vs/workbench/contrib/remote/browser/remoteIndicator.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/c
1616
import { ICommandService } from 'vs/platform/commands/common/commands';
1717
import { Schemas } from 'vs/base/common/network';
1818
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
19-
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
19+
import { QuickPickItem, IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
2020
import { IBrowserWorkbenchEnvironmentService } from 'vs/workbench/services/environment/browser/environmentService';
2121
import { PersistentConnectionEventType } from 'vs/platform/remote/common/remoteAgentConnection';
2222
import { IRemoteAuthorityResolverService } from 'vs/platform/remote/common/remoteAuthorityResolver';
@@ -386,7 +386,7 @@ export class RemoteStatusIndicator extends Disposable implements IWorkbenchContr
386386
const computeItems = () => {
387387
let actionGroups = this.getRemoteMenuActions(true);
388388

389-
const items: (IQuickPickItem | IQuickPickSeparator)[] = [];
389+
const items: QuickPickItem[] = [];
390390

391391
const currentRemoteMatcher = matchCurrentRemote();
392392
if (currentRemoteMatcher) {

src/vs/workbench/contrib/terminal/browser/links/terminalLinkQuickpick.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { EventType } from 'vs/base/browser/dom';
77
import { Emitter } from 'vs/base/common/event';
88
import { localize } from 'vs/nls';
9-
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
9+
import { QuickPickItem, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
1010
import { IDetectedLinks } from 'vs/workbench/contrib/terminal/browser/links/terminalLinkManager';
1111
import { TerminalLinkQuickPickEvent } from 'vs/workbench/contrib/terminal/browser/terminal';
1212
import { ILink } from 'xterm';
@@ -80,4 +80,4 @@ export interface ITerminalLinkQuickPickItem extends IQuickPickItem {
8080
link: ILink;
8181
}
8282

83-
type LinkQuickPickItem = ITerminalLinkQuickPickItem | IQuickPickSeparator | IQuickPickItem;
83+
type LinkQuickPickItem = ITerminalLinkQuickPickItem | QuickPickItem;

src/vs/workbench/contrib/terminal/browser/terminalInstance.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
4141
import { ILogService } from 'vs/platform/log/common/log';
4242
import { INotificationService, IPromptChoice, Severity } from 'vs/platform/notification/common/notification';
4343
import { IProductService } from 'vs/platform/product/common/productService';
44-
import { IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
44+
import { QuickPickItem, IQuickInputButton, IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
4545
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
4646
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
4747
import { ITerminalCommand, TerminalCapability } from 'vs/platform/terminal/common/capabilities/capabilities';
@@ -88,6 +88,8 @@ import { IGenericMarkProperties } from 'vs/platform/terminal/common/terminalProc
8888
import { ICommandService } from 'vs/platform/commands/common/commands';
8989
import { getIconRegistry } from 'vs/platform/theme/common/iconRegistry';
9090
import { TaskSettingId } from 'vs/workbench/contrib/tasks/common/tasks';
91+
import { TerminalStorageKeys } from 'vs/workbench/contrib/terminal/common/terminalStorageKeys';
92+
import { showWithPinnedItems } from 'vs/platform/quickinput/browser/quickPickPin';
9193

9294
const enum Constants {
9395
/**
@@ -849,6 +851,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
849851
if (!this.xterm) {
850852
return;
851853
}
854+
const runRecentStorageKey = `${TerminalStorageKeys.PinnedRecentCommandsPrefix}.${this._shellType}`;
852855
let placeholder: string;
853856
type Item = IQuickPickItem & { command?: ITerminalCommand; rawLabel: string };
854857
let items: (Item | IQuickPickItem & { rawLabel: string } | IQuickPickSeparator)[] = [];
@@ -859,6 +862,12 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
859862
tooltip: nls.localize('removeCommand', "Remove from Command History")
860863
};
861864

865+
const commandOutputButton: IQuickInputButton = {
866+
iconClass: ThemeIcon.asClassName(Codicon.output),
867+
tooltip: nls.localize('viewCommandOutput', "View Command Output"),
868+
alwaysVisible: false
869+
};
870+
862871
if (type === 'command') {
863872
placeholder = isMacintosh ? nls.localize('selectRecentCommandMac', 'Select a command to run (hold Option-key to edit the command)') : nls.localize('selectRecentCommand', 'Select a command to run (hold Alt-key to edit the command)');
864873
const cmdDetection = this.capabilities.get(TerminalCapability.CommandDetection);
@@ -895,12 +904,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
895904
}
896905
}
897906
description = description.trim();
898-
const iconClass = ThemeIcon.asClassName(Codicon.output);
899-
const buttons: IQuickInputButton[] = [{
900-
iconClass,
901-
tooltip: nls.localize('viewCommandOutput', "View Command Output"),
902-
alwaysVisible: false
903-
}];
907+
const buttons: IQuickInputButton[] = [commandOutputButton];
904908
// Merge consecutive commands
905909
const lastItem = items.length > 0 ? items[items.length - 1] : undefined;
906910
if (lastItem?.type !== 'separator' && lastItem?.label === label) {
@@ -1034,7 +1038,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
10341038
} else {
10351039
this._instantiationService.invokeFunction(getDirectoryHistory)?.remove(e.item.label);
10361040
}
1037-
} else {
1041+
} else if (e.button === commandOutputButton) {
10381042
const selectedCommand = (e.item as Item).command;
10391043
const output = selectedCommand?.getOutput();
10401044
if (output && selectedCommand?.command) {
@@ -1052,7 +1056,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
10521056
}
10531057
}
10541058
}
1055-
quickPick.hide();
1059+
await this.runRecent(type, filterMode, value);
1060+
}
1061+
);
1062+
quickPick.onDidChangeValue(async value => {
1063+
if (!value) {
1064+
await this.runRecent(type, filterMode, value);
1065+
}
10561066
});
10571067
quickPick.onDidAccept(async () => {
10581068
const result = quickPick.activeItems[0];
@@ -1069,8 +1079,8 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
10691079
quickPick.value = value;
10701080
}
10711081
return new Promise<void>(r => {
1072-
quickPick.show();
10731082
this._terminalInRunCommandPicker.set(true);
1083+
showWithPinnedItems(this._storageService, runRecentStorageKey, quickPick, true);
10741084
quickPick.onDidHide(() => {
10751085
this._terminalInRunCommandPicker.set(false);
10761086
r();
@@ -2502,7 +2512,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
25022512
const colorTheme = this._themeService.getColorTheme();
25032513
const standardColors: string[] = getStandardColors(colorTheme);
25042514
const styleElement = getColorStyleElement(colorTheme);
2505-
const items: (IQuickPickItem | IQuickPickSeparator)[] = [];
2515+
const items: QuickPickItem[] = [];
25062516
for (const colorKey of standardColors) {
25072517
const colorClass = getColorClass(colorKey);
25082518
items.push({

src/vs/workbench/contrib/terminal/common/terminalStorageKeys.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@ export const enum TerminalStorageKeys {
1010
TabsListWidthVertical = 'tabs-list-width-vertical',
1111
EnvironmentVariableCollections = 'terminal.integrated.environmentVariableCollections',
1212
TerminalBufferState = 'terminal.integrated.bufferState',
13-
TerminalLayoutInfo = 'terminal.integrated.layoutInfo'
13+
TerminalLayoutInfo = 'terminal.integrated.layoutInfo',
14+
PinnedRecentCommandsPrefix = 'terminal.pinnedRecentCommands'
1415
}

src/vs/workbench/contrib/userDataProfile/browser/userDataProfileActions.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { IDialogService, IFileDialogService } from 'vs/platform/dialogs/common/d
1212
import { IFileService } from 'vs/platform/files/common/files';
1313
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1414
import { INotificationService } from 'vs/platform/notification/common/notification';
15-
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
15+
import { QuickPickItem, IQuickInputService, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
1616
import { asJson, asText, IRequestService } from 'vs/platform/request/common/request';
1717
import { IUserDataProfileTemplate, isUserDataProfileTemplate, IUserDataProfileManagementService, IUserDataProfileImportExportService, PROFILES_CATEGORY, PROFILE_EXTENSION, PROFILE_FILTER, ManageProfilesSubMenu, IUserDataProfileService, PROFILES_ENABLEMENT_CONTEXT, HAS_PROFILES_CONTEXT } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
1818
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
@@ -290,7 +290,7 @@ export class MangeSettingsProfileAction extends Action2 {
290290
menu.dispose();
291291

292292
if (actions.length) {
293-
const picks: (IQuickPickItem | IQuickPickSeparator)[] = actions.map(action => {
293+
const picks: QuickPickItem[] = actions.map(action => {
294294
if (action instanceof Separator) {
295295
return { type: 'separator' };
296296
}

0 commit comments

Comments
 (0)