Path: blob/main/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { CancellationToken } from '../../../../base/common/cancellation.js';6import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { getCodeEditor } from '../../../browser/editorBrowser.js';8import { EditorOption, RenderLineNumbersType } from '../../../common/config/editorOptions.js';9import { IPosition } from '../../../common/core/position.js';10import { IRange } from '../../../common/core/range.js';11import { IEditor, ScrollType } from '../../../common/editorCommon.js';12import { AbstractEditorNavigationQuickAccessProvider, IQuickAccessTextEditorContext } from './editorNavigationQuickAccess.js';13import { localize } from '../../../../nls.js';14import { IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';1516interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { }1718export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {1920static PREFIX = ':';2122constructor() {23super({ canAcceptInBackground: true });24}2526protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>): IDisposable {27const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line.");2829picker.items = [{ label }];30picker.ariaLabel = label;3132return Disposable.None;33}3435protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>, token: CancellationToken): IDisposable {36const editor = context.editor;37const disposables = new DisposableStore();3839// Goto line once picked40disposables.add(picker.onDidAccept(event => {41const [item] = picker.selectedItems;42if (item) {43if (!this.isValidLineNumber(editor, item.lineNumber)) {44return;45}4647this.gotoLocation(context, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods, preserveFocus: event.inBackground });4849if (!event.inBackground) {50picker.hide();51}52}53}));5455// React to picker changes56const updatePickerAndEditor = () => {57const position = this.parsePosition(editor, picker.value.trim().substr(AbstractGotoLineQuickAccessProvider.PREFIX.length));58const label = this.getPickLabel(editor, position.lineNumber, position.column);5960// Picker61picker.items = [{62lineNumber: position.lineNumber,63column: position.column,64label65}];6667// ARIA Label68picker.ariaLabel = label;6970// Clear decorations for invalid range71if (!this.isValidLineNumber(editor, position.lineNumber)) {72this.clearDecorations(editor);73return;74}7576// Reveal77const range = this.toRange(position.lineNumber, position.column);78editor.revealRangeInCenter(range, ScrollType.Smooth);7980// Decorate81this.addDecorations(editor, range);82};83updatePickerAndEditor();84disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor()));8586// Adjust line number visibility as needed87const codeEditor = getCodeEditor(editor);88if (codeEditor) {89const options = codeEditor.getOptions();90const lineNumbers = options.get(EditorOption.lineNumbers);91if (lineNumbers.renderType === RenderLineNumbersType.Relative) {92codeEditor.updateOptions({ lineNumbers: 'on' });9394disposables.add(toDisposable(() => codeEditor.updateOptions({ lineNumbers: 'relative' })));95}96}9798return disposables;99}100101private toRange(lineNumber = 1, column = 1): IRange {102return {103startLineNumber: lineNumber,104startColumn: column,105endLineNumber: lineNumber,106endColumn: column107};108}109110private parsePosition(editor: IEditor, value: string): IPosition {111112// Support line-col formats of `line,col`, `line:col`, `line#col`113const numbers = value.split(/,|:|#/).map(part => parseInt(part, 10)).filter(part => !isNaN(part));114const endLine = this.lineCount(editor) + 1;115116return {117lineNumber: numbers[0] > 0 ? numbers[0] : endLine + numbers[0],118column: numbers[1]119};120}121122private getPickLabel(editor: IEditor, lineNumber: number, column: number | undefined): string {123124// Location valid: indicate this as picker label125if (this.isValidLineNumber(editor, lineNumber)) {126if (this.isValidColumn(editor, lineNumber, column)) {127return localize('gotoLineColumnLabel', "Go to line {0} and character {1}.", lineNumber, column);128}129130return localize('gotoLineLabel', "Go to line {0}.", lineNumber);131}132133// Location invalid: show generic label134const position = editor.getPosition() || { lineNumber: 1, column: 1 };135const lineCount = this.lineCount(editor);136if (lineCount > 1) {137return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);138}139140return localize('gotoLineLabelEmpty', "Current Line: {0}, Character: {1}. Type a line number to navigate to.", position.lineNumber, position.column);141}142143private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean {144if (!lineNumber || typeof lineNumber !== 'number') {145return false;146}147148return lineNumber > 0 && lineNumber <= this.lineCount(editor);149}150151private isValidColumn(editor: IEditor, lineNumber: number, column: number | undefined): boolean {152if (!column || typeof column !== 'number') {153return false;154}155156const model = this.getModel(editor);157if (!model) {158return false;159}160161const positionCandidate = { lineNumber, column };162163return model.validatePosition(positionCandidate).equals(positionCandidate);164}165166private lineCount(editor: IEditor): number {167return this.getModel(editor)?.getLineCount() ?? 0;168}169}170171172