Files
azuredatastudio/src/vs/workbench/contrib/quickopen/browser/gotoLineHandler.ts

343 lines
11 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import * as types from 'vs/base/common/types';
import { IEntryRunContext, Mode, IAutoFocus } from 'vs/base/parts/quickopen/common/quickOpen';
import { QuickOpenModel } from 'vs/base/parts/quickopen/browser/quickOpenModel';
import { QuickOpenHandler, EditorQuickOpenEntry, QuickOpenAction } from 'vs/workbench/browser/quickopen';
import { IEditor, IEditorViewState, IDiffEditorModel, ScrollType } from 'vs/editor/common/editorCommon';
import { OverviewRulerLane, IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { ITextEditorOptions } from 'vs/platform/editor/common/editor';
import { IEditorInput, GroupIdentifier } from 'vs/workbench/common/editor';
import { IQuickOpenService } from 'vs/platform/quickOpen/common/quickOpen';
import { IRange } from 'vs/editor/common/core/range';
import { overviewRulerRangeHighlight } from 'vs/editor/common/view/editorColorRegistry';
import { themeColorFromId } from 'vs/platform/theme/common/themeService';
import { IEditorOptions, RenderLineNumbersType } from 'vs/editor/common/config/editorOptions';
import { IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { isCodeEditor, isDiffEditor } from 'vs/editor/browser/editorBrowser';
import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService';
import { Event } from 'vs/base/common/event';
import { CancellationToken } from 'vs/base/common/cancellation';
export const GOTO_LINE_PREFIX = ':';
export class GotoLineAction extends QuickOpenAction {
static readonly ID = 'workbench.action.gotoLine';
static readonly LABEL = nls.localize('gotoLine', "Go to Line...");
constructor(actionId: string, actionLabel: string,
@IQuickOpenService private readonly _quickOpenService: IQuickOpenService,
@IEditorService private readonly editorService: IEditorService
) {
super(actionId, actionLabel, GOTO_LINE_PREFIX, _quickOpenService);
}
run(): Promise<void> {
let activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (!activeTextEditorWidget) {
return Promise.resolve();
}
if (isDiffEditor(activeTextEditorWidget)) {
activeTextEditorWidget = activeTextEditorWidget.getModifiedEditor();
}
let restoreOptions: IEditorOptions | null = null;
if (isCodeEditor(activeTextEditorWidget)) {
const config = activeTextEditorWidget.getConfiguration();
if (config.viewInfo.renderLineNumbers === RenderLineNumbersType.Relative) {
activeTextEditorWidget.updateOptions({
lineNumbers: 'on'
});
restoreOptions = {
lineNumbers: 'relative'
};
}
}
const result = super.run();
if (restoreOptions) {
Event.once(this._quickOpenService.onHide)(() => {
activeTextEditorWidget!.updateOptions(restoreOptions!);
});
}
return result;
}
}
class GotoLineEntry extends EditorQuickOpenEntry {
private line!: number;
private column!: number;
private handler: GotoLineHandler;
constructor(line: string, editorService: IEditorService, handler: GotoLineHandler) {
super(editorService);
this.parseInput(line);
this.handler = handler;
}
private parseInput(line: string) {
const numbers = line.split(/,|:|#/).map(part => parseInt(part, 10)).filter(part => !isNaN(part));
this.line = numbers[0];
this.column = numbers[1];
}
getLabel(): string {
// Inform user about valid range if input is invalid
const maxLineNumber = this.getMaxLineNumber();
if (this.editorService.activeTextEditorWidget && this.invalidRange(maxLineNumber)) {
const position = this.editorService.activeTextEditorWidget.getPosition();
if (position) {
if (maxLineNumber > 0) {
return nls.localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, maxLineNumber);
}
return nls.localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
}
}
// Input valid, indicate action
return this.column ? nls.localize('gotoLineColumnLabel', "Go to line {0} and column {1}.", this.line, this.column) : nls.localize('gotoLineLabel', "Go to line {0}.", this.line);
}
private invalidRange(maxLineNumber: number = this.getMaxLineNumber()): boolean {
return !this.line || !types.isNumber(this.line) || (maxLineNumber > 0 && types.isNumber(this.line) && this.line > maxLineNumber);
}
private getMaxLineNumber(): number {
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (!activeTextEditorWidget) {
return -1;
}
let model = activeTextEditorWidget.getModel();
if (model && (<IDiffEditorModel>model).modified && (<IDiffEditorModel>model).original) {
model = (<IDiffEditorModel>model).modified; // Support for diff editor models
}
return model && types.isFunction((<ITextModel>model).getLineCount) ? (<ITextModel>model).getLineCount() : -1;
}
run(mode: Mode, context: IEntryRunContext): boolean {
if (mode === Mode.OPEN) {
return this.runOpen(context);
}
return this.runPreview();
}
getInput(): IEditorInput | undefined {
return this.editorService.activeEditor;
}
getOptions(pinned?: boolean): ITextEditorOptions {
return {
selection: this.toSelection(),
pinned
};
}
runOpen(context: IEntryRunContext): boolean {
// No-op if range is not valid
if (this.invalidRange()) {
return false;
}
// Check for sideBySide use
const sideBySide = context.keymods.ctrlCmd;
if (sideBySide) {
this.editorService.openEditor(this.getInput()!, this.getOptions(context.keymods.alt), SIDE_GROUP);
}
// Apply selection and focus
const range = this.toSelection();
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (activeTextEditorWidget) {
activeTextEditorWidget.setSelection(range);
activeTextEditorWidget.revealRangeInCenter(range, ScrollType.Smooth);
}
return true;
}
runPreview(): boolean {
// No-op if range is not valid
if (this.invalidRange()) {
this.handler.clearDecorations();
return false;
}
// Select Line Position
const range = this.toSelection();
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (activeTextEditorWidget) {
activeTextEditorWidget.revealRangeInCenter(range, ScrollType.Smooth);
// Decorate if possible
if (this.editorService.activeControl && types.isFunction(activeTextEditorWidget.changeDecorations)) {
this.handler.decorateOutline(range, activeTextEditorWidget, this.editorService.activeControl.group);
}
}
return false;
}
private toSelection(): IRange {
return {
startLineNumber: this.line,
startColumn: this.column || 1,
endLineNumber: this.line,
endColumn: this.column || 1
};
}
}
interface IEditorLineDecoration {
groupId: GroupIdentifier;
rangeHighlightId: string;
lineDecorationId: string;
}
export class GotoLineHandler extends QuickOpenHandler {
static readonly ID = 'workbench.picker.line';
private rangeHighlightDecorationId: IEditorLineDecoration | null = null;
private lastKnownEditorViewState: IEditorViewState | null = null;
constructor(@IEditorService private readonly editorService: IEditorService) {
super();
}
getAriaLabel(): string {
if (this.editorService.activeTextEditorWidget) {
const position = this.editorService.activeTextEditorWidget.getPosition();
if (position) {
return nls.localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
}
}
return nls.localize('cannotRunGotoLine', "Open a text file first to go to a line.");
}
getResults(searchValue: string, token: CancellationToken): Promise<QuickOpenModel> {
searchValue = searchValue.trim();
// Remember view state to be able to restore on cancel
if (!this.lastKnownEditorViewState) {
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (activeTextEditorWidget) {
this.lastKnownEditorViewState = activeTextEditorWidget.saveViewState();
}
}
return Promise.resolve(new QuickOpenModel([new GotoLineEntry(searchValue, this.editorService, this)]));
}
canRun(): boolean | string {
const canRun = !!this.editorService.activeTextEditorWidget;
return canRun ? true : nls.localize('cannotRunGotoLine', "Open a text file first to go to a line.");
}
decorateOutline(range: IRange, editor: IEditor, group: IEditorGroup): void {
editor.changeDecorations(changeAccessor => {
const deleteDecorations: string[] = [];
if (this.rangeHighlightDecorationId) {
deleteDecorations.push(this.rangeHighlightDecorationId.lineDecorationId);
deleteDecorations.push(this.rangeHighlightDecorationId.rangeHighlightId);
this.rangeHighlightDecorationId = null;
}
const newDecorations: IModelDeltaDecoration[] = [
// rangeHighlight at index 0
{
range: range,
options: {
className: 'rangeHighlight',
isWholeLine: true
}
},
// lineDecoration at index 1
{
range: range,
options: {
overviewRuler: {
color: themeColorFromId(overviewRulerRangeHighlight),
position: OverviewRulerLane.Full
}
}
}
];
const decorations = changeAccessor.deltaDecorations(deleteDecorations, newDecorations);
const rangeHighlightId = decorations[0];
const lineDecorationId = decorations[1];
this.rangeHighlightDecorationId = {
groupId: group.id,
rangeHighlightId: rangeHighlightId,
lineDecorationId: lineDecorationId,
};
});
}
clearDecorations(): void {
const rangeHighlightDecorationId = this.rangeHighlightDecorationId;
if (rangeHighlightDecorationId) {
this.editorService.visibleControls.forEach(editor => {
if (editor.group && editor.group.id === rangeHighlightDecorationId.groupId) {
const editorControl = <IEditor>editor.getControl();
editorControl.changeDecorations(changeAccessor => {
changeAccessor.deltaDecorations([
rangeHighlightDecorationId.lineDecorationId,
rangeHighlightDecorationId.rangeHighlightId
], []);
});
}
});
this.rangeHighlightDecorationId = null;
}
}
onClose(canceled: boolean): void {
// Clear Highlight Decorations if present
this.clearDecorations();
// Restore selection if canceled
if (canceled && this.lastKnownEditorViewState) {
const activeTextEditorWidget = this.editorService.activeTextEditorWidget;
if (activeTextEditorWidget) {
activeTextEditorWidget.restoreViewState(this.lastKnownEditorViewState);
}
}
this.lastKnownEditorViewState = null;
}
getAutoFocus(searchValue: string): IAutoFocus {
return {
autoFocusFirstEntry: searchValue.trim().length > 0
};
}
}