Skip to content

Commit 0681eb9

Browse files
authored
Updates to Go To quick access (#273668)
* Don't wait for async setting update of zero-based offset value * Fix labels/descriptions * Update unit-tests
1 parent 5b8296c commit 0681eb9

File tree

3 files changed

+84
-91
lines changed

3 files changed

+84
-91
lines changed

src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts

Lines changed: 79 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,23 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
2424

2525
static PREFIX = ':';
2626

27-
constructor(private useZeroBasedOffset: { value: boolean } = { value: false }) {
27+
private _useZeroBasedOffset: boolean;
28+
29+
constructor(private useZeroBasedOffsetSetting?: { value: boolean }) {
2830
super({ canAcceptInBackground: true });
31+
this._useZeroBasedOffset = useZeroBasedOffsetSetting?.value === true;
32+
}
33+
34+
private get useZeroBasedOffset() {
35+
return this._useZeroBasedOffset;
36+
}
37+
38+
private set useZeroBasedOffset(value: boolean) {
39+
this._useZeroBasedOffset = value;
40+
if (this.useZeroBasedOffsetSetting) {
41+
// Asynchronously persist the setting change
42+
this.useZeroBasedOffsetSetting.value = value;
43+
}
2944
}
3045

3146
protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>): IDisposable {
@@ -45,7 +60,7 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
4560
disposables.add(picker.onDidAccept(event => {
4661
const [item] = picker.selectedItems;
4762
if (item) {
48-
if (!this.isValidLineNumber(editor, item.lineNumber)) {
63+
if (!item.lineNumber) {
4964
return;
5065
}
5166

@@ -60,34 +75,29 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
6075
// React to picker changes
6176
const updatePickerAndEditor = () => {
6277
const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.PREFIX.length);
63-
const inOffsetMode = inputText.startsWith(':');
64-
const position = this.parsePosition(editor, inputText);
65-
const label = this.getPickLabel(editor, position.lineNumber, position.column, inOffsetMode);
78+
const { inOffsetMode, lineNumber, column, label } = this.parsePosition(editor, inputText);
6679

6780
// Show toggle only when input text starts with '::'.
68-
toggle.visible = inOffsetMode;
81+
toggle.visible = !!inOffsetMode;
6982

7083
// Picker
7184
picker.items = [{
72-
lineNumber: position.lineNumber,
73-
column: position.column,
85+
lineNumber,
86+
column,
7487
label,
75-
detail: inputText.length ?
76-
undefined : // Don't show hint once the user has started typing.
77-
localize('gotoLineQuickAccessDescription', "Use :line[:column] or ::offset to go to a position. Negative values are counted from the end.")
7888
}];
7989

8090
// ARIA Label
8191
picker.ariaLabel = label;
8292

8393
// Clear decorations for invalid range
84-
if (!this.isValidLineNumber(editor, position.lineNumber)) {
94+
if (!lineNumber) {
8595
this.clearDecorations(editor);
8696
return;
8797
}
8898

8999
// Reveal
90-
const range = this.toRange(position.lineNumber, position.column);
100+
const range = this.toRange(lineNumber, column);
91101
editor.revealRangeInCenter(range, ScrollType.Smooth);
92102

93103
// Decorate
@@ -96,17 +106,17 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
96106

97107
// Add a toggle to switch between 1- and 0-based offsets.
98108
const toggle = new Toggle({
99-
title: localize('gotoLineToggle', "Use zero-based offset"),
109+
title: localize('gotoLineToggle', "Use Zero-Based Offset"),
100110
icon: Codicon.indexZero,
101-
isChecked: this.useZeroBasedOffset.value,
111+
isChecked: this.useZeroBasedOffset,
102112
inputActiveOptionBorder: asCssVariable(inputActiveOptionBorder),
103113
inputActiveOptionForeground: asCssVariable(inputActiveOptionForeground),
104114
inputActiveOptionBackground: asCssVariable(inputActiveOptionBackground)
105115
});
106116

107117
disposables.add(
108118
toggle.onChange(() => {
109-
this.useZeroBasedOffset.value = !this.useZeroBasedOffset.value;
119+
this.useZeroBasedOffset = !this.useZeroBasedOffset;
110120
updatePickerAndEditor();
111121
}));
112122

@@ -139,95 +149,78 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
139149
};
140150
}
141151

142-
protected parsePosition(editor: IEditor, value: string): IPosition {
152+
protected parsePosition(editor: IEditor, value: string): Partial<IPosition> & { inOffsetMode?: boolean; label: string } {
143153
const model = this.getModel(editor);
154+
if (!model) {
155+
return {
156+
label: localize('gotoLine.noEditor', "Open a text editor first to go to a line.")
157+
};
158+
}
144159

145160
// Support ::<offset> notation to navigate to a specific offset in the model.
146161
if (value.startsWith(':')) {
147162
let offset = parseInt(value.substring(1), 10);
148-
if (!isNaN(offset) && model) {
163+
const maxOffset = model.getValueLength();
164+
if (isNaN(offset)) {
165+
// No valid offset specified.
166+
return {
167+
inOffsetMode: true,
168+
label: localize('gotoLine.offsetPrompt', "Type a character number in the file from 1 to {0} to go to.", maxOffset)
169+
};
170+
} else {
149171
const reverse = offset < 0;
150-
if (!this.useZeroBasedOffset.value) {
172+
if (!this.useZeroBasedOffset) {
151173
// Convert 1-based offset to model's 0-based.
152174
offset -= Math.sign(offset);
153175
}
154176
if (reverse) {
155177
// Offset from the end of the buffer
156-
offset += model.getValueLength();
178+
offset += maxOffset;
157179
}
158-
return model.getPositionAt(offset);
180+
const pos = model.getPositionAt(offset);
181+
return {
182+
...pos,
183+
inOffsetMode: true,
184+
label: localize('gotoLine.goToPosition', "Press Enter to go to line {0} and column {1}.", pos.lineNumber, pos.column)
185+
};
186+
}
187+
} else {
188+
// Support line-col formats of `line,col`, `line:col`, `line#col`
189+
const parts = value.split(/,|:|#/);
190+
191+
const maxLine = model.getLineCount();
192+
let lineNumber = parseInt(parts[0]?.trim(), 10);
193+
if (parts.length < 1 || isNaN(lineNumber)) {
194+
return {
195+
label: localize('gotoLine.linePrompt', "Type a line number from 1 to {0} to go to.", maxLine)
196+
};
159197
}
160-
}
161-
162-
// Support line-col formats of `line,col`, `line:col`, `line#col`
163-
let [lineNumber, column] = value.split(/,|:|#/).map(part => parseInt(part, 10)).filter(part => !isNaN(part));
164198

165-
// Handle negative line numbers and clip to valid range.
166-
const maxLine = (model?.getLineCount() ?? 0) + 1;
167-
lineNumber = lineNumber >= 0 ? lineNumber : maxLine + lineNumber;
168-
lineNumber = Math.min(Math.max(1, lineNumber), maxLine);
199+
// Handle negative line numbers and clip to valid range.
200+
lineNumber = lineNumber >= 0 ? lineNumber : (maxLine + 1) + lineNumber;
201+
lineNumber = Math.min(Math.max(1, lineNumber), maxLine);
169202

170-
// Handle negative column numbers and clip to valid range.
171-
if (column !== undefined && model) {
172203
const maxColumn = model.getLineMaxColumn(lineNumber);
173-
column = column >= 0 ? column : maxColumn + column;
174-
column = Math.min(Math.max(1, column), maxColumn);
175-
}
176-
177-
return { lineNumber, column };
178-
}
179-
180-
private getPickLabel(editor: IEditor, lineNumber: number, column: number | undefined, inOffsetMode: boolean): string {
181-
182-
// Location valid: indicate this as picker label
183-
if (this.isValidLineNumber(editor, lineNumber)) {
184-
if (this.isValidColumn(editor, lineNumber, column)) {
185-
return localize('gotoLineColumnLabel', "Go to line {0} and character {1}.", lineNumber, column);
204+
let column = parseInt(parts[1]?.trim(), 10);
205+
if (parts.length < 2 || isNaN(column)) {
206+
return {
207+
lineNumber,
208+
column: 1,
209+
label: parts.length < 2 ?
210+
localize('gotoLine.lineColumnPrompt', "Press Enter to go to line {0}. Type : to enter column number.", lineNumber) :
211+
localize('gotoLine.columnPrompt', "Press Enter to go to line {0} or enter column number from 1 to {1}.", lineNumber, maxColumn)
212+
};
186213
}
187214

188-
return localize('gotoLineLabel', "Go to line {0}.", lineNumber);
189-
}
190-
191-
// Location invalid: show generic label
192-
const position = editor.getPosition() || { lineNumber: 1, column: 1 };
193-
194-
// When in offset mode, prompt for an offset.
195-
if (inOffsetMode) {
196-
return localize('gotoLineOffsetLabel', "Current Line: {0}, Character: {1}. Type a character offset to navigate to.", position.lineNumber, position.column);
197-
}
198-
199-
const lineCount = this.lineCount(editor);
200-
if (lineCount > 1) {
201-
return localize('gotoLineLabelEmptyWithLimit', "Current Line: {0}, Character: {1}. Type a line number between 1 and {2} to navigate to.", position.lineNumber, position.column, lineCount);
202-
}
203-
204-
return localize('gotoLineLabelEmpty', "Current Line: {0}, Character: {1}. Type a line number to navigate to.", position.lineNumber, position.column);
205-
}
206-
207-
private isValidLineNumber(editor: IEditor, lineNumber: number | undefined): boolean {
208-
if (!lineNumber || typeof lineNumber !== 'number') {
209-
return false;
210-
}
211-
212-
return lineNumber > 0 && lineNumber <= this.lineCount(editor);
213-
}
214-
215-
private isValidColumn(editor: IEditor, lineNumber: number, column: number | undefined): boolean {
216-
if (!column || typeof column !== 'number') {
217-
return false;
218-
}
215+
// Handle negative column numbers and clip to valid range.
216+
column = column >= 0 ? column : maxColumn + column;
217+
column = Math.min(Math.max(1, column), maxColumn);
219218

220-
const model = this.getModel(editor);
221-
if (!model) {
222-
return false;
219+
return {
220+
lineNumber,
221+
column,
222+
label: localize('gotoLine.goToPosition', "Press Enter to go to line {0} and column {1}.", lineNumber, column)
223+
};
223224
}
224-
225-
const positionCandidate = { lineNumber, column };
226-
227-
return model.validatePosition(positionCandidate).equals(positionCandidate);
228-
}
229-
230-
private lineCount(editor: IEditor): number {
231-
return this.getModel(editor)?.getLineCount() ?? 0;
232225
}
233226
}

src/vs/editor/contrib/quickAccess/test/browser/gotoLineQuickAccess.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ suite('AbstractGotoLineQuickAccessProvider', () => {
3535
], {}, (editor, _) => {
3636
const { lineNumber, column } = provider.parsePositionTest(editor, input);
3737
assert.strictEqual(lineNumber, expectedLine);
38-
assert.strictEqual(column, expectedColumn);
38+
assert.strictEqual(column, expectedColumn ?? 1);
3939
});
4040
}
4141

@@ -48,9 +48,9 @@ suite('AbstractGotoLineQuickAccessProvider', () => {
4848
runTest('1', 1);
4949
runTest('2', 2);
5050
runTest('5', 5);
51-
runTest('6', 6);
52-
runTest('7', 6);
53-
runTest('100', 6);
51+
runTest('6', 5);
52+
runTest('7', 5);
53+
runTest('100', 5);
5454

5555
// :line,column
5656
runTest('2:-100', 2, 1);

src/vs/workbench/contrib/codeEditor/browser/quickaccess/gotoLineQuickAccess.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,6 @@ registerAction2(GotoLineAction);
104104
Registry.as<IQuickAccessRegistry>(QuickaccesExtensions.Quickaccess).registerQuickAccessProvider({
105105
ctor: GotoLineQuickAccessProvider,
106106
prefix: AbstractGotoLineQuickAccessProvider.PREFIX,
107-
placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. 42:5 for line 42 and column 5)."),
107+
placeholder: localize('gotoLineQuickAccessPlaceholder', "Type the line number and optional column to go to (e.g. :42:5 for line 42, column 5). Type :: to go to a character offset (e.g. ::1024 for character 1024 from the start of the file). Use negative values to navigate backwards."),
108108
helpEntries: [{ description: localize('gotoLineQuickAccess', "Go to Line/Column"), commandId: GotoLineAction.ID }]
109109
});

0 commit comments

Comments
 (0)