gotoLineQuickAccess.ts 5.0 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 { IQuickPick, IQuickPickItem } from 'vs/platform/quickinput/common/quickInput';
8
import { CancellationToken } from 'vs/base/common/cancellation';
9 10
import { DisposableStore, IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { IEditor, ScrollType } from 'vs/editor/common/editorCommon';
11
import { IRange } from 'vs/editor/common/core/range';
12
import { AbstractEditorNavigationQuickAccessProvider } from 'vs/editor/contrib/quickAccess/editorNavigationQuickAccess';
13 14 15 16
import { IPosition } from 'vs/editor/common/core/position';

interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { }

17
export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider<IGotoLineQuickPickItem> {
18

B
Benjamin Pasero 已提交
19 20
	static PREFIX = ':';

21
	protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem>): IDisposable {
22
		const label = localize('cannotRunGotoLine', "Open a text editor first to go to a line.");
23

24 25 26 27 28 29
		picker.items = [{ label }];
		picker.ariaLabel = label;

		return Disposable.None;
	}

30
	protected provideWithTextEditor(editor: IEditor, picker: IQuickPick<IGotoLineQuickPickItem>, token: CancellationToken): IDisposable {
31 32 33 34 35 36 37 38 39 40
		const disposables = new DisposableStore();

		// Goto line once picked
		disposables.add(picker.onDidAccept(() => {
			const [item] = picker.selectedItems;
			if (item) {
				if (!this.isValidLineNumber(editor, item.lineNumber)) {
					return;
				}

41
				this.gotoLocation(editor, this.toRange(item.lineNumber, item.column), picker.keyMods);
42 43 44 45 46 47 48

				picker.hide();
			}
		}));

		// React to picker changes
		const updatePickerAndEditor = () => {
B
Benjamin Pasero 已提交
49
			const position = this.parsePosition(editor, picker.value.trim().substr(AbstractGotoLineQuickAccessProvider.PREFIX.length));
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 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
			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()));

		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}, Column: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);
		}

		return localize('gotoLineLabelEmpty', "Current Line: {0}, Column: {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;
	}
}