Path: blob/main/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { EventType, addDisposableListener, getActiveWindow, isActiveElement } from '../../../../base/browser/dom.js';6import { IKeyboardEvent, StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { ActionsOrientation } from '../../../../base/browser/ui/actionbar/actionbar.js';8import { alert } from '../../../../base/browser/ui/aria/aria.js';9import { IAction } from '../../../../base/common/actions.js';10import { Codicon } from '../../../../base/common/codicons.js';11import { KeyCode } from '../../../../base/common/keyCodes.js';12import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';13import * as marked from '../../../../base/common/marked/marked.js';14import { Schemas } from '../../../../base/common/network.js';15import { isMacintosh, isWindows } from '../../../../base/common/platform.js';16import { ThemeIcon } from '../../../../base/common/themables.js';17import { URI } from '../../../../base/common/uri.js';18import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';19import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';20import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';21import { IPosition, Position } from '../../../../editor/common/core/position.js';22import { ITextModel } from '../../../../editor/common/model.js';23import { IModelService } from '../../../../editor/common/services/model.js';24import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js';25import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js';26import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js';27import { localize } from '../../../../nls.js';28import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js';29import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';30import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';31import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';32import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';33import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';34import { ICommandService } from '../../../../platform/commands/common/commands.js';35import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';36import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';37import { IContextViewDelegate, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';38import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';39import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';40import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';41import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';42import { IOpenerService } from '../../../../platform/opener/common/opener.js';43import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';44import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';45import { FloatingEditorClickMenu } from '../../../browser/codeeditor.js';46import { IChatCodeBlockContextProviderService } from '../../chat/browser/chat.js';47import { ICodeBlockActionContext } from '../../chat/browser/codeBlockPart.js';48import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';49import { AccessibilityCommandId } from '../common/accessibilityCommands.js';50import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from './accessibilityConfiguration.js';51import { resolveContentAndKeybindingItems } from './accessibleViewKeybindingResolver.js';5253const enum DIMENSIONS {54MAX_WIDTH = 60055}5657export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider;5859interface ICodeBlock {60startLine: number;61endLine: number;62code: string;63languageId?: string;64chatSessionId: string | undefined;65}6667export class AccessibleView extends Disposable implements ITextModelContentProvider {68private _editorWidget: CodeEditorWidget;6970private _accessiblityHelpIsShown: IContextKey<boolean>;71private _onLastLine: IContextKey<boolean>;72private _accessibleViewIsShown: IContextKey<boolean>;73private _accessibleViewSupportsNavigation: IContextKey<boolean>;74private _accessibleViewVerbosityEnabled: IContextKey<boolean>;75private _accessibleViewGoToSymbolSupported: IContextKey<boolean>;76private _accessibleViewCurrentProviderId: IContextKey<string>;77private _accessibleViewInCodeBlock: IContextKey<boolean>;78private _accessibleViewContainsCodeBlocks: IContextKey<boolean>;79private _hasUnassignedKeybindings: IContextKey<boolean>;80private _hasAssignedKeybindings: IContextKey<boolean>;8182private _codeBlocks?: ICodeBlock[];83private _isInQuickPick: boolean = false;8485get editorWidget() { return this._editorWidget; }86private _container: HTMLElement;87private _title: HTMLElement;88private readonly _toolbar: WorkbenchToolBar;8990private _currentProvider: AccesibleViewContentProvider | undefined;91private _currentContent: string | undefined;9293private _lastProvider: AccesibleViewContentProvider | undefined;9495private _viewContainer: HTMLElement | undefined;969798constructor(99@IOpenerService private readonly _openerService: IOpenerService,100@IInstantiationService private readonly _instantiationService: IInstantiationService,101@IConfigurationService private readonly _configurationService: IConfigurationService,102@IModelService private readonly _modelService: IModelService,103@IContextViewService private readonly _contextViewService: IContextViewService,104@IContextKeyService private readonly _contextKeyService: IContextKeyService,105@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,106@IKeybindingService private readonly _keybindingService: IKeybindingService,107@ILayoutService private readonly _layoutService: ILayoutService,108@IMenuService private readonly _menuService: IMenuService,109@ICommandService private readonly _commandService: ICommandService,110@IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService,111@IStorageService private readonly _storageService: IStorageService,112@ITextModelService private readonly textModelResolverService: ITextModelService,113@IQuickInputService private readonly _quickInputService: IQuickInputService,114@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,115) {116super();117118this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService);119this._accessibleViewIsShown = accessibleViewIsShown.bindTo(this._contextKeyService);120this._accessibleViewSupportsNavigation = accessibleViewSupportsNavigation.bindTo(this._contextKeyService);121this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService);122this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService);123this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService);124this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService);125this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService);126this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService);127this._hasUnassignedKeybindings = accessibleViewHasUnassignedKeybindings.bindTo(this._contextKeyService);128this._hasAssignedKeybindings = accessibleViewHasAssignedKeybindings.bindTo(this._contextKeyService);129130this._container = document.createElement('div');131this._container.classList.add('accessible-view');132if (this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView)) {133this._container.classList.add('hide');134}135const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {136contributions: EditorExtensionsRegistry.getEditorContributions().filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID)137};138const titleBar = document.createElement('div');139titleBar.classList.add('accessible-view-title-bar');140this._title = document.createElement('div');141this._title.classList.add('accessible-view-title');142titleBar.appendChild(this._title);143const actionBar = document.createElement('div');144actionBar.classList.add('accessible-view-action-bar');145titleBar.appendChild(actionBar);146this._container.appendChild(titleBar);147this._toolbar = this._register(_instantiationService.createInstance(WorkbenchToolBar, actionBar, { orientation: ActionsOrientation.HORIZONTAL }));148this._toolbar.context = { viewId: 'accessibleView' };149const toolbarElt = this._toolbar.getElement();150toolbarElt.tabIndex = 0;151152const editorOptions: IEditorConstructionOptions = {153...getSimpleEditorOptions(this._configurationService),154lineDecorationsWidth: 6,155dragAndDrop: false,156cursorWidth: 1,157wordWrap: 'off',158wrappingStrategy: 'advanced',159wrappingIndent: 'none',160padding: { top: 2, bottom: 2 },161quickSuggestions: false,162renderWhitespace: 'none',163dropIntoEditor: { enabled: false },164readOnly: true,165fontFamily: 'var(--monaco-monospace-font)'166};167this.textModelResolverService.registerTextModelContentProvider(Schemas.accessibleView, this);168169this._editorWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._container, editorOptions, codeEditorWidgetOptions));170this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {171if (this._currentProvider && this._accessiblityHelpIsShown.get()) {172this.show(this._currentProvider);173}174}));175this._register(this._configurationService.onDidChangeConfiguration(e => {176if (isIAccessibleViewContentProvider(this._currentProvider) && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) {177if (this._accessiblityHelpIsShown.get()) {178this.show(this._currentProvider);179}180this._accessibleViewVerbosityEnabled.set(this._configurationService.getValue(this._currentProvider.verbositySettingKey));181this._updateToolbar(this._currentProvider.actions, this._currentProvider.options.type);182}183if (e.affectsConfiguration(AccessibilityWorkbenchSettingId.HideAccessibleView)) {184this._container.classList.toggle('hide', this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView));185}186}));187this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys()));188this._register(this._editorWidget.onDidChangeCursorPosition(() => {189this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount());190const cursorPosition = this._editorWidget.getPosition()?.lineNumber;191if (this._codeBlocks && cursorPosition !== undefined) {192const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined;193this._accessibleViewInCodeBlock.set(inCodeBlock);194}195this._playDiffSignals();196}));197}198199private _playDiffSignals(): void {200const position = this._editorWidget.getPosition();201const model = this._editorWidget.getModel();202if (!position || !model) {203return undefined;204}205const lineContent = model.getLineContent(position.lineNumber);206if (lineContent?.startsWith('+')) {207this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted);208} else if (lineContent?.startsWith('-')) {209this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted);210}211}212213provideTextContent(resource: URI): Promise<ITextModel | null> | null {214return this._getTextModel(resource);215}216217private _resetContextKeys(): void {218this._accessiblityHelpIsShown.reset();219this._accessibleViewIsShown.reset();220this._accessibleViewSupportsNavigation.reset();221this._accessibleViewVerbosityEnabled.reset();222this._accessibleViewGoToSymbolSupported.reset();223this._accessibleViewCurrentProviderId.reset();224this._hasAssignedKeybindings.reset();225this._hasUnassignedKeybindings.reset();226}227228getPosition(id?: AccessibleViewProviderId): Position | undefined {229if (!id || !this._lastProvider || this._lastProvider.id !== id) {230return undefined;231}232return this._editorWidget.getPosition() || undefined;233}234235setPosition(position: Position, reveal?: boolean, select?: boolean): void {236this._editorWidget.setPosition(position);237if (reveal) {238this._editorWidget.revealPosition(position);239}240if (select) {241const lineLength = this._editorWidget.getModel()?.getLineLength(position.lineNumber) ?? 0;242if (lineLength) {243this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: 1, endLineNumber: position.lineNumber, endColumn: lineLength + 1 });244}245}246}247248getCodeBlockContext(): ICodeBlockActionContext | undefined {249const position = this._editorWidget.getPosition();250if (!this._codeBlocks?.length || !position) {251return;252}253const codeBlockIndex = this._codeBlocks?.findIndex(c => c.startLine <= position?.lineNumber && c.endLine >= position?.lineNumber);254const codeBlock = codeBlockIndex !== undefined && codeBlockIndex > -1 ? this._codeBlocks[codeBlockIndex] : undefined;255if (!codeBlock || codeBlockIndex === undefined) {256return;257}258return { code: codeBlock.code, languageId: codeBlock.languageId, codeBlockIndex, element: undefined, chatSessionId: codeBlock.chatSessionId };259}260261navigateToCodeBlock(type: 'next' | 'previous'): void {262const position = this._editorWidget.getPosition();263if (!this._codeBlocks?.length || !position) {264return;265}266let codeBlock;267const codeBlocks = this._codeBlocks.slice();268if (type === 'previous') {269codeBlock = codeBlocks.reverse().find(c => c.endLine < position.lineNumber);270} else {271codeBlock = codeBlocks.find(c => c.startLine > position.lineNumber);272}273if (!codeBlock) {274return;275}276this.setPosition(new Position(codeBlock.startLine, 1), true);277}278279showLastProvider(id: AccessibleViewProviderId): void {280if (!this._lastProvider || this._lastProvider.options.id !== id) {281return;282}283this.show(this._lastProvider);284}285286show(provider?: AccesibleViewContentProvider, symbol?: IAccessibleViewSymbol, showAccessibleViewHelp?: boolean, position?: IPosition): void {287provider = provider ?? this._currentProvider;288if (!provider) {289return;290}291provider.onOpen?.();292const delegate: IContextViewDelegate = {293getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; },294render: (container) => {295this._viewContainer = container;296this._viewContainer.classList.add('accessible-view-container');297return this._render(provider, container, showAccessibleViewHelp);298},299onHide: () => {300if (!showAccessibleViewHelp) {301this._updateLastProvider();302this._currentProvider?.dispose();303this._currentProvider = undefined;304this._resetContextKeys();305}306}307};308this._contextViewService.showContextView(delegate);309310if (position) {311// Context view takes time to show up, so we need to wait for it to show up before we can set the position312queueMicrotask(() => {313this._editorWidget.revealLine(position.lineNumber);314this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column });315});316}317318if (symbol && this._currentProvider) {319this.showSymbol(this._currentProvider, symbol);320}321if (provider instanceof AccessibleContentProvider && provider.onDidRequestClearLastProvider) {322this._register(provider.onDidRequestClearLastProvider((id: string) => {323if (this._lastProvider?.options.id === id) {324this._lastProvider = undefined;325}326}));327}328if (provider.options.id) {329// only cache a provider with an ID so that it will eventually be cleared.330this._lastProvider = provider;331}332if (provider.id === AccessibleViewProviderId.PanelChat || provider.id === AccessibleViewProviderId.QuickChat) {333this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView'));334}335if (provider instanceof ExtensionContentProvider) {336this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER);337}338if (provider.onDidChangeContent) {339this._register(provider.onDidChangeContent(() => {340if (this._viewContainer) { this._render(provider, this._viewContainer, showAccessibleViewHelp); }341}));342}343}344345previous(): void {346const newContent = this._currentProvider?.providePreviousContent?.();347if (!this._currentProvider || !this._viewContainer || !newContent) {348return;349}350this._render(this._currentProvider, this._viewContainer, undefined, newContent);351}352353next(): void {354const newContent = this._currentProvider?.provideNextContent?.();355if (!this._currentProvider || !this._viewContainer || !newContent) {356return;357}358this._render(this._currentProvider, this._viewContainer, undefined, newContent);359}360361private _verbosityEnabled(): boolean {362if (!this._currentProvider) {363return false;364}365return isIAccessibleViewContentProvider(this._currentProvider) ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false);366}367368goToSymbol(): void {369if (!this._currentProvider) {370return;371}372this._isInQuickPick = true;373this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider);374}375376calculateCodeBlocks(markdown?: string): void {377if (!markdown) {378return;379}380if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {381return;382}383if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') {384// Symbols haven't been provided and we cannot parse this language385return;386}387const lines = markdown.split('\n');388this._codeBlocks = [];389let inBlock = false;390let startLine = 0;391392let languageId: string | undefined;393lines.forEach((line, i) => {394if (!inBlock && line.startsWith('```')) {395inBlock = true;396startLine = i + 1;397languageId = line.substring(3).trim();398} else if (inBlock && line.endsWith('```')) {399inBlock = false;400const endLine = i;401const code = lines.slice(startLine, endLine).join('\n');402this._codeBlocks?.push({ startLine, endLine, code, languageId, chatSessionId: undefined });403}404});405this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0);406}407408getSymbols(): IAccessibleViewSymbol[] | undefined {409const provider = this._currentProvider ? this._currentProvider : undefined;410if (!this._currentContent || !provider) {411return;412}413const symbols: IAccessibleViewSymbol[] = 'getSymbols' in provider ? provider.getSymbols?.() || [] : [];414if (symbols?.length) {415return symbols;416}417if (provider.options.language && provider.options.language !== 'markdown') {418// Symbols haven't been provided and we cannot parse this language419return;420}421const markdownTokens: marked.TokensList | undefined = marked.marked.lexer(this._currentContent);422if (!markdownTokens) {423return;424}425this._convertTokensToSymbols(markdownTokens, symbols);426return symbols.length ? symbols : undefined;427}428429openHelpLink(): void {430if (!this._currentProvider?.options.readMoreUrl) {431return;432}433this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl));434}435436configureKeybindings(unassigned: boolean): void {437this._isInQuickPick = true;438const provider = this._updateLastProvider();439const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems;440if (!items) {441return;442}443const disposables = this._register(new DisposableStore());444const quickPick: IQuickPick<IQuickPickItem> = disposables.add(this._quickInputService.createQuickPick());445quickPick.items = items;446quickPick.title = localize('keybindings', 'Configure keybindings');447quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it');448quickPick.show();449disposables.add(quickPick.onDidAccept(async () => {450const item = quickPick.selectedItems[0];451if (item) {452await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id);453}454quickPick.dispose();455}));456disposables.add(quickPick.onDidHide(() => {457if (!quickPick.selectedItems.length && provider) {458this.show(provider);459}460disposables.dispose();461this._isInQuickPick = false;462}));463}464465private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void {466let firstListItem: string | undefined;467for (const token of tokens) {468let label: string | undefined = undefined;469if ('type' in token) {470switch (token.type) {471case 'heading':472case 'paragraph':473case 'code':474label = token.text;475break;476case 'list': {477const firstItem = (token as marked.Tokens.List).items[0];478if (!firstItem) {479break;480}481firstListItem = `- ${firstItem.text}`;482label = (token as marked.Tokens.List).items.map(i => i.text).join(', ');483break;484}485}486}487if (label) {488symbols.push({ markdownToParse: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label), firstListItem });489firstListItem = undefined;490}491}492}493494showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void {495if (!this._currentContent) {496return;497}498let lineNumber: number | undefined = symbol.lineNumber;499const markdownToParse = symbol.markdownToParse;500if (lineNumber === undefined && markdownToParse === undefined) {501// No symbols provided and we cannot parse this language502return;503}504505if (lineNumber === undefined && markdownToParse) {506// Note that this scales poorly, thus isn't used for worst case scenarios like the terminal, for which a line number will always be provided.507// Parse the markdown to find the line number508const index = this._currentContent.split('\n').findIndex(line => line.includes(markdownToParse.split('\n')[0]) || (symbol.firstListItem && line.includes(symbol.firstListItem))) ?? -1;509if (index >= 0) {510lineNumber = index + 1;511}512}513if (lineNumber === undefined) {514return;515}516this._isInQuickPick = false;517this.show(provider, undefined, undefined, { lineNumber, column: 1 });518this._updateContextKeys(provider, true);519}520521disableHint(): void {522if (!isIAccessibleViewContentProvider(this._currentProvider)) {523return;524}525this._configurationService.updateValue(this._currentProvider?.verbositySettingKey, false);526alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey));527}528529private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void {530if (provider.options.type === AccessibleViewType.Help) {531this._accessiblityHelpIsShown.set(shown);532this._accessibleViewIsShown.reset();533} else {534this._accessibleViewIsShown.set(shown);535this._accessiblityHelpIsShown.reset();536}537this._accessibleViewSupportsNavigation.set(provider.provideNextContent !== undefined || provider.providePreviousContent !== undefined);538this._accessibleViewVerbosityEnabled.set(this._verbosityEnabled());539this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false);540}541542private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void {543let content = updatedContent ?? provider.provideContent();544if (provider.options.type === AccessibleViewType.View) {545this._currentContent = content;546this._hasUnassignedKeybindings.reset();547this._hasAssignedKeybindings.reset();548return;549}550const readMoreLinkHint = this._readMoreHint(provider);551const disableHelpHint = this._disableVerbosityHint(provider);552const screenReaderModeHint = this._screenReaderModeHint(provider);553const exitThisDialogHint = this._exitDialogHint(provider);554let configureKbHint = '';555let configureAssignedKbHint = '';556const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, screenReaderModeHint + content + readMoreLinkHint + disableHelpHint + exitThisDialogHint);557if (resolvedContent) {558content = resolvedContent.content.value;559if (resolvedContent.configureKeybindingItems) {560provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems;561this._hasUnassignedKeybindings.set(true);562configureKbHint = this._configureUnassignedKbHint();563} else {564this._hasAssignedKeybindings.reset();565}566if (resolvedContent.configuredKeybindingItems) {567provider.options.configuredKeybindingItems = resolvedContent.configuredKeybindingItems;568this._hasAssignedKeybindings.set(true);569configureAssignedKbHint = this._configureAssignedKbHint();570} else {571this._hasAssignedKeybindings.reset();572}573}574this._currentContent = content + configureKbHint + configureAssignedKbHint;575}576577private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean, updatedContent?: string): IDisposable {578this._currentProvider = provider;579this._accessibleViewCurrentProviderId.set(provider.id);580const verbose = this._verbosityEnabled();581this._updateContent(provider, updatedContent);582this.calculateCodeBlocks(this._currentContent);583this._updateContextKeys(provider, true);584const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus();585this._getTextModel(URI.from({ path: `accessible-view-${provider.id}`, scheme: Schemas.accessibleView, fragment: this._currentContent })).then((model) => {586if (!model) {587return;588}589this._editorWidget.setModel(model);590const domNode = this._editorWidget.getDomNode();591if (!domNode) {592return;593}594model.setLanguage(provider.options.language ?? 'markdown');595container.appendChild(this._container);596let actionsHint = '';597const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || provider.actions?.length;598if (verbose && !showAccessibleViewHelp && hasActions) {599actionsHint = provider.options.position ? localize('ariaAccessibleViewActionsBottom', 'Explore actions such as disabling this hint (Shift+Tab), use Escape to exit this dialog.') : localize('ariaAccessibleViewActions', 'Explore actions such as disabling this hint (Shift+Tab).');600}601let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View");602this._title.textContent = ariaLabel;603if (actionsHint && provider.options.type === AccessibleViewType.View) {604ariaLabel = localize('accessible-view-hint', "Accessible View, {0}", actionsHint);605} else if (actionsHint) {606ariaLabel = localize('accessibility-help-hint', "Accessibility Help, {0}", actionsHint);607}608if (isWindows && widgetIsFocused) {609// prevent the screen reader on windows from reading610// the aria label again when it's refocused611ariaLabel = '';612}613this._editorWidget.updateOptions({ ariaLabel });614this._editorWidget.focus();615if (this._currentProvider?.options.position) {616const position = this._editorWidget.getPosition();617const isDefaultPosition = position?.lineNumber === 1 && position.column === 1;618if (this._currentProvider.options.position === 'bottom' || this._currentProvider.options.position === 'initial-bottom' && isDefaultPosition) {619const lastLine = this.editorWidget.getModel()?.getLineCount();620const position = lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;621if (position) {622this._editorWidget.setPosition(position);623this._editorWidget.revealLine(position.lineNumber);624}625}626}627});628this._updateToolbar(this._currentProvider.actions, provider.options.type);629630const hide = (e?: KeyboardEvent | IKeyboardEvent): void => {631if (!this._isInQuickPick) {632provider.onClose();633}634e?.stopPropagation();635this._contextViewService.hideContextView();636if (this._isInQuickPick) {637return;638}639this._updateContextKeys(provider, false);640this._lastProvider = undefined;641this._currentContent = undefined;642this._currentProvider?.dispose();643this._currentProvider = undefined;644};645const disposableStore = new DisposableStore();646disposableStore.add(this._editorWidget.onKeyDown((e) => {647if (e.keyCode === KeyCode.Enter) {648this._commandService.executeCommand('editor.action.openLink');649} else if (e.keyCode === KeyCode.Escape || shouldHide(e.browserEvent, this._keybindingService, this._configurationService)) {650hide(e);651} else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) {652const url: string = provider.options.readMoreUrl;653alert(AccessibilityHelpNLS.openingDocs);654this._openerService.open(URI.parse(url));655e.preventDefault();656e.stopPropagation();657}658if (provider instanceof AccessibleContentProvider) {659provider.onKeyDown?.(e);660}661}));662disposableStore.add(addDisposableListener(this._toolbar.getElement(), EventType.KEY_DOWN, (e: KeyboardEvent) => {663const keyboardEvent = new StandardKeyboardEvent(e);664if (keyboardEvent.equals(KeyCode.Escape)) {665hide(e);666}667}));668disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => {669if (!isActiveElement(this._toolbar.getElement())) {670hide();671}672}));673disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout()));674disposableStore.add(this._layoutService.onDidLayoutActiveContainer(() => this._layout()));675return disposableStore;676}677678private _updateToolbar(providedActions?: IAction[], type?: AccessibleViewType): void {679this._toolbar.setAriaLabel(type === AccessibleViewType.Help ? localize('accessibleHelpToolbar', 'Accessibility Help') : localize('accessibleViewToolbar', "Accessible View"));680const toolbarMenu = this._register(this._menuService.createMenu(MenuId.AccessibleView, this._contextKeyService));681const menuActions = getFlatActionBarActions(toolbarMenu.getActions({}));682if (providedActions) {683for (const providedAction of providedActions) {684providedAction.class = providedAction.class || ThemeIcon.asClassName(Codicon.primitiveSquare);685providedAction.checked = undefined;686}687this._toolbar.setActions([...providedActions, ...menuActions]);688} else {689this._toolbar.setActions(menuActions);690}691}692693private _layout(): void {694const dimension = this._layoutService.activeContainerDimension;695const maxHeight = dimension.height && dimension.height * .4;696const height = Math.min(maxHeight, this._editorWidget.getContentHeight());697const width = Math.min(dimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH);698this._editorWidget.layout({ width, height });699}700701private async _getTextModel(resource: URI): Promise<ITextModel | null> {702const existing = this._modelService.getModel(resource);703if (existing && !existing.isDisposed()) {704return existing;705}706return this._modelService.createModel(resource.fragment, null, resource, false);707}708709private _goToSymbolsSupported(): boolean {710if (!this._currentProvider) {711return false;712}713return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AccessibleContentProvider && !!this._currentProvider.getSymbols?.());714}715716private _updateLastProvider(): AccesibleViewContentProvider | undefined {717const provider = this._currentProvider;718if (!provider) {719return;720}721const lastProvider = provider instanceof AccessibleContentProvider ? new AccessibleContentProvider(722provider.id,723provider.options,724provider.provideContent.bind(provider),725provider.onClose.bind(provider),726provider.verbositySettingKey,727provider.onOpen?.bind(provider),728provider.actions,729provider.provideNextContent?.bind(provider),730provider.providePreviousContent?.bind(provider),731provider.onDidChangeContent?.bind(provider),732provider.onKeyDown?.bind(provider),733provider.getSymbols?.bind(provider),734) : new ExtensionContentProvider(735provider.id,736provider.options,737provider.provideContent.bind(provider),738provider.onClose.bind(provider),739provider.onOpen?.bind(provider),740provider.provideNextContent?.bind(provider),741provider.providePreviousContent?.bind(provider),742provider.actions,743provider.onDidChangeContent?.bind(provider),744);745return lastProvider;746}747748public showAccessibleViewHelp(): void {749const lastProvider = this._updateLastProvider();750if (!lastProvider) {751return;752}753let accessibleViewHelpProvider;754if (lastProvider instanceof AccessibleContentProvider) {755accessibleViewHelpProvider = new AccessibleContentProvider(756lastProvider.id,757{ type: AccessibleViewType.Help },758() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),759() => {760this._contextViewService.hideContextView();761// HACK: Delay to allow the context view to hide #207638762queueMicrotask(() => this.show(lastProvider));763},764lastProvider.verbositySettingKey765);766} else {767accessibleViewHelpProvider = new ExtensionContentProvider(768lastProvider.id,769{ type: AccessibleViewType.Help },770() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),771() => {772this._contextViewService.hideContextView();773// HACK: Delay to allow the context view to hide #207638774queueMicrotask(() => this.show(lastProvider));775},776);777}778this._contextViewService.hideContextView();779// HACK: Delay to allow the context view to hide #186514780if (accessibleViewHelpProvider) {781queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true));782}783}784785private _accessibleViewHelpDialogContent(providerHasSymbols?: boolean): string {786const navigationHint = this._navigationHint();787const goToSymbolHint = this._goToSymbolHint(providerHasSymbols);788const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab).");789const chatHints = this._getChatHints();790791let hint = localize('intro', "In the accessible view, you can:\n");792if (navigationHint) {793hint += ' - ' + navigationHint + '\n';794}795if (goToSymbolHint) {796hint += ' - ' + goToSymbolHint + '\n';797}798if (toolbarHint) {799hint += ' - ' + toolbarHint + '\n';800}801if (chatHints) {802hint += chatHints;803}804return hint;805}806807private _getChatHints(): string | undefined {808if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {809return;810}811return [localize('insertAtCursor', " - Insert the code block at the cursor{0}.", '<keybinding:workbench.action.chat.insertCodeBlock>'),812localize('insertIntoNewFile', " - Insert the code block into a new file{0}.", '<keybinding:workbench.action.chat.insertIntoNewFile>'),813localize('runInTerminal', " - Run the code block in the terminal{0}.\n", '<keybinding:workbench.action.chat.runInTerminal>')].join('\n');814}815816private _navigationHint(): string {817return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", `<keybinding:${AccessibilityCommandId.ShowNext}`, `<keybinding:${AccessibilityCommandId.ShowPrevious}>`);818}819820private _disableVerbosityHint(provider: AccesibleViewContentProvider): string {821if (provider.options.type === AccessibleViewType.Help && this._verbosityEnabled()) {822return localize('acessibleViewDisableHint', "\nDisable accessibility verbosity for this feature{0}.", `<keybinding:${AccessibilityCommandId.DisableVerbosityHint}>`);823}824return '';825}826827private _goToSymbolHint(providerHasSymbols?: boolean): string | undefined {828if (!providerHasSymbols) {829return;830}831return localize('goToSymbolHint', 'Go to a symbol{0}.', `<keybinding:${AccessibilityCommandId.GoToSymbol}>`);832}833834private _configureUnassignedKbHint(): string {835const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel();836const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Unassigned Keybindings.';837return localize('configureKb', '\nConfigure keybindings for commands that lack them {0}.', keybindingToConfigureQuickPick);838}839840private _configureAssignedKbHint(): string {841const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings)?.getAriaLabel();842const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Assigned Keybindings.';843return localize('configureKbAssigned', '\nConfigure keybindings for commands that already have assignments {0}.', keybindingToConfigureQuickPick);844}845846private _screenReaderModeHint(provider: AccesibleViewContentProvider): string {847const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized();848let screenReaderModeHint = '';849const turnOnMessage = (850isMacintosh851? AccessibilityHelpNLS.changeConfigToOnMac852: AccessibilityHelpNLS.changeConfigToOnWinLinux853);854if (accessibilitySupport && provider.id === AccessibleViewProviderId.Editor) {855screenReaderModeHint = AccessibilityHelpNLS.auto_on;856screenReaderModeHint += '\n';857} else if (!accessibilitySupport) {858screenReaderModeHint = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage;859screenReaderModeHint += '\n';860}861return screenReaderModeHint;862}863864private _exitDialogHint(provider: AccesibleViewContentProvider): string {865return this._verbosityEnabled() && !provider.options.position ? localize('exit', '\nExit this dialog (Escape).') : '';866}867868private _readMoreHint(provider: AccesibleViewContentProvider): string {869return provider.options.readMoreUrl ? localize("openDoc", "\nOpen a browser window with more information related to accessibility{0}.", `<keybinding:${AccessibilityCommandId.AccessibilityHelpOpenHelpLink}>`) : '';870}871}872873export class AccessibleViewService extends Disposable implements IAccessibleViewService {874declare readonly _serviceBrand: undefined;875private _accessibleView: AccessibleView | undefined;876877constructor(878@IInstantiationService private readonly _instantiationService: IInstantiationService,879@IConfigurationService private readonly _configurationService: IConfigurationService,880@IKeybindingService private readonly _keybindingService: IKeybindingService881) {882super();883}884885show(provider: AccesibleViewContentProvider, position?: Position): void {886if (!this._accessibleView) {887this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView));888}889this._accessibleView.show(provider, undefined, undefined, position);890}891configureKeybindings(unassigned: boolean): void {892this._accessibleView?.configureKeybindings(unassigned);893}894openHelpLink(): void {895this._accessibleView?.openHelpLink();896}897showLastProvider(id: AccessibleViewProviderId): void {898this._accessibleView?.showLastProvider(id);899}900next(): void {901this._accessibleView?.next();902}903previous(): void {904this._accessibleView?.previous();905}906goToSymbol(): void {907this._accessibleView?.goToSymbol();908}909getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {910if (!this._configurationService.getValue(verbositySettingKey)) {911return null;912}913const keybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibleView)?.getAriaLabel();914let hint = null;915if (keybinding) {916hint = localize('acessibleViewHint', "Inspect this in the accessible view with {0}", keybinding);917} else {918hint = localize('acessibleViewHintNoKbEither', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.");919}920return hint;921}922disableHint(): void {923this._accessibleView?.disableHint();924}925showAccessibleViewHelp(): void {926this._accessibleView?.showAccessibleViewHelp();927}928getPosition(id: AccessibleViewProviderId): Position | undefined {929return this._accessibleView?.getPosition(id) ?? undefined;930}931getLastPosition(): Position | undefined {932const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount();933return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;934}935setPosition(position: Position, reveal?: boolean, select?: boolean): void {936this._accessibleView?.setPosition(position, reveal, select);937}938getCodeBlockContext(): ICodeBlockActionContext | undefined {939return this._accessibleView?.getCodeBlockContext();940}941navigateToCodeBlock(type: 'next' | 'previous'): void {942this._accessibleView?.navigateToCodeBlock(type);943}944}945946class AccessibleViewSymbolQuickPick {947constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) {948949}950show(provider: AccesibleViewContentProvider): void {951const disposables = new DisposableStore();952const quickPick = disposables.add(this._quickInputService.createQuickPick<IAccessibleViewSymbol>());953quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols");954quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View");955const picks = [];956const symbols = this._accessibleView.getSymbols();957if (!symbols) {958return;959}960for (const symbol of symbols) {961picks.push({962label: symbol.label,963ariaLabel: symbol.ariaLabel,964firstListItem: symbol.firstListItem,965lineNumber: symbol.lineNumber,966endLineNumber: symbol.endLineNumber,967markdownToParse: symbol.markdownToParse968});969}970quickPick.canSelectMany = false;971quickPick.items = picks;972quickPick.show();973disposables.add(quickPick.onDidAccept(() => {974this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]);975quickPick.hide();976}));977disposables.add(quickPick.onDidHide(() => {978if (quickPick.selectedItems.length === 0) {979// this was escaped, so refocus the accessible view980this._accessibleView.show(provider);981}982disposables.dispose();983}));984}985}986987988function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean {989if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) {990return false;991}992const standardKeyboardEvent = new StandardKeyboardEvent(event);993const resolveResult = keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target);994995const isValidChord = resolveResult.kind === ResultKind.MoreChordsNeeded;996if (keybindingService.inChordMode || isValidChord) {997return false;998}999return shouldHandleKey(event) && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey;1000}10011002function shouldHandleKey(event: KeyboardEvent): boolean {1003return !!event.code.match(/^(Key[A-Z]|Digit[0-9]|Equal|Comma|Period|Slash|Quote|Backquote|Backslash|Minus|Semicolon|Space|Enter)$/);1004}100510061007