gotoSymbolQuickAccess.ts 9.2 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

	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));

R
rebornix 已提交
175 176 177 178 179 180 181 182 183 184 185 186 187 188
			let ignoreFirstActiveEvent = true;
			disposables.add(picker.onDidChangeActive(() => {
				const [entry] = picker.activeItems;

				if (entry && entries[entry.index]) {
					if (ignoreFirstActiveEvent) {
						ignoreFirstActiveEvent = false;
						return;
					}

					entries[entry.index]?.reveal();
				}
			}));

189 190 191 192 193 194 195
		}).catch(err => {
			onUnexpectedError(err);
			picker.hide();
		});

		return disposables;
	}
196 197
}

198
Registry.as<IQuickAccessRegistry>(QuickaccessExtensions.Quickaccess).registerQuickAccessProvider({
199 200
	ctor: GotoSymbolQuickAccessProvider,
	prefix: AbstractGotoSymbolQuickAccessProvider.PREFIX,
201
	contextKey: 'inFileSymbolsPicker',
202 203
	placeholder: localize('gotoSymbolQuickAccessPlaceholder', "Type the name of a symbol to go to."),
	helpEntries: [
204 205
		{ 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 }
206 207
	]
});
208

209
export class GotoSymbolAction extends Action {
210 211 212 213 214

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

	constructor(
215 216 217
		id: string,
		label: string,
		@IQuickInputService private readonly quickInputService: IQuickInputService
218
	) {
219 220 221 222 223
		super(id, label);
	}

	async run(): Promise<void> {
		this.quickInputService.quickAccess.show(GotoSymbolQuickAccessProvider.PREFIX);
224 225
	}
}
226

227
Registry.as<IWorkbenchActionRegistry>(ActionExtensions.WorkbenchActions).registerWorkbenchAction(SyncActionDescriptor.from(GotoSymbolAction, {
228 229
	primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KEY_O
}), 'Go to Symbol in Editor...');
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 256 257 258 259 260 261 262 263 264 265 266 267 268 269


//#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