gotoSymbolQuickAccess.ts 8.9 KB
Newer Older
1 2 3 4 5 6
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import { localize } from 'vs/nls';
7
import { IKeyMods, IQuickPickSeparator, IQuickInputService, IQuickPick } from 'vs/platform/quickinput/common/quickInput';
8 9 10 11
import { IEditor } from 'vs/editor/common/editorCommon';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IRange } from 'vs/editor/common/core/range';
import { Registry } from 'vs/platform/registry/common/platform';
12
import { IQuickAccessRegistry, Extensions as QuickaccessExtensions } from 'vs/platform/quickinput/common/quickAccess';
13
import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from 'vs/editor/contrib/quickAccess/gotoSymbolQuickAccess';
14
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
15
import { IWorkbenchEditorConfiguration, IEditorPane } from 'vs/workbench/common/editor';
16
import { ITextModel } from 'vs/editor/common/model';
17
import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
18
import { timeout } from 'vs/base/common/async';
19
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
20
import { Action } from 'vs/base/common/actions';
21 22 23
import { IWorkbenchActionRegistry, Extensions as ActionExtensions } from 'vs/workbench/common/actions';
import { SyncActionDescriptor } from 'vs/platform/actions/common/actions';
import { KeyMod, KeyCode } from 'vs/base/common/keyCodes';
24
import { prepareQuery } from 'vs/base/common/fuzzyScorer';
25 26 27
import { SymbolKind } from 'vs/editor/common/modes';
import { fuzzyScore, createMatches } from 'vs/base/common/filters';
import { onUnexpectedError } from 'vs/base/common/errors';
28 29 30

export class GotoSymbolQuickAccessProvider extends AbstractGotoSymbolQuickAccessProvider {

31
	protected readonly onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange;
32

33 34 35 36
	constructor(
		@IEditorService private readonly editorService: IEditorService,
		@IConfigurationService private readonly configurationService: IConfigurationService
	) {
37 38 39 40 41
		super({
			openSideBySideDirection: () => this.configuration.openSideBySideDirection
		});
	}

42 43
	//#region DocumentSymbols (text editor required)

44 45 46 47 48 49 50 51 52
	private get configuration() {
		const editorConfig = this.configurationService.getValue<IWorkbenchEditorConfiguration>().workbench.editor;

		return {
			openEditorPinned: !editorConfig.enablePreviewFromQuickOpen,
			openSideBySideDirection: editorConfig.openSideBySideDirection
		};
	}

53
	protected get activeTextEditorControl() {
54 55 56
		return this.editorService.activeTextEditorControl;
	}

57
	protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void {
58 59

		// Check for sideBySide use
60
		if ((options.keyMods.ctrlCmd || options.forceSideBySide) && this.editorService.activeEditor) {
61
			this.editorService.openEditor(this.editorService.activeEditor, {
62
				selection: options.range,
63 64
				pinned: options.keyMods.alt || this.configuration.openEditorPinned,
				preserveFocus: options.preserveFocus
65
			}, SIDE_GROUP);
66 67 68 69
		}

		// Otherwise let parent handle it
		else {
70
			super.gotoLocation(editor, options);
71 72
		}
	}
73

74
	//#endregion
75 76 77 78 79

	//#region public methods to use this picker from other pickers

	private static readonly SYMBOL_PICKS_TIMEOUT = 8000;

80
	async getSymbolPicks(model: ITextModel, filter: string, options: { extraContainerLabel?: string }, disposables: DisposableStore, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
81 82 83 84 85 86 87 88 89 90 91 92 93 94

		// If the registry does not know the model, we wait for as long as
		// the registry knows it. This helps in cases where a language
		// registry was not activated yet for providing any symbols.
		// To not wait forever, we eventually timeout though.
		const result = await Promise.race([
			this.waitForLanguageSymbolRegistry(model, disposables),
			timeout(GotoSymbolQuickAccessProvider.SYMBOL_PICKS_TIMEOUT)
		]);

		if (!result || token.isCancellationRequested) {
			return [];
		}

95
		return this.doGetSymbolPicks(this.getDocumentSymbols(model, true, token), prepareQuery(filter), options, token);
96 97 98 99 100 101 102 103 104 105 106
	}

	addDecorations(editor: IEditor, range: IRange): void {
		super.addDecorations(editor, range);
	}

	clearDecorations(editor: IEditor): void {
		super.clearDecorations(editor);
	}

	//#endregion
107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181

