Skip to content
Merged
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
126 changes: 122 additions & 4 deletions src/vs/workbench/contrib/accessibility/browser/accessibleView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

Expand All @@ -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
Expand Down Expand Up @@ -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();
Expand All @@ -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;
}

Expand All @@ -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}`;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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<IAccessibleViewSymbol>();
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]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't going to dismiss the quick pick. Is that expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

accepting one does because of the focus change. escaping the quick pick wasn't handled, but now is #189658

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a setting called ignoreFocusOut that won't close the quick pick when focus is lost.

As a result this qp won't go away with that setting. I recommend explicitly closing it

});
}
}

interface IAccessibleViewSymbol extends IPickerQuickAccessItem {
info: string;
}