From 8ceb04651afcf58f28771ffddb468bf51d6d41cb Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:20:59 +0200 Subject: [PATCH 1/6] Settings editor - add focus next/previous setting commands Add settings.action.focusNextSetting (F7) and settings.action.focusPreviousSetting (Shift+F7), which move focus directly to the next/previous setting's value control, skipping group headers. They work no matter whether focus is in the search input, on a tree row, or inside a setting's control, so pressing the shortcut N times from the top reaches the Nth setting's control. Navigation walks the tree model rather than the DOM since the tree is virtualized; the target row is revealed first so its control can be focused (same pattern as revealing a setting link target). Fixes #52815 --- .../browser/preferences.contribution.ts | 46 +++++++++++++++++++ .../preferences/browser/settingsEditor2.ts | 42 +++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 006b15f7d6e9d..e6e3bf1feeb06 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts @@ -59,6 +59,8 @@ const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH = 'settings.action.focu const SETTINGS_EDITOR_COMMAND_SHOW_PREVIOUS_SEARCH = 'settings.action.showPreviousSearch'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_FROM_SEARCH_ON_ENTER = 'settings.action.focusSettingsFromSearchOnEnter'; const SETTINGS_EDITOR_COMMAND_FOCUS_SEARCH_FROM_SETTINGS = 'settings.action.focusSearchFromSettings'; +const SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING = 'settings.action.focusNextSetting'; +const SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING = 'settings.action.focusPreviousSetting'; const SETTINGS_EDITOR_COMMAND_FOCUS_SETTINGS_LIST = 'settings.action.focusSettingsList'; const SETTINGS_EDITOR_COMMAND_FOCUS_TOC = 'settings.action.focusTOC'; const SETTINGS_EDITOR_COMMAND_FOCUS_CONTROL = 'settings.action.focusSettingControl'; @@ -742,6 +744,50 @@ class PreferencesActionsContribution extends Disposable implements IWorkbenchCon } })); + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_NEXT_SETTING, + precondition: CONTEXT_SETTINGS_EDITOR, + keybinding: { + primary: KeyCode.F7, + weight: KeybindingWeight.WorkbenchContrib, + when: null + }, + f1: true, + category, + title: nls.localize2('settings.focusNextSetting', "Focus Next Setting") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + preferencesEditor?.focusNextSetting(); + } + })); + + this._register(registerAction2(class extends Action2 { + constructor() { + super({ + id: SETTINGS_EDITOR_COMMAND_FOCUS_PREVIOUS_SETTING, + precondition: CONTEXT_SETTINGS_EDITOR, + keybinding: { + primary: KeyMod.Shift | KeyCode.F7, + weight: KeybindingWeight.WorkbenchContrib, + when: null + }, + f1: true, + category, + title: nls.localize2('settings.focusPreviousSetting', "Focus Previous Setting") + }); + } + + run(accessor: ServicesAccessor): void { + const preferencesEditor = getPreferencesEditor(accessor); + preferencesEditor?.focusPreviousSetting(); + } + })); + this._register(registerAction2(class extends Action2 { constructor() { super({ diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 499f9e6c73fe4..2db386effe9e7 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -678,6 +678,48 @@ export class SettingsEditor2 extends EditorPane { this.tocTree.domFocus(); } + /** + * Moves focus to the next setting's control, skipping group headers. Works no matter + * whether focus is currently in the search input, on a tree row, or inside a setting's + * control. Stops at the last setting. + */ + focusNextSetting(): void { + this.focusAdjacentSetting(true); + } + + /** + * Like {@link focusNextSetting}, but backwards. Stops at the first setting. + */ + focusPreviousSetting(): void { + this.focusAdjacentSetting(false); + } + + private focusAdjacentSetting(next: boolean): void { + // The tree's focus tracks the setting whose control contains DOM focus, so this + // anchor is correct even while focus is inside a control. It is empty when no + // setting has been focused yet, in which case navigation starts from the top, + // or from the bottom when navigating backwards. + const anchor = this.settingsTree.getFocus()[0]; + const navigator = this.settingsTree.navigate(anchor); + let target = !anchor && !next ? navigator.last() : next ? navigator.next() : navigator.previous(); + while (target && !(target instanceof SettingsTreeSettingElement)) { + target = next ? navigator.next() : navigator.previous(); + } + if (!(target instanceof SettingsTreeSettingElement)) { + return; + } + + // Reveal renders the row synchronously so its control can be queried below. + this.settingsTree.reveal(target); + this.settingsTree.setFocus([target]); + const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), target.setting.key); + // eslint-disable-next-line no-restricted-syntax + const control = domElements[0]?.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); + if (control) { + (control).focus(); + } + } + /** * Invoked when the user presses the down arrow while the search input is focused. * Navigates forward through the search history first; only once there are no more From 94a76d2263ef8c81a9d776ea985a729d097d65c5 Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:35:48 +0200 Subject: [PATCH 2/6] Settings editor - fix isSearchUpToDate never calling isTriggered Delayer.isTriggered is a method; referencing it without calling it made the expression always truthy, so isSearchUpToDate() always returned false and focusing the first setting from search never ran. --- src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 2db386effe9e7..deada21458aed 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -747,7 +747,7 @@ export class SettingsEditor2 extends EditorPane { * focus is not moved into stale results. */ private isSearchUpToDate(): boolean { - return !this.searchInputDelayer.isTriggered && this.renderedSearchQuery === this.searchWidget.getValue().trim(); + return !this.searchInputDelayer.isTriggered() && this.renderedSearchQuery === this.searchWidget.getValue().trim(); } /** From e8a61fa70f6e88b4863bccc80fd0011d21d3f58e Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:35:49 +0200 Subject: [PATCH 3/6] Settings editor - address review feedback for focus next/previous setting Guard the navigation behind isSearchUpToDate() so a pending search cannot be navigated stale, query the focused row's control instead of looking it up by setting key (which can match a Commonly Used duplicate), and trim the anchor comment to one line. --- .../contrib/preferences/browser/settingsEditor2.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index deada21458aed..9da071e6ea179 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -695,10 +695,11 @@ export class SettingsEditor2 extends EditorPane { } private focusAdjacentSetting(next: boolean): void { - // The tree's focus tracks the setting whose control contains DOM focus, so this - // anchor is correct even while focus is inside a control. It is empty when no - // setting has been focused yet, in which case navigation starts from the top, - // or from the bottom when navigating backwards. + if (!this.isSearchUpToDate()) { + return; + } + + // The tree's focus tracks the setting whose control contains DOM focus, so the anchor is valid even while a control is focused. const anchor = this.settingsTree.getFocus()[0]; const navigator = this.settingsTree.navigate(anchor); let target = !anchor && !next ? navigator.last() : next ? navigator.next() : navigator.previous(); @@ -709,12 +710,11 @@ export class SettingsEditor2 extends EditorPane { return; } - // Reveal renders the row synchronously so its control can be queried below. + // Reveal renders the row synchronously so its control can be focused below. this.settingsTree.reveal(target); this.settingsTree.setFocus([target]); - const domElements = this.settingRenderers.getDOMElementsForSettingKey(this.settingsTree.getHTMLElement(), target.setting.key); // eslint-disable-next-line no-restricted-syntax - const control = domElements[0]?.querySelector(AbstractSettingRenderer.CONTROL_SELECTOR); + const control = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); if (control) { (control).focus(); } From d4522676c50376bc7eca219c43f0141f94f550fa Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:46:57 +0200 Subject: [PATCH 4/6] Settings editor - look up the target control by setting element id The previously focused setting's container keeps its focused class until DOM focus moves, so a global .focused selector could match the old control first when navigating forward. Scope the lookup to the target row via its data-id, which is also unique for Commonly Used duplicates. --- src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index 9da071e6ea179..ea7127bf0c549 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -714,7 +714,7 @@ export class SettingsEditor2 extends EditorPane { this.settingsTree.reveal(target); this.settingsTree.setFocus([target]); // eslint-disable-next-line no-restricted-syntax - const control = this.settingsTree.getHTMLElement().querySelector(`.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); + const control = this.settingsTree.getHTMLElement().querySelector(`[${AbstractSettingRenderer.SETTING_ID_ATTR}="${target.id}"] ${AbstractSettingRenderer.CONTROL_SELECTOR}`); if (control) { (control).focus(); } From bc0e9d16678af27439f3d2baa39f7ea8817c0e68 Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 18:58:04 +0200 Subject: [PATCH 5/6] Settings editor - only anchor navigation on tree or control focus The settings tree retains its focus when the user moves to the search input or the TOC, so navigation would continue from the previously visited setting. Start from the first/last setting instead unless focus is in the settings tree or a setting control. --- .../workbench/contrib/preferences/browser/settingsEditor2.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index ea7127bf0c549..a5166fa873173 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -700,7 +700,9 @@ export class SettingsEditor2 extends EditorPane { } // The tree's focus tracks the setting whose control contains DOM focus, so the anchor is valid even while a control is focused. - const anchor = this.settingsTree.getFocus()[0]; + const anchor = this._currentFocusContext === SettingsFocusContext.SettingTree || this._currentFocusContext === SettingsFocusContext.SettingControl + ? this.settingsTree.getFocus()[0] + : undefined; const navigator = this.settingsTree.navigate(anchor); let target = !anchor && !next ? navigator.last() : next ? navigator.next() : navigator.previous(); while (target && !(target instanceof SettingsTreeSettingElement)) { From a960d53ad65dcd2cbd0ad687d36252d089beb14b Mon Sep 17 00:00:00 2001 From: soreavis <263610811+soreavis@users.noreply.github.com> Date: Fri, 17 Jul 2026 19:10:58 +0200 Subject: [PATCH 6/6] Settings editor - select the focused row control without a key selector Interpolating the setting id into the selector could throw for a key containing a quote. Use the list's focused-row trait class, which targets a single row (avoiding the stale focused container on the previously visited setting) without a dynamic attribute value. --- .../workbench/contrib/preferences/browser/settingsEditor2.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index a5166fa873173..9e59cab310e80 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -715,8 +715,10 @@ export class SettingsEditor2 extends EditorPane { // Reveal renders the row synchronously so its control can be focused below. this.settingsTree.reveal(target); this.settingsTree.setFocus([target]); + // The row selector avoids matching the previously focused setting, whose container + // keeps a stale focused class until DOM focus moves away from its control. // eslint-disable-next-line no-restricted-syntax - const control = this.settingsTree.getHTMLElement().querySelector(`[${AbstractSettingRenderer.SETTING_ID_ATTR}="${target.id}"] ${AbstractSettingRenderer.CONTROL_SELECTOR}`); + const control = this.settingsTree.getHTMLElement().querySelector(`.monaco-list-row.focused ${AbstractSettingRenderer.CONTROL_SELECTOR}`); if (control) { (control).focus(); }