Skip to content

Commit f3a8b62

Browse files
authored
Merge pull request #189575 from microsoft/merogge/go-to-symbol-accessible-view
2 parents 2d039ca + a1f77aa commit f3a8b62

1 file changed

Lines changed: 122 additions & 4 deletions

File tree

src/vs/workbench/contrib/accessibility/browser/accessibleView.ts

Lines changed: 122 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ import { AccessibilityVerbositySettingId, accessibilityHelpIsShown, accessibleVi
2929
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
3030
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
3131
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
32-
32+
import { IQuickInputService } from 'vs/platform/quickinput/common/quickInput';
33+
import { IPickerQuickAccessItem } from 'vs/platform/quickinput/browser/pickerQuickAccess';
34+
import { marked } from 'vs/base/common/marked/marked';
3335

3436
const enum DIMENSIONS {
3537
MAX_WIDTH = 600
@@ -42,6 +44,10 @@ export interface IAccessibleContentProvider {
4244
onKeyDown?(e: IKeyboardEvent): void;
4345
previous?(): void;
4446
next?(): void;
47+
/**
48+
* When the language is markdown, this is provided by default.
49+
*/
50+
getSymbols?(): IAccessibleViewSymbol[];
4551
options: IAccessibleViewOptions;
4652
}
4753

@@ -52,6 +58,7 @@ export interface IAccessibleViewService {
5258
show(provider: IAccessibleContentProvider): void;
5359
next(): void;
5460
previous(): void;
61+
goToSymbol(): void;
5562
/**
5663
* If the setting is enabled, provides the open accessible view hint as a localized string.
5764
* @param verbositySettingKey The setting key for the verbosity of the feature
@@ -128,15 +135,21 @@ class AccessibleView extends Disposable {
128135
}));
129136
}
130137

131-
show(provider: IAccessibleContentProvider): void {
138+
show(provider?: IAccessibleContentProvider, symbol?: IAccessibleViewSymbol): void {
139+
if (!provider) {
140+
provider = this._currentProvider;
141+
}
142+
if (!provider) {
143+
return;
144+
}
132145
const delegate: IContextViewDelegate = {
133146
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 }; },
134147
render: (container) => {
135148
container.classList.add('accessible-view-container');
136-
return this._render(provider, container);
149+
return this._render(provider!, container);
137150
},
138151
onHide: () => {
139-
if (provider.options.type === AccessibleViewType.Help) {
152+
if (provider!.options.type === AccessibleViewType.Help) {
140153
this._accessiblityHelpIsShown.reset();
141154
} else {
142155
this._accessibleViewIsShown.reset();
@@ -150,6 +163,9 @@ class AccessibleView extends Disposable {
150163
} else {
151164
this._accessibleViewIsShown.set(true);
152165
}
166+
if (symbol && this._currentProvider) {
167+
this.showSymbol(this._currentProvider, symbol);
168+
}
153169
this._currentProvider = provider;
154170
}
155171

@@ -167,6 +183,51 @@ class AccessibleView extends Disposable {
167183
this._currentProvider.next?.();
168184
}
169185

186+
goToSymbol(): void {
187+
this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider!);
188+
}
189+
190+
getSymbols(): IAccessibleViewSymbol[] | undefined {
191+
if (!this._currentProvider) {
192+
return;
193+
}
194+
const tokens = this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown' ? this._currentProvider.getSymbols?.() : marked.lexer(this._currentProvider.provideContent());
195+
if (!tokens) {
196+
return;
197+
}
198+
const symbols: IAccessibleViewSymbol[] = [];
199+
for (const token of tokens) {
200+
let label: string | undefined = undefined;
201+
if ('type' in token) {
202+
switch (token.type) {
203+
case 'heading':
204+
case 'paragraph':
205+
case 'code':
206+
label = token.text;
207+
break;
208+
case 'list':
209+
label = token.items?.map(i => i.text).join(', ');
210+
break;
211+
}
212+
} else {
213+
label = token.label;
214+
}
215+
if (label) {
216+
symbols.push({ info: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label) });
217+
}
218+
}
219+
return symbols;
220+
}
221+
222+
showSymbol(provider: IAccessibleContentProvider, symbol: IAccessibleViewSymbol): void {
223+
const index = provider.provideContent().split('\n').findIndex(line => line.includes(symbol.info.split('\n')[0])) ?? -1;
224+
if (index >= 0) {
225+
this.show(provider);
226+
this._editorWidget.revealLine(index + 1);
227+
this._editorWidget.setSelection({ startLineNumber: index + 1, startColumn: 1, endLineNumber: index + 1, endColumn: 1 });
228+
}
229+
}
230+
170231
private _render(provider: IAccessibleContentProvider, container: HTMLElement): IDisposable {
171232
this._currentProvider = provider;
172233
const settingKey = `accessibility.verbosity.${provider.verbositySettingKey}`;
@@ -286,6 +347,9 @@ export class AccessibleViewService extends Disposable implements IAccessibleView
286347
previous(): void {
287348
this._accessibleView?.previous();
288349
}
350+
goToSymbol(): void {
351+
this._accessibleView?.goToSymbol();
352+
}
289353
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
290354
if (!this._configurationService.getValue(verbositySettingKey)) {
291355
return null;
@@ -321,6 +385,30 @@ class AccessibleViewNextAction extends Action2 {
321385
registerAction2(AccessibleViewNextAction);
322386

323387

388+
class AccessibleViewGoToSymbolAction extends Action2 {
389+
static id: 'editor.action.accessibleViewGoToSymbol';
390+
constructor() {
391+
super({
392+
id: 'editor.action.accessibleViewGoToSymbol',
393+
precondition: accessibleViewIsShown,
394+
keybinding: {
395+
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyO,
396+
weight: KeybindingWeight.WorkbenchContrib + 10
397+
},
398+
menu: [{
399+
id: MenuId.CommandPalette,
400+
group: '',
401+
order: 1
402+
}],
403+
title: localize('editor.action.accessibleViewGoToSymbol', "Go To Symbol in Accessible View")
404+
});
405+
}
406+
run(accessor: ServicesAccessor, ...args: unknown[]): void {
407+
accessor.get(IAccessibleViewService).goToSymbol();
408+
}
409+
}
410+
registerAction2(AccessibleViewGoToSymbolAction);
411+
324412
class AccessibleViewPreviousAction extends Action2 {
325413
static id: 'editor.action.accessibleViewPrevious';
326414
constructor() {
@@ -389,3 +477,33 @@ export const AccessibleViewAction = registerCommand(new MultiCommand({
389477
}],
390478
}));
391479

480+
481+
class AccessibleViewSymbolQuickPick {
482+
constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) {
483+
484+
}
485+
show(provider: IAccessibleContentProvider): void {
486+
const quickPick = this._quickInputService.createQuickPick<IAccessibleViewSymbol>();
487+
const picks = [];
488+
const symbols = this._accessibleView.getSymbols();
489+
if (!symbols) {
490+
return;
491+
}
492+
for (const symbol of symbols) {
493+
picks.push({
494+
label: symbol.label,
495+
ariaLabel: symbol.ariaLabel
496+
});
497+
}
498+
quickPick.canSelectMany = false;
499+
quickPick.items = symbols;
500+
quickPick.show();
501+
quickPick.onDidAccept(() => {
502+
this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]);
503+
});
504+
}
505+
}
506+
507+
interface IAccessibleViewSymbol extends IPickerQuickAccessItem {
508+
info: string;
509+
}

0 commit comments

Comments
 (0)