Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts
5254 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { CancellationToken } from '../../../../base/common/cancellation.js';
7
import { Codicon } from '../../../../base/common/codicons.js';
8
import { Disposable, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
9
import { ThemeIcon } from '../../../../base/common/themables.js';
10
import { localize } from '../../../../nls.js';
11
import { IQuickInputButton, IQuickPick, IQuickPickItem, QuickInputButtonLocation } from '../../../../platform/quickinput/common/quickInput.js';
12
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
13
import { getCodeEditor } from '../../../browser/editorBrowser.js';
14
import { EditorOption, RenderLineNumbersType } from '../../../common/config/editorOptions.js';
15
import { CursorColumns } from '../../../common/core/cursorColumns.js';
16
import { IPosition } from '../../../common/core/position.js';
17
import { IRange } from '../../../common/core/range.js';
18
import { IEditor, ScrollType } from '../../../common/editorCommon.js';
19
import { AbstractEditorNavigationQuickAccessProvider, IQuickAccessTextEditorContext } from './editorNavigationQuickAccess.js';
20
21
interface IGotoLineQuickPickItem extends IQuickPickItem, Partial<IPosition> { }
22
23
export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditorNavigationQuickAccessProvider {
24
25
static readonly GO_TO_LINE_PREFIX = ':';
26
static readonly GO_TO_OFFSET_PREFIX = '::';
27
private static readonly ZERO_BASED_OFFSET_STORAGE_KEY = 'gotoLine.useZeroBasedOffset';
28
29
constructor() {
30
super({ canAcceptInBackground: true });
31
}
32
33
protected abstract readonly storageService: IStorageService;
34
35
private get useZeroBasedOffset() {
36
return this.storageService.getBoolean(
37
AbstractGotoLineQuickAccessProvider.ZERO_BASED_OFFSET_STORAGE_KEY,
38
StorageScope.APPLICATION,
39
false);
40
}
41
42
private set useZeroBasedOffset(value: boolean) {
43
this.storageService.store(
44
AbstractGotoLineQuickAccessProvider.ZERO_BASED_OFFSET_STORAGE_KEY,
45
value,
46
StorageScope.APPLICATION,
47
StorageTarget.USER);
48
}
49
50
protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>): IDisposable {
51
const label = localize('gotoLine.noEditor', "Open a text editor first to go to a line or an offset.");
52
53
picker.items = [{ label }];
54
picker.ariaLabel = label;
55
56
return Disposable.None;
57
}
58
59
protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>, token: CancellationToken): IDisposable {
60
const editor = context.editor;
61
const disposables = new DisposableStore();
62
63
// Set initial ariaLabel for screen readers
64
picker.ariaLabel = localize('gotoLine.ariaLabel', "Go to line. Type a line number, optionally followed by colon and column number.");
65
66
// Goto line once picked
67
disposables.add(picker.onDidAccept(event => {
68
const [item] = picker.selectedItems;
69
if (item) {
70
if (!item.lineNumber) {
71
return;
72
}
73
74
this.gotoLocation(context, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods, preserveFocus: event.inBackground });
75
76
if (!event.inBackground) {
77
picker.hide();
78
}
79
}
80
}));
81
82
// Add a toggle to switch between 1- and 0-based offsets.
83
const offsetButton: IQuickInputButton = {
84
iconClass: ThemeIcon.asClassName(Codicon.indexZero),
85
tooltip: localize('gotoLineToggleButton', "Toggle Zero-Based Offset"),
86
location: QuickInputButtonLocation.Input,
87
toggle: { checked: this.useZeroBasedOffset }
88
};
89
90
// React to picker changes
91
const updatePickerAndEditor = () => {
92
const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX.length);
93
const { inOffsetMode, lineNumber, column, label } = this.parsePosition(editor, inputText);
94
95
// Show toggle only when input text starts with '::'.
96
picker.buttons = inOffsetMode ? [offsetButton] : [];
97
98
// Picker
99
picker.items = [{
100
lineNumber,
101
column,
102
label,
103
ariaLabel: lineNumber
104
? localize('gotoLine.itemAriaLabel', "Go to line {0}, column {1}. Press Enter to navigate.", lineNumber, column || 1)
105
: label,
106
}];
107
108
// Clear decorations for invalid range
109
if (!lineNumber) {
110
this.clearDecorations(editor);
111
return;
112
}
113
114
// Reveal
115
const range = this.toRange(lineNumber, column);
116
editor.revealRangeInCenter(range, ScrollType.Smooth);
117
118
// Decorate
119
this.addDecorations(editor, range);
120
};
121
122
disposables.add(picker.onDidTriggerButton(button => {
123
if (button === offsetButton) {
124
this.useZeroBasedOffset = button.toggle?.checked ?? !this.useZeroBasedOffset;
125
updatePickerAndEditor();
126
}
127
}));
128
129
updatePickerAndEditor();
130
disposables.add(picker.onDidChangeValue(() => updatePickerAndEditor()));
131
132
// Adjust line number visibility as needed
133
const codeEditor = getCodeEditor(editor);
134
if (codeEditor) {
135
const options = codeEditor.getOptions();
136
const lineNumbers = options.get(EditorOption.lineNumbers);
137
if (lineNumbers.renderType === RenderLineNumbersType.Relative) {
138
codeEditor.updateOptions({ lineNumbers: 'on' });
139
140
disposables.add(toDisposable(() => codeEditor.updateOptions({ lineNumbers: 'relative' })));
141
}
142
}
143
144
return disposables;
145
}
146
147
private toRange(lineNumber = 1, column = 1): IRange {
148
return {
149
startLineNumber: lineNumber,
150
startColumn: column,
151
endLineNumber: lineNumber,
152
endColumn: column
153
};
154
}
155
156
protected parsePosition(editor: IEditor, value: string): Partial<IPosition> & { inOffsetMode?: boolean; label: string } {
157
const model = this.getModel(editor);
158
if (!model) {
159
return {
160
label: localize('gotoLine.noEditor', "Open a text editor first to go to a line or an offset.")
161
};
162
}
163
164
// Support ::<offset> notation to navigate to a specific offset in the model.
165
if (value.startsWith(':')) {
166
let offset = parseInt(value.substring(1), 10);
167
const maxOffset = model.getValueLength();
168
if (isNaN(offset)) {
169
// No valid offset specified.
170
return {
171
inOffsetMode: true,
172
label: this.useZeroBasedOffset ?
173
localize('gotoLine.offsetPromptZero', "Type a character position to go to (from 0 to {0}).", maxOffset - 1) :
174
localize('gotoLine.offsetPrompt', "Type a character position to go to (from 1 to {0}).", maxOffset)
175
};
176
} else {
177
const reverse = offset < 0;
178
if (!this.useZeroBasedOffset) {
179
// Convert 1-based offset to model's 0-based.
180
offset -= Math.sign(offset);
181
}
182
183
if (reverse) {
184
// Offset from the end of the buffer
185
offset += maxOffset;
186
}
187
188
const pos = model.getPositionAt(offset);
189
const visibleColumn = CursorColumns.visibleColumnFromColumn(
190
model.getLineContent(pos.lineNumber),
191
pos.column,
192
model.getOptions().tabSize) + 1;
193
194
return {
195
...pos,
196
inOffsetMode: true,
197
label: localize('gotoLine.goToPosition', "Press 'Enter' to go to line {0} at column {1}.", pos.lineNumber, visibleColumn)
198
};
199
}
200
} else {
201
// Support line-col formats of `line,col`, `line:col`, `line#col`
202
const parts = value.split(/,|:|#/);
203
204
const maxLine = model.getLineCount();
205
let lineNumber = parseInt(parts[0]?.trim(), 10);
206
if (parts.length < 1 || isNaN(lineNumber)) {
207
return {
208
label: localize('gotoLine.linePrompt', "Type a line number to go to (from 1 to {0}).", maxLine)
209
};
210
}
211
212
// Handle negative line numbers and clip to valid range.
213
lineNumber = lineNumber >= 0 ? lineNumber : (maxLine + 1) + lineNumber;
214
lineNumber = Math.min(Math.max(1, lineNumber), maxLine);
215
216
// Treat column number as visible column
217
const tabSize = model.getOptions().tabSize;
218
const lineContent = model.getLineContent(lineNumber);
219
const maxColumn = CursorColumns.visibleColumnFromColumn(lineContent, model.getLineMaxColumn(lineNumber), tabSize) + 1;
220
221
let column = parseInt(parts[1]?.trim(), 10);
222
if (parts.length < 2 || isNaN(column)) {
223
return {
224
lineNumber,
225
column: 1,
226
label: parts.length < 2 ?
227
localize('gotoLine.lineColumnPrompt', "Press 'Enter' to go to line {0} or enter colon : to add a column number.", lineNumber) :
228
localize('gotoLine.columnPrompt', "Press 'Enter' to go to line {0} or enter a column number (from 1 to {1}).", lineNumber, maxColumn)
229
};
230
}
231
232
// Handle negative column numbers and clip to valid range.
233
column = column >= 0 ? column : maxColumn + column;
234
column = Math.min(Math.max(1, column), maxColumn);
235
236
const realColumn = CursorColumns.columnFromVisibleColumn(lineContent, column - 1, tabSize);
237
return {
238
lineNumber,
239
column: realColumn,
240
label: localize('gotoLine.goToPosition', "Press 'Enter' to go to line {0} at column {1}.", lineNumber, column)
241
};
242
}
243
}
244
}
245
246