Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/vs/editor/common/config/editorConfigurationSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ const editorConfiguration: IConfigurationNode = {
description: nls.localize('wordBasedSuggestions', "Controls whether completions should be computed based on words in the document and from which documents they are computed."),
experiment: { mode: 'auto' },
},
'editor.gotoLine.autoReveal': {
type: 'boolean',
default: true,
scope: ConfigurationScope.WINDOW,
markdownDescription: nls.localize('editor.gotoLine.autoReveal', "Controls whether the Go to Line/Column picker automatically reveals the target location while typing."),
},
'editor.semanticHighlighting.enabled': {
enum: [true, false, 'configuredByTheme'],
enumDescriptions: [
Expand Down
150 changes: 136 additions & 14 deletions src/vs/editor/contrib/quickAccess/browser/gotoLineQuickAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
static readonly GO_TO_LINE_PREFIX = ':';
static readonly GO_TO_OFFSET_PREFIX = '::';
private static readonly ZERO_BASED_OFFSET_STORAGE_KEY = 'gotoLine.useZeroBasedOffset';
private static readonly DISABLE_AUTO_REVEAL_STORAGE_KEY = 'gotoLine.disableAutoReveal';
private static readonly AUTO_REVEAL_CONFIG_SNAPSHOT_STORAGE_KEY = 'gotoLine.autoRevealConfigSnapshot';

private sessionDisableAutoReveal: boolean | undefined;

constructor() {
super({ canAcceptInBackground: true });
Expand All @@ -47,6 +51,102 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
StorageTarget.USER);
}

private get disableAutoReveal() {
if (typeof this.sessionDisableAutoReveal === 'boolean') {
return this.sessionDisableAutoReveal;
}

const persisted = this.getPersistedAutoRevealPreference();
if (typeof persisted === 'boolean') {
return persisted;
}

const configured = this.getConfiguredAutoReveal();
if (typeof configured === 'boolean') {
const value = !configured;
this.persistAutoRevealPreference(value, configured);
return value;
}

return !this.isAutoRevealEnabledByDefault();
}

private set disableAutoReveal(value: boolean) {
this.sessionDisableAutoReveal = value;
this.persistAutoRevealPreference(value);
}

protected onAutoRevealConfigurationChanged(): void {
this.sessionDisableAutoReveal = undefined;
const configured = this.getConfiguredAutoReveal();
if (typeof configured === 'boolean') {
this.persistAutoRevealPreference(!configured, configured);
return;
}

this.clearPersistedAutoRevealPreference();
}

protected isAutoRevealEnabledByDefault(): boolean {
return true;
}

protected getConfiguredAutoReveal(): boolean | undefined {
return undefined;
}

private getPersistedAutoRevealPreference(): boolean | undefined {
const value = this.storageService.getBoolean(
AbstractGotoLineQuickAccessProvider.DISABLE_AUTO_REVEAL_STORAGE_KEY,
StorageScope.APPLICATION
);
if (typeof value !== 'boolean') {
return undefined;
}

const snapshotRaw = this.storageService.get(
AbstractGotoLineQuickAccessProvider.AUTO_REVEAL_CONFIG_SNAPSHOT_STORAGE_KEY,
StorageScope.APPLICATION
);
const snapshot = snapshotRaw === 'true' ? true : snapshotRaw === 'false' ? false : undefined;

const configured = this.getConfiguredAutoReveal();
if (snapshot !== configured) {
if (typeof configured === 'boolean') {
this.persistAutoRevealPreference(!configured, configured);
return !configured;
}

this.clearPersistedAutoRevealPreference();
return undefined;
}

return value;
}

private persistAutoRevealPreference(value: boolean, configuredSnapshot: boolean | undefined = this.getConfiguredAutoReveal()): void {
this.storageService.store(
AbstractGotoLineQuickAccessProvider.DISABLE_AUTO_REVEAL_STORAGE_KEY,
value,
StorageScope.APPLICATION,
StorageTarget.USER);

if (typeof configuredSnapshot === 'boolean') {
this.storageService.store(
AbstractGotoLineQuickAccessProvider.AUTO_REVEAL_CONFIG_SNAPSHOT_STORAGE_KEY,
configuredSnapshot ? 'true' : 'false',
StorageScope.APPLICATION,
StorageTarget.USER);
} else {
this.storageService.remove(AbstractGotoLineQuickAccessProvider.AUTO_REVEAL_CONFIG_SNAPSHOT_STORAGE_KEY, StorageScope.APPLICATION);
}
}

private clearPersistedAutoRevealPreference(): void {
this.storageService.remove(AbstractGotoLineQuickAccessProvider.DISABLE_AUTO_REVEAL_STORAGE_KEY, StorageScope.APPLICATION);
this.storageService.remove(AbstractGotoLineQuickAccessProvider.AUTO_REVEAL_CONFIG_SNAPSHOT_STORAGE_KEY, StorageScope.APPLICATION);
}

protected provideWithoutTextEditor(picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>): IDisposable {
const label = localize('gotoLine.noEditor', "Open a text editor first to go to a line or an offset.");

Expand All @@ -57,22 +157,25 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
}

protected provideWithTextEditor(context: IQuickAccessTextEditorContext, picker: IQuickPick<IGotoLineQuickPickItem, { useSeparators: true }>, token: CancellationToken): IDisposable {
this.sessionDisableAutoReveal = undefined;
const editor = context.editor;
const disposables = new DisposableStore();
disposables.add(toDisposable(() => {
this.sessionDisableAutoReveal = undefined;
}));

// Goto line once picked
disposables.add(picker.onDidAccept(event => {
const [item] = picker.selectedItems;
if (item) {
if (!item.lineNumber) {
return;
}
const item = picker.selectedItems[0];
if (!item || !item.lineNumber) {
return;
}

this.gotoLocation(context, { range: this.toRange(item.lineNumber, item.column), keyMods: picker.keyMods, preserveFocus: event.inBackground });
const range = this.toRange(item.lineNumber, item.column);
this.gotoLocation(context, { range, keyMods: picker.keyMods, preserveFocus: event.inBackground });

if (!event.inBackground) {
picker.hide();
}
if (!event.inBackground) {
picker.hide();
}
}));

Expand All @@ -84,13 +187,24 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
toggle: { checked: this.useZeroBasedOffset }
};

const autoRevealButton: IQuickInputButton = {
iconClass: ThemeIcon.asClassName(Codicon.eye),
tooltip: localize('gotoLineAutoRevealToggleButton', "Toggle Auto Reveal"),
location: QuickInputButtonLocation.Inline,
secondary: true,
toggle: { checked: !this.disableAutoReveal }
};

// React to picker changes
const updatePickerAndEditor = () => {
const inputText = picker.value.trim().substring(AbstractGotoLineQuickAccessProvider.GO_TO_LINE_PREFIX.length);
const { inOffsetMode, lineNumber, column, label } = this.parsePosition(editor, inputText);

// Show toggle only when input text starts with '::'.
picker.buttons = inOffsetMode ? [offsetButton] : [];
const buttons: IQuickInputButton[] = [autoRevealButton];
if (inOffsetMode) {
buttons.push(offsetButton);
}
picker.buttons = buttons;

// Picker
picker.items = [{
Expand All @@ -105,18 +219,26 @@ export abstract class AbstractGotoLineQuickAccessProvider extends AbstractEditor
return;
}

// Reveal
const range = this.toRange(lineNumber, column);
editor.revealRangeInCenter(range, ScrollType.Smooth);

// Decorate
// Always update decorations so the target line is highlighted
this.addDecorations(editor, range);

if (this.disableAutoReveal) {
return;
}

// Reveal only when auto reveal is enabled
editor.revealRangeInCenter(range, ScrollType.Smooth);
};

disposables.add(picker.onDidTriggerButton(button => {
if (button === offsetButton) {
this.useZeroBasedOffset = button.toggle?.checked ?? !this.useZeroBasedOffset;
updatePickerAndEditor();
} else if (button === autoRevealButton) {
this.disableAutoReveal = !(button.toggle?.checked ?? !this.disableAutoReveal);
updatePickerAndEditor();
}
}));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/tes
import { IEditor } from '../../../../common/editorCommon.js';
import { withTestCodeEditor } from '../../../../test/browser/testCodeEditor.js';
import { AbstractGotoLineQuickAccessProvider } from '../../browser/gotoLineQuickAccess.js';
import { IStorageService } from '../../../../../platform/storage/common/storage.js';
import { IStorageService, InMemoryStorageService } from '../../../../../platform/storage/common/storage.js';

class TestGotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvider {
protected override onDidActiveTextEditorControlChange = Event.None;
Expand All @@ -27,6 +27,45 @@ class TestGotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProvide
}
}

class TestAutoRevealPreferenceProvider extends AbstractGotoLineQuickAccessProvider {
protected override onDidActiveTextEditorControlChange = Event.None;
protected override activeTextEditorControl: IEditor | undefined;

private configuredValue: boolean | undefined;

constructor(private readonly storageImpl: InMemoryStorageService = new InMemoryStorageService()) {
super();
}

setConfiguredValue(value: boolean | undefined): void {
this.configuredValue = value;
}

getAutoRevealDisabledForTest(): boolean {
return Reflect.get(this as object, 'disableAutoReveal') as boolean;
}

setAutoRevealDisabledForTest(value: boolean): void {
Reflect.set(this as object, 'disableAutoReveal', value);
}

triggerConfigurationChangedForTest(): void {
this.onAutoRevealConfigurationChanged();
}

protected override get storageService(): IStorageService {
return this.storageImpl;
}

protected override getConfiguredAutoReveal(): boolean | undefined {
return this.configuredValue;
}

protected override isAutoRevealEnabledByDefault(): boolean {
return true;
}
}

suite('AbstractGotoLineQuickAccessProvider', () => {
ensureNoDisposablesAreLeakedInTestSuite();

Expand Down Expand Up @@ -135,5 +174,36 @@ suite('AbstractGotoLineQuickAccessProvider', () => {
runTabTest('2:9', 2, 3);
runTabTest('2:11', 2, 5);
});

test('auto reveal preference persists across sessions even when configured', () => {
const storage = new InMemoryStorageService();
try {
const providerA = new TestAutoRevealPreferenceProvider(storage);
providerA.setConfiguredValue(true);
assert.strictEqual(providerA.getAutoRevealDisabledForTest(), false);
providerA.setAutoRevealDisabledForTest(true);
const providerB = new TestAutoRevealPreferenceProvider(storage);
providerB.setConfiguredValue(true);
assert.strictEqual(providerB.getAutoRevealDisabledForTest(), true);
} finally {
storage.dispose();
}
});

test('configuration changes override persisted preference', () => {
const storage = new InMemoryStorageService();
try {
const providerA = new TestAutoRevealPreferenceProvider(storage);
providerA.setConfiguredValue(undefined);
providerA.setAutoRevealDisabledForTest(false);
providerA.setConfiguredValue(false);
providerA.triggerConfigurationChangedForTest();
const providerB = new TestAutoRevealPreferenceProvider(storage);
providerB.setConfiguredValue(false);
assert.strictEqual(providerB.getAutoRevealDisabledForTest(), true);
} finally {
storage.dispose();
}
});
});

Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv
) {
super();
this.onDidActiveTextEditorControlChange = this.editorService.onDidActiveEditorChange;
this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('editor.gotoLine.autoReveal')) {
this.onAutoRevealConfigurationChanged();
}
});
}

private get configuration() {
Expand All @@ -48,6 +53,16 @@ export class GotoLineQuickAccessProvider extends AbstractGotoLineQuickAccessProv
return this.editorService.activeTextEditorControl;
}

protected override isAutoRevealEnabledByDefault(): boolean {
const value = this.configurationService.getValue<boolean>('editor.gotoLine.autoReveal');
return value ?? true;
}

protected override getConfiguredAutoReveal(): boolean | undefined {
const inspect = this.configurationService.inspect<boolean>('editor.gotoLine.autoReveal');
return inspect.memoryValue ?? inspect.workspaceFolderValue ?? inspect.workspaceValue ?? inspect.userRemoteValue ?? inspect.userLocalValue ?? inspect.userValue ?? inspect.applicationValue;
}

protected override gotoLocation(context: IQuickAccessTextEditorContext, options: { range: IRange; keyMods: IKeyMods; forceSideBySide?: boolean; preserveFocus?: boolean }): void {

// Check for sideBySide use
Expand Down