	protected provideWithoutTextEditor(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable {
		const pane = this.editorService.activeEditorPane;
		if (!pane || !TableOfContentsProviderRegistry.has(pane.getId())) {
			//
			return super.provideWithoutTextEditor(picker);
		}

		const provider = TableOfContentsProviderRegistry.get(pane.getId())!;
		const cts = new CancellationTokenSource();

		const disposables = new DisposableStore();
		disposables.add(toDisposable(() => cts.dispose(true)));

		picker.busy = true;

		provider.provideTableOfContents(pane, cts.token).then(entries => {

			picker.busy = false;

			if (cts.token.isCancellationRequested || !entries || entries.length === 0) {
				return;
			}

			const items: IGotoSymbolQuickPickItem[] = entries.map((entry, idx) => {
				return {
					kind: SymbolKind.File,
					index: idx,
					score: 0,
					label: entry.label,
					detail: entry.detail,
					description: entry.description,
				};
			});

			disposables.add(picker.onDidAccept(() => {
				picker.hide();
				const [entry] = picker.selectedItems;
				entries[entry.index]?.reveal();
			}));

			const updatePickerItems = () => {
				const filteredItems = items.filter(item => {
					if (picker.value === '@') {
						// default, no filtering, scoring...
						item.score = 0;
						item.highlights = undefined;
						return true;
					}
					const score = fuzzyScore(picker.value, picker.value.toLowerCase(), 1 /*@-character*/, item.label, item.label.toLowerCase(), 0, true);
					if (!score) {
						return false;
					}
					item.score = score[1];
					item.highlights = { label: createMatches(score) };
					return true;
				});
				if (filteredItems.length === 0) {
					const label = localize('empty', 'No matching entries');
					picker.items = [{ label, index: -1, kind: SymbolKind.String }];
					picker.ariaLabel = label;
				} else {
					picker.items = filteredItems;
				}
			};
			updatePickerItems();
			disposables.add(picker.onDidChangeValue(updatePickerItems));

		}).catch(err => {
			onUnexpectedError(err);
			picker.hide();
		});

		return disposables;
	}
182 183
}

184
Registry.as<IQuickAccessRegistry>(QuickaccessExtensions.Quickaccess).registerQuickAccessProvider({
185 186
	ctor: GotoSymbolQuickAccessProvider,
	prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX,
187
	contextKey: 'inFileSymbolsPicker',
188 189
	placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."),
	helpEntries: [
190 191
		{ description: localize('gotoSymbolQuickAccess', "Go to Symbol in Editor"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX, needsEditor: true },
		{ description: localize('gotoSymbolByCategoryQuickAccess', "Go to Symbol in Editor by Category"), prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX_BY_CATEGORY, needsEditor: true }
192 193
	]
});
194

195
export class GotoSymbolAction extends Action {
196 197 198 199 200

	static readonly ID = 'workbench.action.gotoSymbol';
	static readonly LABEL = localize('gotoSymbol', "Go to Symbol in Editor...");

	constructor(
201 202 203
		id: string,
		label: string,
		@IQuickInputService private readonly quickInputService: IQuickInputService
204
	) {
205 206 207 208 209
		super(id, label);
	}

	async run(): Promise<void> {
		this.quickInputService.quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX);
210 211
	}
}
212

213
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).registerWorkbenchAction(SyncActionDescriptor.from(GotoSymbolAction, {
214 215
	primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O
}), 'Go to Symbol in Editor...');
216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255


//#region toc definition and logic

export interface ITableOfContentsEntry {
	label: string;
	detail?: string;
	description?: string;
	reveal(): any;
}

export interface ITableOfContentsProvider<T extends IEditorPane = IEditorPane> {
	provideTableOfContents(editor: T, token: CancellationToken): Promise<ITableOfContentsEntry[] | undefined | null>;
}

class ProviderRegistry {

	private readonly _provider = new Map<string, ITableOfContentsProvider>();

	register(type: string, provider: ITableOfContentsProvider): IDisposable {
		this._provider.set(type, provider);
		return toDisposable(() => {
			if (this._provider.get(type) === provider) {
				this._provider.delete(type);
			}
		});
	}

	get(type: string): ITableOfContentsProvider | undefined {
		return this._provider.get(type);
	}

	has(type: string): boolean {
		return this._provider.has(type);
	}
}

export const TableOfContentsProviderRegistry = new ProviderRegistry();

//#endregion