diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index 118c12d3f23..976563aaafa 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -29,7 +29,9 @@ import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleVi import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; - +import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput'; +import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess'; +import { marked } from 'vs/base/common/marked/marked'; const enum DIMENSIONS { MAX_WIDTH = 600 @@ -42,6 +44,10 @@ export interface IAccessibleContentProvider { onKeyDown?(e: IKeyboardEvent): void; previous?(): void; next?(): void; + /** + * When the language is markdown, this is provided by default. + */ + getSymbols?(): IAccessibleViewSymbol[]; options: IAccessibleViewOptions; } @@ -52,6 +58,7 @@ export interface IAccessibleViewService { show(provider: IAccessibleContentProvider): void; next(): void; previous(): void; + goToSymbol(): void; /** * If the setting is enabled, provides the open accessible view hint as a localized string. * @param verbositySettingKey The setting key for the verbosity of the feature @@ -128,15 +135,21 @@ class AccessibleView extends Disposable { })); } - show(provider: IAccessibleContentProvider): void { + show(provider?: IAccessibleContentProvider, symbol?: IAccessibleViewSymbol): void { + if (!provider) { + provider = this._currentProvider; + } + if (!provider) { + return; + } const delegate: IContextViewDelegate = { getAnchor: () => { return { x: (window.innerWidth / 2) - ((Math.min(this._layoutService.dimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.offset.quickPickTop }; }, render: (container) => { container.classList.add('accessible-view-container'); - return this._render(provider, container); + return this._render(provider!, container); }, onHide: () => { - if (provider.options.type === AccessibleViewType.Help) { + if (provider!.options.type === AccessibleViewType.Help) { this._accessiblityHelpIsShown.reset(); } else { this._accessibleViewIsShown.reset(); @@ -150,6 +163,9 @@ class AccessibleView extends Disposable { } else { this._accessibleViewIsShown.set(true); } + if (symbol && this._currentProvider) { + this.showSymbol(this._currentProvider, symbol); + } this._currentProvider = provider; } @@ -167,6 +183,51 @@ class AccessibleView extends Disposable { this._currentProvider.next?.(); } + goToSymbol(): void { + this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider!); + } + + getSymbols(): IAccessibleViewSymbol[] | undefined { + if (!this._currentProvider) { + return; + } + const tokens = this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown' ? this._currentProvider.getSymbols?.() : marked.lexer(this._currentProvider.provideContent()); + if (!tokens) { + return; + } + const symbols: IAccessibleViewSymbol[] = []; + for (const token of tokens) { + let label: string | undefined = undefined; + if ('type' in token) { + switch (token.type) { + case 'heading': + case 'paragraph': + case 'code': + label = token.text; + break; + case 'list': + label = token.items?.map(i => i.text).join(', '); + break; + } + } else { + label = token.label; + } + if (label) { + symbols.push({ info: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label) }); + } + } + return symbols; + } + + showSymbol(provider: IAccessibleContentProvider, symbol: IAccessibleViewSymbol): void { + const index = provider.provideContent().split('\n').findIndex(line => line.includes(symbol.info.split('\n')[0])) ?? -1; + if (index >= 0) { + this.show(provider); + this._editorWidget.revealLine(index + 1); + this._editorWidget.setSelection({ startLineNumber: index + 1, startColumn: 1, endLineNumber: index + 1, endColumn: 1 }); + } + } + private _render(provider: IAccessibleContentProvider, container: HTMLElement): IDisposable { this._currentProvider = provider; const settingKey = `accessibility.verbosity.${provider.verbositySettingKey}`; @@ -286,6 +347,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView previous(): void { this._accessibleView?.previous(); } + goToSymbol(): void { + this._accessibleView?.goToSymbol(); + } getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null { if (!this._configurationService.getValue(verbositySettingKey)) { return null; @@ -321,6 +385,30 @@ class AccessibleViewNextAction extends Action2 { registerAction2(AccessibleViewNextAction); +class AccessibleViewGoToSymbolAction extends Action2 { + static id: 'editor.action.accessibleViewGoToSymbol'; + constructor() { + super({ + id: 'editor.action.accessibleViewGoToSymbol', + precondition: accessibleViewIsShown, + keybinding: { + primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO, + weight: KeybindingWeight.WorkbenchContrib + 10 + }, + menu: [{ + id: MenuId.CommandPalette, + group: '', + order: 1 + }], + title: localize('editor.action.accessibleViewGoToSymbol', "Go To Symbol in Accessible View") + }); + } + run(accessor: ServicesAccessor, ...args: unknown[]): void { + accessor.get(IAccessibleViewService).goToSymbol(); + } +} +registerAction2(AccessibleViewGoToSymbolAction); + class AccessibleViewPreviousAction extends Action2 { static id: 'editor.action.accessibleViewPrevious'; constructor() { @@ -389,3 +477,33 @@ export const AccessibleViewAction = registerCommand(new MultiCommand({ }], })); + +class AccessibleViewSymbolQuickPick { + constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) { + + } + show(provider: IAccessibleContentProvider): void { + const quickPick = this._quickInputService.createQuickPick(); + const picks = []; + const symbols = this._accessibleView.getSymbols(); + if (!symbols) { + return; + } + for (const symbol of symbols) { + picks.push({ + label: symbol.label, + ariaLabel: symbol.ariaLabel + }); + } + quickPick.canSelectMany = false; + quickPick.items = symbols; + quickPick.show(); + quickPick.onDidAccept(() => { + this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]); + }); + } +} + +interface IAccessibleViewSymbol extends IPickerQuickAccessItem { + info: string; +}