mirror of
https://github.com/coder/code-server.git
synced 2026-05-10 06:17:27 +02:00
Merge commit 'be3e8236086165e5e45a5a10783823874b3f3ebd' as 'lib/vscode'
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AbstractCommandsQuickAccessProvider, ICommandQuickPick, ICommandsQuickAccessOptions } from 'vs/platform/quickinput/browser/commandsQuickAccess';
|
||||
import { IEditor } from 'vs/editor/common/editorCommon';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { INotificationService } from 'vs/platform/notification/common/notification';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { stripCodicons } from 'vs/base/common/codicons';
|
||||
|
||||
export abstract class AbstractEditorCommandsQuickAccessProvider extends AbstractCommandsQuickAccessProvider {
|
||||
|
||||
constructor(
|
||||
options: ICommandsQuickAccessOptions,
|
||||
instantiationService: IInstantiationService,
|
||||
keybindingService: IKeybindingService,
|
||||
commandService: ICommandService,
|
||||
telemetryService: ITelemetryService,
|
||||
notificationService: INotificationService
|
||||
) {
|
||||
super(options, instantiationService, keybindingService, commandService, telemetryService, notificationService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses to provide the current active editor control.
|
||||
*/
|
||||
protected abstract activeTextEditorControl: IEditor | undefined;
|
||||
|
||||
protected getCodeEditorCommandPicks(): ICommandQuickPick[] {
|
||||
const activeTextEditorControl = this.activeTextEditorControl;
|
||||
if (!activeTextEditorControl) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const editorCommandPicks: ICommandQuickPick[] = [];
|
||||
for (const editorAction of activeTextEditorControl.getSupportedActions()) {
|
||||
editorCommandPicks.push({
|
||||
commandId: editorAction.id,
|
||||
commandAlias: editorAction.alias,
|
||||
label: stripCodicons(editorAction.label) || editorAction.id,
|
||||
});
|
||||
}
|
||||
|
||||
return editorCommandPicks;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IQuickAccessProvider } from 'vs/platform/quickinput/common/quickAccess';
|
||||
import { IEditor, ScrollType, IDiffEditor } from 'vs/editor/common/editorCommon';
|
||||
import { IModelDeltaDecoration, OverviewRulerLane, ITextModel } from 'vs/editor/common/model';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
|
||||
import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry';
|
||||
import { IQuickPick, IQuickPickItem, IKeyMods } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IDisposable, DisposableStore, toDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { isDiffEditor, getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { once } from 'vs/base/common/functional';
|
||||
|
||||
interface IEditorLineDecoration {
|
||||
rangeHighlightId: string;
|
||||
overviewRulerDecorationId: string;
|
||||
}
|
||||
|
||||
export interface IEditorNavigationQuickAccessOptions {
|
||||
canAcceptInBackground?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reusable quick access provider for the editor with support
|
||||
* for adding decorations for navigating in the currently active file
|
||||
* (for example "Go to line", "Go to symbol").
|
||||
*/
|
||||
export abstract class AbstractEditorNavigationQuickAccessProvider implements IQuickAccessProvider {
|
||||
|
||||
constructor(protected options?: IEditorNavigationQuickAccessOptions) { }
|
||||
|
||||
//#region Provider methods
|
||||
|
||||
provide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Apply options if any
|
||||
picker.canAcceptInBackground = !!this.options?.canAcceptInBackground;
|
||||
|
||||
// Disable filtering & sorting, we control the results
|
||||
picker.matchOnLabel = picker.matchOnDescription = picker.matchOnDetail = picker.sortByLabel = false;
|
||||
|
||||
// Provide based on current active editor
|
||||
const pickerDisposable = disposables.add(new MutableDisposable());
|
||||
pickerDisposable.value = this.doProvide(picker, token);
|
||||
|
||||
// Re-create whenever the active editor changes
|
||||
disposables.add(this.onDidActiveTextEditorControlChange(() => {
|
||||
|
||||
// Clear old
|
||||
pickerDisposable.value = undefined;
|
||||
|
||||
// Add new
|
||||
pickerDisposable.value = this.doProvide(picker, token);
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private doProvide(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// With text control
|
||||
const editor = this.activeTextEditorControl;
|
||||
if (editor && this.canProvideWithTextEditor(editor)) {
|
||||
|
||||
// Restore any view state if this picker was closed
|
||||
// without actually going to a line
|
||||
const codeEditor = getCodeEditor(editor);
|
||||
if (codeEditor) {
|
||||
|
||||
// Remember view state and update it when the cursor position
|
||||
// changes even later because it could be that the user has
|
||||
// configured quick access to remain open when focus is lost and
|
||||
// we always want to restore the current location.
|
||||
let lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState());
|
||||
disposables.add(codeEditor.onDidChangeCursorPosition(() => {
|
||||
lastKnownEditorViewState = withNullAsUndefined(editor.saveViewState());
|
||||
}));
|
||||
|
||||
disposables.add(once(token.onCancellationRequested)(() => {
|
||||
if (lastKnownEditorViewState && editor === this.activeTextEditorControl) {
|
||||
editor.restoreViewState(lastKnownEditorViewState);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Clean up decorations on dispose
|
||||
disposables.add(toDisposable(() => this.clearDecorations(editor)));
|
||||
|
||||
// Ask subclass for entries
|
||||
disposables.add(this.provideWithTextEditor(editor, picker, token));
|
||||
}
|
||||
|
||||
// Without text control
|
||||
else {
|
||||
disposables.add(this.provideWithoutTextEditor(picker, token));
|
||||
}
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses to implement if they can operate on the text editor.
|
||||
*/
|
||||
protected canProvideWithTextEditor(editor: IEditor): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Subclasses to implement to provide picks for the picker when an editor is active.
|
||||
*/
|
||||
protected abstract provideWithTextEditor(editor: IEditor, picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
|
||||
|
||||
/**
|
||||
* Subclasses to implement to provide picks for the picker when no editor is active.
|
||||
*/
|
||||
protected abstract provideWithoutTextEditor(picker: IQuickPick<IQuickPickItem>, token: CancellationToken): IDisposable;
|
||||
|
||||
protected gotoLocation(editor: IEditor, options: { range: IRange, keyMods: IKeyMods, forceSideBySide?: boolean, preserveFocus?: boolean }): void {
|
||||
editor.setSelection(options.range);
|
||||
editor.revealRangeInCenter(options.range, ScrollType.Smooth);
|
||||
if (!options.preserveFocus) {
|
||||
editor.focus();
|
||||
}
|
||||
}
|
||||
|
||||
protected getModel(editor: IEditor | IDiffEditor): ITextModel | undefined {
|
||||
return isDiffEditor(editor) ?
|
||||
editor.getModel()?.modified :
|
||||
editor.getModel() as ITextModel;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Editor access
|
||||
|
||||
/**
|
||||
* Subclasses to provide an event when the active editor control changes.
|
||||
*/
|
||||
protected abstract readonly onDidActiveTextEditorControlChange: Event<void>;
|
||||
|
||||
/**
|
||||
* Subclasses to provide the current active editor control.
|
||||
*/
|
||||
protected abstract activeTextEditorControl: IEditor | undefined;
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
//#region Decorations Utils
|
||||
|
||||
private rangeHighlightDecorationId: IEditorLineDecoration | undefined = undefined;
|
||||
|
||||
protected addDecorations(editor: IEditor, range: IRange): void {
|
||||
editor.changeDecorations(changeAccessor => {
|
||||
|
||||
// Reset old decorations if any
|
||||
const deleteDecorations: string[] = [];
|
||||
if (this.rangeHighlightDecorationId) {
|
||||
deleteDecorations.push(this.rangeHighlightDecorationId.overviewRulerDecorationId);
|
||||
deleteDecorations.push(this.rangeHighlightDecorationId.rangeHighlightId);
|
||||
|
||||
this.rangeHighlightDecorationId = undefined;
|
||||
}
|
||||
|
||||
// Add new decorations for the range
|
||||
const newDecorations: IModelDeltaDecoration[] = [
|
||||
|
||||
// highlight the entire line on the range
|
||||
{
|
||||
range,
|
||||
options: {
|
||||
className: 'rangeHighlight',
|
||||
isWholeLine: true
|
||||
}
|
||||
},
|
||||
|
||||
// also add overview ruler highlight
|
||||
{
|
||||
range,
|
||||
options: {
|
||||
overviewRuler: {
|
||||
color: themeColorFromId(overviewRulerRangeHighlight),
|
||||
position: OverviewRulerLane.Full
|
||||
}
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const [rangeHighlightId, overviewRulerDecorationId] = changeAccessor.deltaDecorations(deleteDecorations, newDecorations);
|
||||
|
||||
this.rangeHighlightDecorationId = { rangeHighlightId, overviewRulerDecorationId };
|
||||
});
|
||||
}
|
||||
|
||||
protected clearDecorations(editor: IEditor): void {
|
||||
const rangeHighlightDecorationId = this.rangeHighlightDecorationId;
|
||||
if (rangeHighlightDecorationId) {
|
||||
editor.changeDecorations(changeAccessor => {
|
||||
changeAccessor.deltaDecorations([
|
||||
rangeHighlightDecorationId.overviewRulerDecorationId,
|
||||
rangeHighlightDecorationId.rangeHighlightId
|
||||
], []);
|
||||
});
|
||||
|
||||
this.rangeHighlightDecorationId = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEditor, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { IRange } from 'vs/editor/common/core/range';
|
||||
import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
|
||||
import { IPosition } from 'vs/editor/common/core/position';
|
||||
import { getCodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorOption, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions';
|
||||
|
||||
interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { }
|
||||
|
||||
export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {
|
||||
|
||||
static PREFIX = ':';
|
||||
|
||||
constructor() {
|
||||
super({ canAcceptInBackground: true });
|
||||
}
|
||||
|
||||
protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem>): IDisposable {
|
||||
const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line.");
|
||||
|
||||
picker.items = [{ label }];
|
||||
picker.ariaLabel = label;
|
||||
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
protected provideWithTextEditor(editor: IEditor, picker: IQuickPick<IGotoLineQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Goto line once picked
|
||||
disposables.add(picker.onDidAccept(event => {
|
||||
const [item] = picker.selectedItems;
|
||||
if (item) {
|
||||
if (!this.isValidLineNumber(editor, item.lineNumber)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.gotoLocation(editor, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods, preserveFocus: event.inBackground });
|
||||
|
||||
if (!event.inBackground) {
|
||||
picker.hide();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// React to picker changes
|
||||
const updatePickerAndEditor = () => {
|
||||
const position = this.parsePosition(editor, picker.value.trim().substr(AbstractGotoLineQuickAccessProvider.PREFIX.length));
|
||||
const label = this.getPickLabel(editor, position.lineNumber, position.column);
|
||||
|
||||
// Picker
|
||||
picker.items = [{
|
||||
lineNumber: position.lineNumber,
|
||||
column: position.column,
|
||||
label
|
||||
}];
|
||||
|
||||
// ARIA Label
|
||||
picker.ariaLabel = label;
|
||||
|
||||
// Clear decorations for invalid range
|
||||
if (!this.isValidLineNumber(editor, position.lineNumber)) {
|
||||
this.clearDecorations(editor);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reveal
|
||||
const range = this.toRange(position.lineNumber, position.column);
|
||||
editor.revealRangeInCenter(range, ScrollType.Smooth);
|
||||
|
||||
// Decorate
|
||||
this.addDecorations(editor, range);
|
||||
};
|
||||
updatePickerAndEditor();
|
||||
disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor()));
|
||||
|
||||
// Adjust line number visibility as needed
|
||||
const codeEditor = getCodeEditor(editor);
|
||||
if (codeEditor) {
|
||||
const options = codeEditor.getOptions();
|
||||
const lineNumbers = options.get(EditorOption.lineNumbers);
|
||||
if (lineNumbers.renderType === RenderLineNumbersType.Relative) {
|
||||
codeEditor.updateOptions({ lineNumbers: 'on' });
|
||||
|
||||
disposables.add(toDisposable(() => codeEditor.updateOptions({ lineNumbers: 'relative' })));
|
||||
}
|
||||
}
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private toRange(lineNumber = 1, column = 1): IRange {
|
||||
return {
|
||||
startLineNumber: lineNumber,
|
||||
startColumn: column,
|
||||
endLineNumber: lineNumber,
|
||||
endColumn: column
|
||||
};
|
||||
}
|
||||
|
||||
private parsePosition(editor: IEditor, value: string): IPosition {
|
||||
|
||||
// Support line-col formats of `line,col`, `line:col`, `line#col`
|
||||
const numbers = value.split(/,|:|#/).map(part => parseInt(part, 10)).filter(part => !isNaN(part));
|
||||
const endLine = this.lineCount(editor) + 1;
|
||||
|
||||
return {
|
||||
lineNumber: numbers[0] > 0 ? numbers[0] : endLine + numbers[0],
|
||||
column: numbers[1]
|
||||
};
|
||||
}
|
||||
|
||||
private getPickLabel(editor: IEditor, lineNumber: number, column: number | undefined): string {
|
||||
|
||||
// Location valid: indicate this as picker label
|
||||
if (this.isValidLineNumber(editor, lineNumber)) {
|
||||
if (this.isValidColumn(editor, lineNumber, column)) {
|
||||
return localize('gotoLineColumnLabel', "Go to line {0} and column {1}.", lineNumber, column);
|
||||
}
|
||||
|
||||
return localize('gotoLineLabel', "Go to line {0}.", lineNumber);
|
||||
}
|
||||
|
||||
// Location invalid: show generic label
|
||||
const position = editor.getPosition() || { lineNumber: 1, column: 1 };
|
||||
const lineCount = this.lineCount(editor);
|
||||
if (lineCount > 1) {
|
||||
return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);
|
||||
}
|
||||
|
||||
return localize('gotoLineLabelEmpty', "Current Line: {0}, Character: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
|
||||
}
|
||||
|
||||
private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean {
|
||||
if (!lineNumber || typeof lineNumber !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return lineNumber > 0 && lineNumber <= this.lineCount(editor);
|
||||
}
|
||||
|
||||
private isValidColumn(editor: IEditor, lineNumber: number, column: number | undefined): boolean {
|
||||
if (!column || typeof column !== 'number') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const model = this.getModel(editor);
|
||||
if (!model) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const positionCandidate = { lineNumber, column };
|
||||
|
||||
return model.validatePosition(positionCandidate).equals(positionCandidate);
|
||||
}
|
||||
|
||||
private lineCount(editor: IEditor): number {
|
||||
return this.getModel(editor)?.getLineCount() ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,496 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* 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';
|
||||
import { IQuickPick, IQuickPickItem, IQuickPickSeparator } from 'vs/platform/quickinput/common/quickInput';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { DisposableStore, IDisposable, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IEditor, ScrollType } from 'vs/editor/common/editorCommon';
|
||||
import { ITextModel } from 'vs/editor/common/model';
|
||||
import { IRange, Range } from 'vs/editor/common/core/range';
|
||||
import { AbstractEditorNavigationQuickAccessProvider, IEditorNavigationQuickAccessOptions } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
|
||||
import { DocumentSymbol, SymbolKinds, SymbolTag, DocumentSymbolProviderRegistry, SymbolKind } from 'vs/editor/common/modes';
|
||||
import { OutlineModel, OutlineElement } from 'vs/editor/contrib/documentSymbols/outlineModel';
|
||||
import { trim, format } from 'vs/base/common/strings';
|
||||
import { prepareQuery, IPreparedQuery, pieceToQuery, scoreFuzzy2 } from 'vs/base/common/fuzzyScorer';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { Iterable } from 'vs/base/common/iterator';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export interface IGotoSymbolQuickPickItem extends IQuickPickItem {
|
||||
kind: SymbolKind,
|
||||
index: number,
|
||||
score?: number;
|
||||
range?: { decoration: IRange, selection: IRange }
|
||||
}
|
||||
|
||||
export interface IGotoSymbolQuickAccessProviderOptions extends IEditorNavigationQuickAccessOptions {
|
||||
openSideBySideDirection?: () => undefined | 'right' | 'down'
|
||||
}
|
||||
|
||||
export abstract class AbstractGotoSymbolQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {
|
||||
|
||||
static PREFIX = '@';
|
||||
static SCOPE_PREFIX = ':';
|
||||
static PREFIX_BY_CATEGORY = `${AbstractGotoSymbolQuickAccessProvider.PREFIX}${AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX}`;
|
||||
|
||||
constructor(protected options: IGotoSymbolQuickAccessProviderOptions = Object.create(null)) {
|
||||
super(options);
|
||||
|
||||
options.canAcceptInBackground = true;
|
||||
}
|
||||
|
||||
protected provideWithoutTextEditor(picker: IQuickPick<IGotoSymbolQuickPickItem>): IDisposable {
|
||||
this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutEditor', "To go to a symbol, first open a text editor with symbol information."));
|
||||
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
protected provideWithTextEditor(editor: IEditor, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const model = this.getModel(editor);
|
||||
if (!model) {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
// Provide symbols from model if available in registry
|
||||
if (DocumentSymbolProviderRegistry.has(model)) {
|
||||
return this.doProvideWithEditorSymbols(editor, model, picker, token);
|
||||
}
|
||||
|
||||
// Otherwise show an entry for a model without registry
|
||||
// But give a chance to resolve the symbols at a later
|
||||
// point if possible
|
||||
return this.doProvideWithoutEditorSymbols(editor, model, picker, token);
|
||||
}
|
||||
|
||||
private doProvideWithoutEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Generic pick for not having any symbol information
|
||||
this.provideLabelPick(picker, localize('cannotRunGotoSymbolWithoutSymbolProvider', "The active text editor does not provide symbol information."));
|
||||
|
||||
// Wait for changes to the registry and see if eventually
|
||||
// we do get symbols. This can happen if the picker is opened
|
||||
// very early after the model has loaded but before the
|
||||
// language registry is ready.
|
||||
// https://github.com/microsoft/vscode/issues/70607
|
||||
(async () => {
|
||||
const result = await this.waitForLanguageSymbolRegistry(model, disposables);
|
||||
if (!result || token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
disposables.add(this.doProvideWithEditorSymbols(editor, model, picker, token));
|
||||
})();
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
private provideLabelPick(picker: IQuickPick<IGotoSymbolQuickPickItem>, label: string): void {
|
||||
picker.items = [{ label, index: 0, kind: SymbolKind.String }];
|
||||
picker.ariaLabel = label;
|
||||
}
|
||||
|
||||
protected async waitForLanguageSymbolRegistry(model: ITextModel, disposables: DisposableStore): Promise<boolean> {
|
||||
if (DocumentSymbolProviderRegistry.has(model)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let symbolProviderRegistryPromiseResolve: (res: boolean) => void;
|
||||
const symbolProviderRegistryPromise = new Promise<boolean>(resolve => symbolProviderRegistryPromiseResolve = resolve);
|
||||
|
||||
// Resolve promise when registry knows model
|
||||
const symbolProviderListener = disposables.add(DocumentSymbolProviderRegistry.onDidChange(() => {
|
||||
if (DocumentSymbolProviderRegistry.has(model)) {
|
||||
symbolProviderListener.dispose();
|
||||
|
||||
symbolProviderRegistryPromiseResolve(true);
|
||||
}
|
||||
}));
|
||||
|
||||
// Resolve promise when we get disposed too
|
||||
disposables.add(toDisposable(() => symbolProviderRegistryPromiseResolve(false)));
|
||||
|
||||
return symbolProviderRegistryPromise;
|
||||
}
|
||||
|
||||
private doProvideWithEditorSymbols(editor: IEditor, model: ITextModel, picker: IQuickPick<IGotoSymbolQuickPickItem>, token: CancellationToken): IDisposable {
|
||||
const disposables = new DisposableStore();
|
||||
|
||||
// Goto symbol once picked
|
||||
disposables.add(picker.onDidAccept(event => {
|
||||
const [item] = picker.selectedItems;
|
||||
if (item && item.range) {
|
||||
this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods, preserveFocus: event.inBackground });
|
||||
|
||||
if (!event.inBackground) {
|
||||
picker.hide();
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// Goto symbol side by side if enabled
|
||||
disposables.add(picker.onDidTriggerItemButton(({ item }) => {
|
||||
if (item && item.range) {
|
||||
this.gotoLocation(editor, { range: item.range.selection, keyMods: picker.keyMods, forceSideBySide: true });
|
||||
|
||||
picker.hide();
|
||||
}
|
||||
}));
|
||||
|
||||
// Resolve symbols from document once and reuse this
|
||||
// request for all filtering and typing then on
|
||||
const symbolsPromise = this.getDocumentSymbols(model, true, token);
|
||||
|
||||
// Set initial picks and update on type
|
||||
let picksCts: CancellationTokenSource | undefined = undefined;
|
||||
const updatePickerItems = async () => {
|
||||
|
||||
// Cancel any previous ask for picks and busy
|
||||
picksCts?.dispose(true);
|
||||
picker.busy = false;
|
||||
|
||||
// Create new cancellation source for this run
|
||||
picksCts = new CancellationTokenSource(token);
|
||||
|
||||
// Collect symbol picks
|
||||
picker.busy = true;
|
||||
try {
|
||||
const query = prepareQuery(picker.value.substr(AbstractGotoSymbolQuickAccessProvider.PREFIX.length).trim());
|
||||
const items = await this.doGetSymbolPicks(symbolsPromise, query, undefined, picksCts.token);
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (items.length > 0) {
|
||||
picker.items = items;
|
||||
} else {
|
||||
if (query.original.length > 0) {
|
||||
this.provideLabelPick(picker, localize('noMatchingSymbolResults', "No matching editor symbols"));
|
||||
} else {
|
||||
this.provideLabelPick(picker, localize('noSymbolResults', "No editor symbols"));
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!token.isCancellationRequested) {
|
||||
picker.busy = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
disposables.add(picker.onDidChangeValue(() => updatePickerItems()));
|
||||
updatePickerItems();
|
||||
|
||||
// Reveal and decorate when active item changes
|
||||
// However, ignore the very first event so that
|
||||
// opening the picker is not immediately revealing
|
||||
// and decorating the first entry.
|
||||
let ignoreFirstActiveEvent = true;
|
||||
disposables.add(picker.onDidChangeActive(() => {
|
||||
const [item] = picker.activeItems;
|
||||
if (item && item.range) {
|
||||
if (ignoreFirstActiveEvent) {
|
||||
ignoreFirstActiveEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Reveal
|
||||
editor.revealRangeInCenter(item.range.selection, ScrollType.Smooth);
|
||||
|
||||
// Decorate
|
||||
this.addDecorations(editor, item.range.decoration);
|
||||
}
|
||||
}));
|
||||
|
||||
return disposables;
|
||||
}
|
||||
|
||||
protected async doGetSymbolPicks(symbolsPromise: Promise<DocumentSymbol[]>, query: IPreparedQuery, options: { extraContainerLabel?: string } | undefined, token: CancellationToken): Promise<Array<IGotoSymbolQuickPickItem | IQuickPickSeparator>> {
|
||||
const symbols = await symbolsPromise;
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const filterBySymbolKind = query.original.indexOf(AbstractGotoSymbolQuickAccessProvider.SCOPE_PREFIX) === 0;
|
||||
const filterPos = filterBySymbolKind ? 1 : 0;
|
||||
|
||||
// Split between symbol and container query
|
||||
let symbolQuery: IPreparedQuery;
|
||||
let containerQuery: IPreparedQuery | undefined;
|
||||
if (query.values && query.values.length > 1) {
|
||||
symbolQuery = pieceToQuery(query.values[0]); // symbol: only match on first part
|
||||
containerQuery = pieceToQuery(query.values.slice(1)); // container: match on all but first parts
|
||||
} else {
|
||||
symbolQuery = query;
|
||||
}
|
||||
|
||||
// Convert to symbol picks and apply filtering
|
||||
const filteredSymbolPicks: IGotoSymbolQuickPickItem[] = [];
|
||||
for (let index = 0; index < symbols.length; index++) {
|
||||
const symbol = symbols[index];
|
||||
|
||||
const symbolLabel = trim(symbol.name);
|
||||
const symbolLabelWithIcon = `$(symbol-${SymbolKinds.toString(symbol.kind) || 'property'}) ${symbolLabel}`;
|
||||
const symbolLabelIconOffset = symbolLabelWithIcon.length - symbolLabel.length;
|
||||
|
||||
let containerLabel = symbol.containerName;
|
||||
if (options?.extraContainerLabel) {
|
||||
if (containerLabel) {
|
||||
containerLabel = `${options.extraContainerLabel} • ${containerLabel}`;
|
||||
} else {
|
||||
containerLabel = options.extraContainerLabel;
|
||||
}
|
||||
}
|
||||
|
||||
let symbolScore: number | undefined = undefined;
|
||||
let symbolMatches: IMatch[] | undefined = undefined;
|
||||
|
||||
let containerScore: number | undefined = undefined;
|
||||
let containerMatches: IMatch[] | undefined = undefined;
|
||||
|
||||
if (query.original.length > filterPos) {
|
||||
|
||||
// First: try to score on the entire query, it is possible that
|
||||
// the symbol matches perfectly (e.g. searching for "change log"
|
||||
// can be a match on a markdown symbol "change log"). In that
|
||||
// case we want to skip the container query altogether.
|
||||
let skipContainerQuery = false;
|
||||
if (symbolQuery !== query) {
|
||||
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, { ...query, values: undefined /* disable multi-query support */ }, filterPos, symbolLabelIconOffset);
|
||||
if (typeof symbolScore === 'number') {
|
||||
skipContainerQuery = true; // since we consumed the query, skip any container matching
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise: score on the symbol query and match on the container later
|
||||
if (typeof symbolScore !== 'number') {
|
||||
[symbolScore, symbolMatches] = scoreFuzzy2(symbolLabelWithIcon, symbolQuery, filterPos, symbolLabelIconOffset);
|
||||
if (typeof symbolScore !== 'number') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Score by container if specified
|
||||
if (!skipContainerQuery && containerQuery) {
|
||||
if (containerLabel && containerQuery.original.length > 0) {
|
||||
[containerScore, containerMatches] = scoreFuzzy2(containerLabel, containerQuery);
|
||||
}
|
||||
|
||||
if (typeof containerScore !== 'number') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof symbolScore === 'number') {
|
||||
symbolScore += containerScore; // boost symbolScore by containerScore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const deprecated = symbol.tags && symbol.tags.indexOf(SymbolTag.Deprecated) >= 0;
|
||||
|
||||
filteredSymbolPicks.push({
|
||||
index,
|
||||
kind: symbol.kind,
|
||||
score: symbolScore,
|
||||
label: symbolLabelWithIcon,
|
||||
ariaLabel: symbolLabel,
|
||||
description: containerLabel,
|
||||
highlights: deprecated ? undefined : {
|
||||
label: symbolMatches,
|
||||
description: containerMatches
|
||||
},
|
||||
range: {
|
||||
selection: Range.collapseToStart(symbol.selectionRange),
|
||||
decoration: symbol.range
|
||||
},
|
||||
strikethrough: deprecated,
|
||||
buttons: (() => {
|
||||
const openSideBySideDirection = this.options?.openSideBySideDirection ? this.options?.openSideBySideDirection() : undefined;
|
||||
if (!openSideBySideDirection) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
iconClass: openSideBySideDirection === 'right' ? Codicon.splitHorizontal.classNames : Codicon.splitVertical.classNames,
|
||||
tooltip: openSideBySideDirection === 'right' ? localize('openToSide', "Open to the Side") : localize('openToBottom', "Open to the Bottom")
|
||||
}
|
||||
];
|
||||
})()
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by score
|
||||
const sortedFilteredSymbolPicks = filteredSymbolPicks.sort((symbolA, symbolB) => filterBySymbolKind ?
|
||||
this.compareByKindAndScore(symbolA, symbolB) :
|
||||
this.compareByScore(symbolA, symbolB)
|
||||
);
|
||||
|
||||
// Add separator for types
|
||||
// - @ only total number of symbols
|
||||
// - @: grouped by symbol kind
|
||||
let symbolPicks: Array<IGotoSymbolQuickPickItem | IQuickPickSeparator> = [];
|
||||
if (filterBySymbolKind) {
|
||||
let lastSymbolKind: SymbolKind | undefined = undefined;
|
||||
let lastSeparator: IQuickPickSeparator | undefined = undefined;
|
||||
let lastSymbolKindCounter = 0;
|
||||
|
||||
function updateLastSeparatorLabel(): void {
|
||||
if (lastSeparator && typeof lastSymbolKind === 'number' && lastSymbolKindCounter > 0) {
|
||||
lastSeparator.label = format(NLS_SYMBOL_KIND_CACHE[lastSymbolKind] || FALLBACK_NLS_SYMBOL_KIND, lastSymbolKindCounter);
|
||||
}
|
||||
}
|
||||
|
||||
for (const symbolPick of sortedFilteredSymbolPicks) {
|
||||
|
||||
// Found new kind
|
||||
if (lastSymbolKind !== symbolPick.kind) {
|
||||
|
||||
// Update last separator with number of symbols we found for kind
|
||||
updateLastSeparatorLabel();
|
||||
|
||||
lastSymbolKind = symbolPick.kind;
|
||||
lastSymbolKindCounter = 1;
|
||||
|
||||
// Add new separator for new kind
|
||||
lastSeparator = { type: 'separator' };
|
||||
symbolPicks.push(lastSeparator);
|
||||
}
|
||||
|
||||
// Existing kind, keep counting
|
||||
else {
|
||||
lastSymbolKindCounter++;
|
||||
}
|
||||
|
||||
// Add to final result
|
||||
symbolPicks.push(symbolPick);
|
||||
}
|
||||
|
||||
// Update last separator with number of symbols we found for kind
|
||||
updateLastSeparatorLabel();
|
||||
} else if (sortedFilteredSymbolPicks.length > 0) {
|
||||
symbolPicks = [
|
||||
{ label: localize('symbols', "symbols ({0})", filteredSymbolPicks.length), type: 'separator' },
|
||||
...sortedFilteredSymbolPicks
|
||||
];
|
||||
}
|
||||
|
||||
return symbolPicks;
|
||||
}
|
||||
|
||||
private compareByScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number {
|
||||
if (typeof symbolA.score !== 'number' && typeof symbolB.score === 'number') {
|
||||
return 1;
|
||||
} else if (typeof symbolA.score === 'number' && typeof symbolB.score !== 'number') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (typeof symbolA.score === 'number' && typeof symbolB.score === 'number') {
|
||||
if (symbolA.score > symbolB.score) {
|
||||
return -1;
|
||||
} else if (symbolA.score < symbolB.score) {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (symbolA.index < symbolB.index) {
|
||||
return -1;
|
||||
} else if (symbolA.index > symbolB.index) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private compareByKindAndScore(symbolA: IGotoSymbolQuickPickItem, symbolB: IGotoSymbolQuickPickItem): number {
|
||||
const kindA = NLS_SYMBOL_KIND_CACHE[symbolA.kind] || FALLBACK_NLS_SYMBOL_KIND;
|
||||
const kindB = NLS_SYMBOL_KIND_CACHE[symbolB.kind] || FALLBACK_NLS_SYMBOL_KIND;
|
||||
|
||||
// Sort by type first if scoped search
|
||||
const result = kindA.localeCompare(kindB);
|
||||
if (result === 0) {
|
||||
return this.compareByScore(symbolA, symbolB);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
protected async getDocumentSymbols(document: ITextModel, flatten: boolean, token: CancellationToken): Promise<DocumentSymbol[]> {
|
||||
const model = await OutlineModel.create(document, token);
|
||||
if (token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const roots: DocumentSymbol[] = [];
|
||||
for (const child of model.children.values()) {
|
||||
if (child instanceof OutlineElement) {
|
||||
roots.push(child.symbol);
|
||||
} else {
|
||||
roots.push(...Iterable.map(child.children.values(), child => child.symbol));
|
||||
}
|
||||
}
|
||||
|
||||
let flatEntries: DocumentSymbol[] = [];
|
||||
if (flatten) {
|
||||
this.flattenDocumentSymbols(flatEntries, roots, '');
|
||||
} else {
|
||||
flatEntries = roots;
|
||||
}
|
||||
|
||||
return flatEntries.sort((symbolA, symbolB) => Range.compareRangesUsingStarts(symbolA.range, symbolB.range));
|
||||
}
|
||||
|
||||
private flattenDocumentSymbols(bucket: DocumentSymbol[], entries: DocumentSymbol[], overrideContainerLabel: string): void {
|
||||
for (const entry of entries) {
|
||||
bucket.push({
|
||||
kind: entry.kind,
|
||||
tags: entry.tags,
|
||||
name: entry.name,
|
||||
detail: entry.detail,
|
||||
containerName: entry.containerName || overrideContainerLabel,
|
||||
range: entry.range,
|
||||
selectionRange: entry.selectionRange,
|
||||
children: undefined, // we flatten it...
|
||||
});
|
||||
|
||||
// Recurse over children
|
||||
if (entry.children) {
|
||||
this.flattenDocumentSymbols(bucket, entry.children, entry.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #region NLS Helpers
|
||||
|
||||
const FALLBACK_NLS_SYMBOL_KIND = localize('property', "properties ({0})");
|
||||
const NLS_SYMBOL_KIND_CACHE: { [type: number]: string } = {
|
||||
[SymbolKind.Method]: localize('method', "methods ({0})"),
|
||||
[SymbolKind.Function]: localize('function', "functions ({0})"),
|
||||
[SymbolKind.Constructor]: localize('_constructor', "constructors ({0})"),
|
||||
[SymbolKind.Variable]: localize('variable', "variables ({0})"),
|
||||
[SymbolKind.Class]: localize('class', "classes ({0})"),
|
||||
[SymbolKind.Struct]: localize('struct', "structs ({0})"),
|
||||
[SymbolKind.Event]: localize('event', "events ({0})"),
|
||||
[SymbolKind.Operator]: localize('operator', "operators ({0})"),
|
||||
[SymbolKind.Interface]: localize('interface', "interfaces ({0})"),
|
||||
[SymbolKind.Namespace]: localize('namespace', "namespaces ({0})"),
|
||||
[SymbolKind.Package]: localize('package', "packages ({0})"),
|
||||
[SymbolKind.TypeParameter]: localize('typeParameter', "type parameters ({0})"),
|
||||
[SymbolKind.Module]: localize('modules', "modules ({0})"),
|
||||
[SymbolKind.Property]: localize('property', "properties ({0})"),
|
||||
[SymbolKind.Enum]: localize('enum', "enumerations ({0})"),
|
||||
[SymbolKind.EnumMember]: localize('enumMember', "enumeration members ({0})"),
|
||||
[SymbolKind.String]: localize('string', "strings ({0})"),
|
||||
[SymbolKind.File]: localize('file', "files ({0})"),
|
||||
[SymbolKind.Array]: localize('array', "arrays ({0})"),
|
||||
[SymbolKind.Number]: localize('number', "numbers ({0})"),
|
||||
[SymbolKind.Boolean]: localize('boolean', "booleans ({0})"),
|
||||
[SymbolKind.Object]: localize('object', "objects ({0})"),
|
||||
[SymbolKind.Key]: localize('key', "keys ({0})"),
|
||||
[SymbolKind.Field]: localize('field', "fields ({0})"),
|
||||
[SymbolKind.Constant]: localize('constant', "constants ({0})")
|
||||
};
|
||||
|
||||
//#endregion
|
||||
Reference in New Issue
Block a user