diff --git a/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts b/src/vs/workbench/contrib/preferences/browser/preferences.contribution.ts index 006b15f7d6e9d7..e6e3bf1feeb063 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 499f9e6c73fe49..9e59cab310e806 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -678,6 +678,52 @@ 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 { + 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._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)) { + target = next ? navigator.next() : navigator.previous(); + } + if (!(target instanceof SettingsTreeSettingElement)) { + return; + } + + // 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(`.monaco-list-row.focused ${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 @@ -705,7 +751,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(); } /**