Skip to content

Commit f44fbc5

Browse files
author
Benjamin Pasero
committed
quick access - enable support to open in background
Via arrow right or mouse middle button.
1 parent b792db2 commit f44fbc5

9 files changed

Lines changed: 117 additions & 32 deletions

File tree

src/vs/base/browser/ui/inputbox/inputBox.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,10 @@ export class InputBox extends Widget {
295295
}
296296
}
297297

298+
public isSelectionAtEnd(): boolean {
299+
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
300+
}
301+
298302
public enable(): void {
299303
this.input.removeAttribute('disabled');
300304
}

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import 'vs/css!./media/quickInput';
7-
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
7+
import { IQuickPickItem, IPickOptions, IInputOptions, IQuickNavigateConfiguration, IQuickPick, IQuickInput, IQuickInputButton, IInputBox, IQuickPickItemButtonEvent, QuickPickInput, IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
88
import * as dom from 'vs/base/browser/dom';
99
import { CancellationToken } from 'vs/base/common/cancellation';
1010
import { QuickInputList } from './quickInputList';
@@ -379,11 +379,12 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
379379
private _ariaLabel = QuickPick.DEFAULT_ARIA_LABEL;
380380
private _placeholder: string | undefined;
381381
private readonly onDidChangeValueEmitter = this._register(new Emitter<string>());
382-
private readonly onDidAcceptEmitter = this._register(new Emitter<void>());
382+
private readonly onDidAcceptEmitter = this._register(new Emitter<IQuickPickAcceptEvent>());
383383
private readonly onDidCustomEmitter = this._register(new Emitter<void>());
384384
private _items: Array<T | IQuickPickSeparator> = [];
385385
private itemsUpdated = false;
386386
private _canSelectMany = false;
387+
private _canAcceptInBackground = false;
387388
private _matchOnDescription = false;
388389
private _matchOnDetail = false;
389390
private _matchOnLabel = true;
@@ -462,6 +463,14 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
462463
this.update();
463464
}
464465

466+
get canAcceptInBackground() {
467+
return this._canAcceptInBackground;
468+
}
469+
470+
set canAcceptInBackground(canAcceptInBackground: boolean) {
471+
this._canAcceptInBackground = canAcceptInBackground;
472+
}
473+
465474
get matchOnDescription() {
466475
return this._matchOnDescription;
467476
}
@@ -663,6 +672,22 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
663672
this.ui.list.domFocus();
664673
}
665674
event.preventDefault();
675+
break;
676+
case KeyCode.RightArrow:
677+
if (!this._canAcceptInBackground) {
678+
return; // needs to be enabled
679+
}
680+
681+
if (!this.ui.inputBox.isSelectionAtEnd()) {
682+
return; // ensure input box selection at end
683+
}
684+
685+
if (this.activeItems[0]) {
686+
this._selectedItems = [this.activeItems[0]];
687+
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
688+
this.onDidAcceptEmitter.fire({ inBackground: true });
689+
}
690+
666691
break;
667692
}
668693
}));
@@ -671,7 +696,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
671696
this._selectedItems = [this.activeItems[0]];
672697
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
673698
}
674-
this.onDidAcceptEmitter.fire(undefined);
699+
this.onDidAcceptEmitter.fire({ inBackground: false });
675700
}));
676701
this.visibleDisposables.add(this.ui.onDidCustom(() => {
677702
this.onDidCustomEmitter.fire(undefined);
@@ -686,7 +711,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
686711
this._activeItems = focusedItems as T[];
687712
this.onDidChangeActiveEmitter.fire(focusedItems as T[]);
688713
}));
689-
this.visibleDisposables.add(this.ui.list.onDidChangeSelection(selectedItems => {
714+
this.visibleDisposables.add(this.ui.list.onDidChangeSelection(({ items: selectedItems, event }) => {
690715
if (this.canSelectMany) {
691716
if (selectedItems.length) {
692717
this.ui.list.setSelectedElements([]);
@@ -699,7 +724,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
699724
this._selectedItems = selectedItems as T[];
700725
this.onDidChangeSelectionEmitter.fire(selectedItems as T[]);
701726
if (selectedItems.length) {
702-
this.onDidAcceptEmitter.fire(undefined);
727+
this.onDidAcceptEmitter.fire({ inBackground: (event as MouseEvent).button === 1 /* mouse middle click */ });
703728
}
704729
}));
705730
this.visibleDisposables.add(this.ui.list.onChangedCheckedElements(checkedItems => {
@@ -762,7 +787,7 @@ class QuickPick<T extends IQuickPickItem> extends QuickInput implements IQuickPi
762787
if (wasTriggerKeyPressed && this.activeItems[0]) {
763788
this._selectedItems = [this.activeItems[0]];
764789
this.onDidChangeSelectionEmitter.fire(this.selectedItems);
765-
this.onDidAcceptEmitter.fire(undefined);
790+
this.onDidAcceptEmitter.fire({ inBackground: false });
766791
}
767792
});
768793
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ export class QuickInputBox extends Disposable {
5454
this.inputBox.select(range);
5555
}
5656

57-
setPlaceholder(placeholder: string) {
57+
isSelectionAtEnd(): boolean {
58+
return this.inputBox.isSelectionAtEnd();
59+
}
60+
61+
setPlaceholder(placeholder: string): void {
5862
this.inputBox.setPlaceHolder(placeholder);
5963
}
6064

@@ -90,11 +94,11 @@ export class QuickInputBox extends Disposable {
9094
return this.inputBox.hasFocus();
9195
}
9296

93-
setAttribute(name: string, value: string) {
97+
setAttribute(name: string, value: string): void {
9498
this.inputBox.inputElement.setAttribute(name, value);
9599
}
96100

97-
removeAttribute(name: string) {
101+
removeAttribute(name: string): void {
98102
this.inputBox.inputElement.removeAttribute(name);
99103
}
100104

@@ -118,7 +122,7 @@ export class QuickInputBox extends Disposable {
118122
this.inputBox.layout();
119123
}
120124

121-
style(styles: IInputBoxStyles) {
125+
style(styles: IInputBoxStyles): void {
122126
this.inputBox.style(styles);
123127
}
124128
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ export class QuickInputList {
318318

319319
@memoize
320320
get onDidChangeSelection() {
321-
return Event.map(this.list.onDidChangeSelection, e => e.elements.map(e => e.item));
321+
return Event.map(this.list.onDidChangeSelection, e => ({ items: e.elements.map(e => e.item), event: e.browserEvent }));
322322
}
323323

324324
getAllVisibleChecked() {

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ export interface IQuickPickItem {
2424
ariaLabel?: string;
2525
description?: string;
2626
detail?: string;
27+
/**
28+
* Allows to show a keybinding next to the item to indicate
29+
* how the item can be triggered outside of the picker using
30+
* keyboard shortcut.
31+
*/
2732
keybinding?: ResolvedKeybinding;
2833
iconClasses?: string[];
2934
italic?: boolean;
@@ -171,6 +176,15 @@ export interface IQuickInput extends IDisposable {
171176
hide(): void;
172177
}
173178

179+
export interface IQuickPickAcceptEvent {
180+
181+
/**
182+
* Signals if the picker item is to be accepted
183+
* in the background while keeping the picker open.
184+
*/
185+
inBackground: boolean;
186+
}
187+
174188
export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
175189

176190
value: string;
@@ -187,7 +201,14 @@ export interface IQuickPick<T extends IQuickPickItem> extends IQuickInput {
187201

188202
readonly onDidChangeValue: Event<string>;
189203

190-
readonly onDidAccept: Event<void>;
204+
readonly onDidAccept: Event<IQuickPickAcceptEvent>;
205+
206+
/**
207+
* If enabled, will fire the `onDidAccept` event when
208+
* pressing the arrow-right key with the idea of accepting
209+
* the selected item without closing the picker.
210+
*/
211+
canAcceptInBackground: boolean;
191212

192213
ok: boolean | 'default';
193214

@@ -269,7 +290,6 @@ export interface IQuickInputButton {
269290
/** iconPath or iconClass required */
270291
iconClass?: string;
271292
tooltip?: string;
272-
alwaysShow?: boolean;
273293
}
274294

275295
export interface IQuickPickItemButtonEvent<T extends IQuickPickItem> {

src/vs/platform/quickinput/common/quickAccess.ts

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { first } from 'vs/base/common/arrays';
1010
import { startsWith } from 'vs/base/common/strings';
1111
import { assertIsDefined } from 'vs/base/common/types';
1212
import { IDisposable, toDisposable, DisposableStore } from 'vs/base/common/lifecycle';
13-
import { IQuickPickSeparator, IKeyMods } from 'vs/base/parts/quickinput/common/quickInput';
13+
import { IQuickPickSeparator, IKeyMods, IQuickPickAcceptEvent } from 'vs/base/parts/quickinput/common/quickInput';
1414

1515
export interface IQuickAccessController {
1616

@@ -169,8 +169,9 @@ export interface IPickerQuickAccessItem extends IQuickPickItem {
169169
* the picker. The picker will close automatically before running this.
170170
*
171171
* @param keyMods the state of modifier keys when the item was accepted.
172+
* @param event the underlying event that caused the accept to trigger.
172173
*/
173-
accept?(keyMods: IKeyMods): void;
174+
accept?(keyMods: IKeyMods, event: IQuickPickAcceptEvent): void;
174175

175176
/**
176177
* A method that will be executed when a button of the pick item was
@@ -194,6 +195,9 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
194195
provide(picker: IQuickPick<T>, token: CancellationToken): IDisposable {
195196
const disposables = new DisposableStore();
196197

198+
// Allow subclasses to configure picker
199+
this.configure(picker);
200+
197201
// Disable filtering & sorting, we control the results
198202
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
199203

@@ -232,11 +236,13 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
232236
updatePickerItems();
233237

234238
// Accept the pick on accept and hide picker
235-
disposables.add(picker.onDidAccept(() => {
239+
disposables.add(picker.onDidAccept(event => {
236240
const [item] = picker.selectedItems;
237241
if (typeof item?.accept === 'function') {
238-
picker.hide();
239-
item.accept(picker.keyMods);
242+
if (!event.inBackground) {
243+
picker.hide(); // hide picker unless we accept in background
244+
}
245+
item.accept(picker.keyMods, event);
240246
}
241247
}));
242248

@@ -269,6 +275,13 @@ export abstract class PickerQuickAccessProvider<T extends IPickerQuickAccessItem
269275
return disposables;
270276
}
271277

278+
/**
279+
* Subclasses can override this method to configure the picker before showing it.
280+
*
281+
* @param picker the picker instance used for the quick access before it opens.
282+
*/
283+
protected configure(picker: IQuickPick<T>): void { }
284+
272285
/**
273286
* Returns an array of picks and separators as needed. If the picks are resolved
274287
* long running, the provided cancellation token should be used to cancel the

src/vs/workbench/browser/parts/editor/editorQuickAccess.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { localize } from 'vs/nls';
7-
import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource } from 'vs/platform/quickinput/common/quickInput';
7+
import { IQuickPickSeparator, quickPickItemScorerAccessor, IQuickPickItemWithResource, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
88
import { PickerQuickAccessProvider, IPickerQuickAccessItem, TriggerAction } from 'vs/platform/quickinput/common/quickAccess';
99
import { IEditorGroupsService, GroupsOrder } from 'vs/workbench/services/editor/common/editorGroupsService';
1010
import { EditorsOrder, IEditorIdentifier, toResource, SideBySideEditor } from 'vs/workbench/common/editor';
@@ -28,6 +28,12 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
2828
super(prefix);
2929
}
3030

31+
protected configure(picker: IQuickPick<IEditorQuickPickItem>): void {
32+
33+
// Allow to open editors in background without closing picker
34+
picker.canAcceptInBackground = true;
35+
}
36+
3137
protected getPicks(filter: string): Array<IEditorQuickPickItem | IQuickPickSeparator> {
3238
const query = prepareQuery(filter);
3339
const scorerCache = Object.create(null);
@@ -108,7 +114,7 @@ export abstract class BaseEditorQuickAccessProvider extends PickerQuickAccessPro
108114

109115
return TriggerAction.REFRESH_PICKER;
110116
},
111-
accept: () => this.editorGroupService.getGroup(groupId)?.openEditor(editor),
117+
accept: (keyMods, event) => this.editorGroupService.getGroup(groupId)?.openEditor(editor, { preserveFocus: event.inBackground }),
112118
};
113119
});
114120
}

src/vs/workbench/contrib/search/browser/symbolsQuickAccess.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { IEditorService, SIDE_GROUP, ACTIVE_GROUP } from 'vs/workbench/services/
1919
import { Range } from 'vs/editor/common/core/range';
2020
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
2121
import { IWorkbenchEditorConfiguration } from 'vs/workbench/common/editor';
22-
import { IKeyMods } from 'vs/platform/quickinput/common/quickInput';
22+
import { IKeyMods, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
2323

2424
interface ISymbolsQuickPickItem extends IPickerQuickAccessItem {
2525
score: FuzzyScore;
@@ -43,6 +43,12 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
4343
super(SymbolsQuickAccessProvider.PREFIX);
4444
}
4545

46+
protected configure(picker: IQuickPick<ISymbolsQuickPickItem>): void {
47+
48+
// Allow to open symbols in background without closing picker
49+
picker.canAcceptInBackground = true;
50+
}
51+
4652
private get configuration() {
4753
const editorConfig = this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor;
4854

@@ -129,9 +135,9 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
129135
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
130136
}
131137
],
132-
accept: async keyMods => this.openSymbol(provider, symbol, token, keyMods),
133-
trigger: async (buttonIndex, keyMods) => {
134-
this.openSymbol(provider, symbol, token, keyMods, true);
138+
accept: async (keyMods, event) => this.openSymbol(provider, symbol, token, keyMods, { preserveFocus: event.inBackground }),
139+
trigger: (buttonIndex, keyMods) => {
140+
this.openSymbol(provider, symbol, token, keyMods, { forceOpenSideBySide: true });
135141

136142
return TriggerAction.CLOSE_PICKER;
137143
}
@@ -145,7 +151,7 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
145151
return symbolPicks;
146152
}
147153

148-
private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, keyMods: IKeyMods, forceOpenSideBySide = false): Promise<void> {
154+
private async openSymbol(provider: IWorkspaceSymbolProvider, symbol: IWorkspaceSymbol, token: CancellationToken, keyMods: IKeyMods, options: { forceOpenSideBySide?: boolean, preserveFocus?: boolean }): Promise<void> {
149155

150156
// Resolve actual symbol to open for providers that can resolve
151157
let symbolToOpen = symbol;
@@ -159,18 +165,19 @@ export class SymbolsQuickAccessProvider extends PickerQuickAccessProvider<ISymbo
159165

160166
// Open HTTP(s) links with opener service
161167
if (symbolToOpen.location.uri.scheme === Schemas.http || symbolToOpen.location.uri.scheme === Schemas.https) {
162-
this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true });
168+
await this.openerService.open(symbolToOpen.location.uri, { fromUserGesture: true });
163169
}
164170

165171
// Otherwise open as editor
166172
else {
167-
this.editorService.openEditor({
173+
await this.editorService.openEditor({
168174
resource: symbolToOpen.location.uri,
169175
options: {
170-
pinned: keyMods.alt || forceOpenSideBySide || this.configuration.openEditorPinned,
176+
preserveFocus: options?.preserveFocus,
177+
pinned: keyMods.alt || options?.preserveFocus || options?.forceOpenSideBySide || this.configuration.openEditorPinned,
171178
selection: symbolToOpen.location.range ? Range.collapseToStart(symbolToOpen.location.range) : undefined
172179
}
173-
}, keyMods.ctrlCmd || forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP);
180+
}, keyMods.ctrlCmd || options?.forceOpenSideBySide ? SIDE_GROUP : ACTIVE_GROUP);
174181
}
175182
}
176183

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { localize } from 'vs/nls';
7-
import { IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
7+
import { IQuickPickSeparator, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
88
import { IPickerQuickAccessItem, PickerQuickAccessProvider, TriggerAction } from 'vs/platform/quickinput/common/quickAccess';
99
import { matchesFuzzy } from 'vs/base/common/filters';
1010
import { ITerminalService } from 'vs/workbench/contrib/terminal/browser/terminal';
@@ -22,6 +22,12 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick
2222
super(TerminalQuickAccessProvider.PREFIX);
2323
}
2424

25+
protected configure(picker: IQuickPick<IPickerQuickAccessItem>): void {
26+
27+
// Allow to open terminals in background without closing picker
28+
picker.canAcceptInBackground = true;
29+
}
30+
2531
protected getPicks(filter: string): Array<IPickerQuickAccessItem | IQuickPickSeparator> {
2632
const terminalPicks: Array<IPickerQuickAccessItem | IQuickPickSeparator> = [];
2733

@@ -60,9 +66,9 @@ export class TerminalQuickAccessProvider extends PickerQuickAccessProvider<IPick
6066

6167
return TriggerAction.NO_ACTION;
6268
},
63-
accept: () => {
69+
accept: (keyMod, event) => {
6470
this.terminalService.setActiveInstance(terminal);
65-
this.terminalService.showPanel(true);
71+
this.terminalService.showPanel(!event.inBackground);
6672
}
6773
});
6874
}

0 commit comments

Comments
 (0)