Path: blob/main/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts
5241 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, getWindow, 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';2425import { AccessibilityHelpNLS } from '../../../../editor/common/standaloneStrings.js';26import { CodeActionController } from '../../../../editor/contrib/codeAction/browser/codeActionController.js';27import { FloatingEditorToolbar } from '../../../../editor/contrib/floatingMenu/browser/floatingMenu.js';28import { localize } from '../../../../nls.js';29import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType, ExtensionContentProvider, IAccessibleViewService, IAccessibleViewSymbol, isIAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js';30import { ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX, IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';31import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';32import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';33import { WorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';34import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';35import { ICommandService } from '../../../../platform/commands/common/commands.js';36import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';37import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';38import { IContextViewDelegate, IContextViewService } from '../../../../platform/contextview/browser/contextView.js';39import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';40import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';41import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';42import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';43import { IOpenerService } from '../../../../platform/opener/common/opener.js';44import { IQuickInputService, IQuickPick, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js';45import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';46import { FloatingEditorClickMenu } from '../../../browser/codeeditor.js';47import { IChatCodeBlockContextProviderService } from '../../chat/browser/chat.js';48import { ICodeBlockActionContext } from '../../chat/browser/widget/chatContentParts/codeBlockPart.js';49import { getSimpleEditorOptions } from '../../codeEditor/browser/simpleEditorOptions.js';50import { AccessibilityCommandId } from '../common/accessibilityCommands.js';51import { AccessibilityVerbositySettingId, AccessibilityWorkbenchSettingId, accessibilityHelpIsShown, accessibleViewContainsCodeBlocks, accessibleViewCurrentProviderId, accessibleViewGoToSymbolSupported, accessibleViewHasAssignedKeybindings, accessibleViewHasUnassignedKeybindings, accessibleViewInCodeBlock, accessibleViewIsShown, accessibleViewOnLastLine, accessibleViewSupportsNavigation, accessibleViewVerbosityEnabled } from './accessibilityConfiguration.js';52import { resolveContentAndKeybindingItems } from './accessibleViewKeybindingResolver.js';5354const enum DIMENSIONS {55MAX_WIDTH = 60056}5758export type AccesibleViewContentProvider = AccessibleContentProvider | ExtensionContentProvider;5960interface ICodeBlock {61startLine: number;62endLine: number;63code: string;64languageId?: string;65chatSessionResource: URI | undefined;66}6768export class AccessibleView extends Disposable {69private _editorWidget: CodeEditorWidget;7071private _accessiblityHelpIsShown: IContextKey<boolean>;72private _onLastLine: IContextKey<boolean>;73private _accessibleViewIsShown: IContextKey<boolean>;74private _accessibleViewSupportsNavigation: IContextKey<boolean>;75private _accessibleViewVerbosityEnabled: IContextKey<boolean>;76private _accessibleViewGoToSymbolSupported: IContextKey<boolean>;77private _accessibleViewCurrentProviderId: IContextKey<string>;78private _accessibleViewInCodeBlock: IContextKey<boolean>;79private _accessibleViewContainsCodeBlocks: IContextKey<boolean>;80private _hasUnassignedKeybindings: IContextKey<boolean>;81private _hasAssignedKeybindings: IContextKey<boolean>;8283private _codeBlocks?: ICodeBlock[];84private _isInQuickPick: boolean = false;8586get editorWidget() { return this._editorWidget; }87private _container: HTMLElement;88private _title: HTMLElement;89private readonly _toolbar: WorkbenchToolBar;9091private _currentProvider: AccesibleViewContentProvider | undefined;92private _currentContent: string | undefined;9394private _lastProvider: AccesibleViewContentProvider | undefined;95private _lastProviderPosition: Map<string, Position> = new Map();9697private _viewContainer: HTMLElement | undefined;9899100constructor(101@IOpenerService private readonly _openerService: IOpenerService,102@IInstantiationService private readonly _instantiationService: IInstantiationService,103@IConfigurationService private readonly _configurationService: IConfigurationService,104@IModelService private readonly _modelService: IModelService,105@IContextViewService private readonly _contextViewService: IContextViewService,106@IContextKeyService private readonly _contextKeyService: IContextKeyService,107@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,108@IKeybindingService private readonly _keybindingService: IKeybindingService,109@ILayoutService private readonly _layoutService: ILayoutService,110@IMenuService private readonly _menuService: IMenuService,111@ICommandService private readonly _commandService: ICommandService,112@IChatCodeBlockContextProviderService private readonly _codeBlockContextProviderService: IChatCodeBlockContextProviderService,113@IStorageService private readonly _storageService: IStorageService,114@IQuickInputService private readonly _quickInputService: IQuickInputService,115@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,116) {117super();118119this._accessiblityHelpIsShown = accessibilityHelpIsShown.bindTo(this._contextKeyService);120this._accessibleViewIsShown = accessibleViewIsShown.bindTo(this._contextKeyService);121this._accessibleViewSupportsNavigation = accessibleViewSupportsNavigation.bindTo(this._contextKeyService);122this._accessibleViewVerbosityEnabled = accessibleViewVerbosityEnabled.bindTo(this._contextKeyService);123this._accessibleViewGoToSymbolSupported = accessibleViewGoToSymbolSupported.bindTo(this._contextKeyService);124this._accessibleViewCurrentProviderId = accessibleViewCurrentProviderId.bindTo(this._contextKeyService);125this._accessibleViewInCodeBlock = accessibleViewInCodeBlock.bindTo(this._contextKeyService);126this._accessibleViewContainsCodeBlocks = accessibleViewContainsCodeBlocks.bindTo(this._contextKeyService);127this._onLastLine = accessibleViewOnLastLine.bindTo(this._contextKeyService);128this._hasUnassignedKeybindings = accessibleViewHasUnassignedKeybindings.bindTo(this._contextKeyService);129this._hasAssignedKeybindings = accessibleViewHasAssignedKeybindings.bindTo(this._contextKeyService);130131this._container = document.createElement('div');132this._container.classList.add('accessible-view');133if (this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView)) {134this._container.classList.add('hide');135}136const codeEditorWidgetOptions: ICodeEditorWidgetOptions = {137contributions: EditorExtensionsRegistry.getEditorContributions()138.filter(c => c.id !== CodeActionController.ID && c.id !== FloatingEditorClickMenu.ID && c.id !== FloatingEditorToolbar.ID)139};140const titleBar = document.createElement('div');141titleBar.classList.add('accessible-view-title-bar');142this._title = document.createElement('div');143this._title.classList.add('accessible-view-title');144titleBar.appendChild(this._title);145const actionBar = document.createElement('div');146actionBar.classList.add('accessible-view-action-bar');147titleBar.appendChild(actionBar);148this._container.appendChild(titleBar);149this._toolbar = this._register(_instantiationService.createInstance(WorkbenchToolBar, actionBar, { orientation: ActionsOrientation.HORIZONTAL }));150this._toolbar.context = { viewId: 'accessibleView' };151const toolbarElt = this._toolbar.getElement();152toolbarElt.tabIndex = 0;153154const editorOptions: IEditorConstructionOptions = {155...getSimpleEditorOptions(this._configurationService),156lineDecorationsWidth: 6,157dragAndDrop: false,158cursorWidth: 1,159wordWrap: 'off',160wrappingStrategy: 'advanced',161wrappingIndent: 'none',162padding: { top: 2, bottom: 2 },163quickSuggestions: false,164renderWhitespace: 'none',165dropIntoEditor: { enabled: false },166readOnly: true,167fontFamily: 'var(--monaco-monospace-font)'168};169170this._editorWidget = this._register(this._instantiationService.createInstance(CodeEditorWidget, this._container, editorOptions, codeEditorWidgetOptions));171this._register(this._accessibilityService.onDidChangeScreenReaderOptimized(() => {172if (this._currentProvider && this._accessiblityHelpIsShown.get()) {173this.show(this._currentProvider);174}175}));176this._register(this._configurationService.onDidChangeConfiguration(e => {177if (isIAccessibleViewContentProvider(this._currentProvider) && e.affectsConfiguration(this._currentProvider.verbositySettingKey)) {178if (this._accessiblityHelpIsShown.get()) {179this.show(this._currentProvider);180}181this._accessibleViewVerbosityEnabled.set(this._configurationService.getValue(this._currentProvider.verbositySettingKey));182this._updateToolbar(this._currentProvider.actions, this._currentProvider.options.type);183}184if (e.affectsConfiguration(AccessibilityWorkbenchSettingId.HideAccessibleView)) {185this._container.classList.toggle('hide', this._configurationService.getValue(AccessibilityWorkbenchSettingId.HideAccessibleView));186}187}));188this._register(this._editorWidget.onDidDispose(() => this._resetContextKeys()));189this._register(this._editorWidget.onDidChangeCursorPosition(() => {190this._onLastLine.set(this._editorWidget.getPosition()?.lineNumber === this._editorWidget.getModel()?.getLineCount());191const cursorPosition = this._editorWidget.getPosition()?.lineNumber;192if (this._codeBlocks && cursorPosition !== undefined) {193const inCodeBlock = this._codeBlocks.find(c => c.startLine <= cursorPosition && c.endLine >= cursorPosition) !== undefined;194this._accessibleViewInCodeBlock.set(inCodeBlock);195}196this._playDiffSignals();197}));198}199200private _playDiffSignals(): void {201if (this._currentProvider?.id !== AccessibleViewProviderId.DiffEditor && this._currentProvider?.id !== AccessibleViewProviderId.InlineCompletions) {202return;203}204const position = this._editorWidget.getPosition();205const model = this._editorWidget.getModel();206if (!position || !model) {207return undefined;208}209const lineContent = model.getLineContent(position.lineNumber);210if (lineContent?.startsWith('+')) {211this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineInserted);212} else if (lineContent?.startsWith('-')) {213this._accessibilitySignalService.playSignal(AccessibilitySignal.diffLineDeleted);214}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, chatSessionResource: codeBlock.chatSessionResource };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();302// Save cursor position before disposing so it can be restored on reopen303if (this._currentProvider) {304const currentPosition = this._editorWidget.getPosition();305if (currentPosition) {306this._lastProviderPosition.set(this._currentProvider.id, currentPosition);307}308}309this._currentProvider?.dispose();310this._currentProvider = undefined;311this._resetContextKeys();312}313}314};315this._contextViewService.showContextView(delegate);316317if (position) {318// Context view takes time to show up, so we need to wait for it to show up before we can set the position319queueMicrotask(() => {320this._editorWidget.revealLine(position.lineNumber);321this._editorWidget.setSelection({ startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column });322});323}324325if (symbol && this._currentProvider) {326this.showSymbol(this._currentProvider, symbol);327}328if (provider instanceof AccessibleContentProvider && provider.onDidRequestClearLastProvider) {329this._register(provider.onDidRequestClearLastProvider((id: string) => {330if (this._lastProvider?.options.id === id) {331this._lastProvider = undefined;332}333this._lastProviderPosition.delete(id);334}));335}336if (provider.options.id) {337// only cache a provider with an ID so that it will eventually be cleared.338this._lastProvider = provider;339}340if (provider.id === AccessibleViewProviderId.PanelChat || provider.id === AccessibleViewProviderId.QuickChat) {341this._register(this._codeBlockContextProviderService.registerProvider({ getCodeBlockContext: () => this.getCodeBlockContext() }, 'accessibleView'));342}343if (provider instanceof ExtensionContentProvider) {344this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER);345}346if (provider.onDidChangeContent) {347this._register(provider.onDidChangeContent(() => {348if (this._viewContainer) { this._render(provider, this._viewContainer, showAccessibleViewHelp); }349}));350}351}352353previous(): void {354const newContent = this._currentProvider?.providePreviousContent?.();355if (!this._currentProvider || !this._viewContainer || !newContent) {356return;357}358this._render(this._currentProvider, this._viewContainer, undefined, newContent);359}360361next(): void {362const newContent = this._currentProvider?.provideNextContent?.();363if (!this._currentProvider || !this._viewContainer || !newContent) {364return;365}366this._render(this._currentProvider, this._viewContainer, undefined, newContent);367}368369private _verbosityEnabled(): boolean {370if (!this._currentProvider) {371return false;372}373return isIAccessibleViewContentProvider(this._currentProvider) ? this._configurationService.getValue(this._currentProvider.verbositySettingKey) === true : this._storageService.getBoolean(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${this._currentProvider.id}`, StorageScope.APPLICATION, false);374}375376goToSymbol(): void {377if (!this._currentProvider) {378return;379}380this._isInQuickPick = true;381this._instantiationService.createInstance(AccessibleViewSymbolQuickPick, this).show(this._currentProvider);382}383384calculateCodeBlocks(markdown?: string): void {385if (!markdown) {386return;387}388if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {389return;390}391if (this._currentProvider.options.language && this._currentProvider.options.language !== 'markdown') {392// Symbols haven't been provided and we cannot parse this language393return;394}395const lines = markdown.split('\n');396this._codeBlocks = [];397let inBlock = false;398let startLine = 0;399400let languageId: string | undefined;401lines.forEach((line, i) => {402if (!inBlock && line.startsWith('```')) {403inBlock = true;404startLine = i + 1;405languageId = line.substring(3).trim();406} else if (inBlock && line.endsWith('```')) {407inBlock = false;408const endLine = i;409const code = lines.slice(startLine, endLine).join('\n');410this._codeBlocks?.push({ startLine, endLine, code, languageId, chatSessionResource: undefined });411}412});413this._accessibleViewContainsCodeBlocks.set(this._codeBlocks.length > 0);414}415416getSymbols(): IAccessibleViewSymbol[] | undefined {417const provider = this._currentProvider ? this._currentProvider : undefined;418if (!this._currentContent || !provider) {419return;420}421const symbols: IAccessibleViewSymbol[] = 'getSymbols' in provider ? provider.getSymbols?.() || [] : [];422if (symbols?.length) {423return symbols;424}425if (provider.options.language && provider.options.language !== 'markdown') {426// Symbols haven't been provided and we cannot parse this language427return;428}429const markdownTokens: marked.TokensList | undefined = marked.marked.lexer(this._currentContent);430if (!markdownTokens) {431return;432}433this._convertTokensToSymbols(markdownTokens, symbols);434return symbols.length ? symbols : undefined;435}436437openHelpLink(): void {438if (!this._currentProvider?.options.readMoreUrl) {439return;440}441this._openerService.open(URI.parse(this._currentProvider.options.readMoreUrl));442}443444configureKeybindings(unassigned: boolean): void {445this._isInQuickPick = true;446const provider = this._updateLastProvider();447const items = unassigned ? provider?.options?.configureKeybindingItems : provider?.options?.configuredKeybindingItems;448if (!items) {449return;450}451const disposables = this._register(new DisposableStore());452const quickPick: IQuickPick<IQuickPickItem> = disposables.add(this._quickInputService.createQuickPick());453quickPick.items = items;454quickPick.title = localize('keybindings', 'Configure keybindings');455quickPick.placeholder = localize('selectKeybinding', 'Select a command ID to configure a keybinding for it');456quickPick.show();457disposables.add(quickPick.onDidAccept(async () => {458const item = quickPick.selectedItems[0];459if (item) {460await this._commandService.executeCommand('workbench.action.openGlobalKeybindings', item.id);461}462quickPick.dispose();463}));464disposables.add(quickPick.onDidHide(() => {465if (!quickPick.selectedItems.length && provider) {466this.show(provider);467}468disposables.dispose();469this._isInQuickPick = false;470}));471}472473private _convertTokensToSymbols(tokens: marked.TokensList, symbols: IAccessibleViewSymbol[]): void {474let firstListItem: string | undefined;475for (const token of tokens) {476let label: string | undefined = undefined;477if ('type' in token) {478switch (token.type) {479case 'heading':480case 'paragraph':481case 'code':482label = token.text;483break;484case 'list': {485const firstItem = (token as marked.Tokens.List).items[0];486if (!firstItem) {487break;488}489firstListItem = `- ${firstItem.text}`;490label = (token as marked.Tokens.List).items.map(i => i.text).join(', ');491break;492}493}494}495if (label) {496symbols.push({ markdownToParse: label, label: localize('symbolLabel', "({0}) {1}", token.type, label), ariaLabel: localize('symbolLabelAria', "({0}) {1}", token.type, label), firstListItem });497firstListItem = undefined;498}499}500}501502showSymbol(provider: AccesibleViewContentProvider, symbol: IAccessibleViewSymbol): void {503if (!this._currentContent) {504return;505}506let lineNumber: number | undefined = symbol.lineNumber;507const markdownToParse = symbol.markdownToParse;508if (lineNumber === undefined && markdownToParse === undefined) {509// No symbols provided and we cannot parse this language510return;511}512513if (lineNumber === undefined && markdownToParse) {514// 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.515// Parse the markdown to find the line number516const index = this._currentContent.split('\n').findIndex(line => line.includes(markdownToParse.split('\n')[0]) || (symbol.firstListItem && line.includes(symbol.firstListItem))) ?? -1;517if (index >= 0) {518lineNumber = index + 1;519}520}521if (lineNumber === undefined) {522return;523}524this._isInQuickPick = false;525this.show(provider, undefined, undefined, { lineNumber, column: 1 });526this._updateContextKeys(provider, true);527}528529disableHint(): void {530if (!isIAccessibleViewContentProvider(this._currentProvider)) {531return;532}533this._configurationService.updateValue(this._currentProvider?.verbositySettingKey, false);534alert(localize('disableAccessibilityHelp', '{0} accessibility verbosity is now disabled', this._currentProvider.verbositySettingKey));535}536537private _updateContextKeys(provider: AccesibleViewContentProvider, shown: boolean): void {538if (provider.options.type === AccessibleViewType.Help) {539this._accessiblityHelpIsShown.set(shown);540this._accessibleViewIsShown.reset();541} else {542this._accessibleViewIsShown.set(shown);543this._accessiblityHelpIsShown.reset();544}545this._accessibleViewSupportsNavigation.set(provider.provideNextContent !== undefined || provider.providePreviousContent !== undefined);546this._accessibleViewVerbosityEnabled.set(this._verbosityEnabled());547this._accessibleViewGoToSymbolSupported.set(this._goToSymbolsSupported() ? this.getSymbols()?.length! > 0 : false);548}549550private _getStableUri(providerId: string): URI {551return URI.from({ path: `accessible-view-${providerId}`, scheme: Schemas.accessibleView });552}553554private _updateContent(provider: AccesibleViewContentProvider, updatedContent?: string): void {555let content = updatedContent ?? provider.provideContent();556if (provider.options.type === AccessibleViewType.View) {557this._currentContent = content;558this._hasUnassignedKeybindings.reset();559this._hasAssignedKeybindings.reset();560return;561}562const readMoreLinkHint = this._readMoreHint(provider);563const disableHelpHint = this._disableVerbosityHint(provider);564const screenReaderModeHint = this._screenReaderModeHint(provider);565const exitThisDialogHint = this._exitDialogHint(provider);566let configureKbHint = '';567let configureAssignedKbHint = '';568const resolvedContent = resolveContentAndKeybindingItems(this._keybindingService, screenReaderModeHint + content + readMoreLinkHint + disableHelpHint + exitThisDialogHint);569if (resolvedContent) {570content = resolvedContent.content.value;571if (resolvedContent.configureKeybindingItems) {572provider.options.configureKeybindingItems = resolvedContent.configureKeybindingItems;573this._hasUnassignedKeybindings.set(true);574configureKbHint = this._configureUnassignedKbHint();575} else {576this._hasAssignedKeybindings.reset();577}578if (resolvedContent.configuredKeybindingItems) {579provider.options.configuredKeybindingItems = resolvedContent.configuredKeybindingItems;580this._hasAssignedKeybindings.set(true);581configureAssignedKbHint = this._configureAssignedKbHint();582} else {583this._hasAssignedKeybindings.reset();584}585}586this._currentContent = content + configureKbHint + configureAssignedKbHint;587}588589private _render(provider: AccesibleViewContentProvider, container: HTMLElement, showAccessibleViewHelp?: boolean, updatedContent?: string): IDisposable {590const isSameProvider = this._currentProvider?.id === provider.id;591const previousPosition = isSameProvider ? this._editorWidget.getPosition() : undefined;592this._currentProvider = provider;593this._accessibleViewCurrentProviderId.set(provider.id);594const verbose = this._verbosityEnabled();595this._updateContent(provider, updatedContent);596this.calculateCodeBlocks(this._currentContent);597this._updateContextKeys(provider, true);598const widgetIsFocused = this._editorWidget.hasTextFocus() || this._editorWidget.hasWidgetFocus();599const stableUri = this._getStableUri(provider.id);600this._getTextModel(stableUri).then((model) => {601if (!model) {602return;603}604// Update the content of the existing model instead of creating a new one605// This preserves the cursor position when content changes606const currentContent = this._currentContent ?? '';607if (model.getValue() !== currentContent) {608model.setValue(currentContent);609}610if (this._editorWidget.getModel() !== model) {611this._editorWidget.setModel(model);612}613const domNode = this._editorWidget.getDomNode();614if (!domNode) {615return;616}617model.setLanguage(provider.options.language ?? 'markdown');618container.appendChild(this._container);619let actionsHint = '';620const hasActions = this._accessibleViewSupportsNavigation.get() || this._accessibleViewVerbosityEnabled.get() || this._accessibleViewGoToSymbolSupported.get() || provider.actions?.length;621if (verbose && !showAccessibleViewHelp && hasActions) {622actionsHint = 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).');623}624let ariaLabel = provider.options.type === AccessibleViewType.Help ? localize('accessibility-help', "Accessibility Help") : localize('accessible-view', "Accessible View");625this._title.textContent = ariaLabel;626if (actionsHint && provider.options.type === AccessibleViewType.View) {627ariaLabel = localize('accessible-view-hint', "Accessible View, {0}", actionsHint);628} else if (actionsHint) {629ariaLabel = localize('accessibility-help-hint', "Accessibility Help, {0}", actionsHint);630}631if (isWindows && widgetIsFocused) {632// prevent the screen reader on windows from reading633// the aria label again when it's refocused634ariaLabel = '';635}636this._editorWidget.updateOptions({ ariaLabel });637this._editorWidget.focus();638if (this._currentProvider?.options.position) {639const position = this._editorWidget.getPosition();640const isDefaultPosition = position?.lineNumber === 1 && position.column === 1;641if (this._currentProvider.options.position === 'bottom' || this._currentProvider.options.position === 'initial-bottom' && isDefaultPosition) {642const lastLine = this.editorWidget.getModel()?.getLineCount();643const position = lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;644if (position) {645this._editorWidget.setPosition(position);646this._editorWidget.revealLine(position.lineNumber);647}648}649} else if (previousPosition) {650this._editorWidget.setPosition(previousPosition);651} else {652// Restore the saved position for this provider if available (e.g., after close and reopen)653const savedPosition = this._lastProviderPosition.get(provider.id);654if (savedPosition) {655const lineCount = this._editorWidget.getModel()?.getLineCount() ?? 0;656// Only restore if the saved position is still valid within the current content657if (savedPosition.lineNumber <= lineCount) {658this._editorWidget.setPosition(savedPosition);659this._editorWidget.revealPosition(savedPosition);660}661}662}663});664this._updateToolbar(this._currentProvider.actions, provider.options.type);665666const hide = (e?: KeyboardEvent | IKeyboardEvent): void => {667const thisWindowIsFocused = getWindow(this._editorWidget.getDomNode()).document.hasFocus();668if (!thisWindowIsFocused) {669// When switching windows, keep accessible view open670e?.preventDefault();671e?.stopPropagation();672return;673}674if (!this._isInQuickPick) {675provider.onClose();676}677e?.stopPropagation();678this._contextViewService.hideContextView();679if (this._isInQuickPick) {680return;681}682this._updateContextKeys(provider, false);683// Save the cursor position for this provider so it can be restored on reopen684const currentPosition = this._editorWidget.getPosition();685if (currentPosition) {686this._lastProviderPosition.set(provider.id, currentPosition);687}688this._lastProvider = undefined;689this._currentContent = undefined;690this._currentProvider?.dispose();691this._currentProvider = undefined;692};693const disposableStore = new DisposableStore();694disposableStore.add(this._editorWidget.onKeyDown((e) => {695if (e.keyCode === KeyCode.Enter) {696this._commandService.executeCommand('editor.action.openLink');697} else if (e.keyCode === KeyCode.Escape || shouldHide(e.browserEvent, this._keybindingService, this._configurationService)) {698hide(e);699} else if (e.keyCode === KeyCode.KeyH && provider.options.readMoreUrl) {700const url: string = provider.options.readMoreUrl;701alert(AccessibilityHelpNLS.openingDocs);702this._openerService.open(URI.parse(url));703e.preventDefault();704e.stopPropagation();705}706if (provider instanceof AccessibleContentProvider) {707provider.onKeyDown?.(e);708}709}));710disposableStore.add(addDisposableListener(this._toolbar.getElement(), EventType.KEY_DOWN, (e: KeyboardEvent) => {711const keyboardEvent = new StandardKeyboardEvent(e);712if (keyboardEvent.equals(KeyCode.Escape)) {713hide(e);714}715}));716disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => {717if (!isActiveElement(this._toolbar.getElement())) {718hide();719}720}));721disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout()));722disposableStore.add(this._layoutService.onDidLayoutActiveContainer(() => this._layout()));723return disposableStore;724}725726private _updateToolbar(providedActions?: IAction[], type?: AccessibleViewType): void {727this._toolbar.setAriaLabel(type === AccessibleViewType.Help ? localize('accessibleHelpToolbar', 'Accessibility Help') : localize('accessibleViewToolbar', "Accessible View"));728const toolbarMenu = this._register(this._menuService.createMenu(MenuId.AccessibleView, this._contextKeyService));729const menuActions = getFlatActionBarActions(toolbarMenu.getActions({}));730if (providedActions) {731for (const providedAction of providedActions) {732providedAction.class = providedAction.class || ThemeIcon.asClassName(Codicon.primitiveSquare);733providedAction.checked = undefined;734}735this._toolbar.setActions([...providedActions, ...menuActions]);736} else {737this._toolbar.setActions(menuActions);738}739}740741private _layout(): void {742const dimension = this._layoutService.activeContainerDimension;743const maxHeight = dimension.height && dimension.height * .4;744const height = Math.min(maxHeight, this._editorWidget.getContentHeight());745const width = Math.min(dimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH);746this._editorWidget.layout({ width, height });747}748749private async _getTextModel(resource: URI): Promise<ITextModel | null> {750const existing = this._modelService.getModel(resource);751if (existing && !existing.isDisposed()) {752return existing;753}754// Create an empty model - content will be set via setValue() to preserve cursor position755return this._modelService.createModel('', null, resource, false);756}757758private _goToSymbolsSupported(): boolean {759if (!this._currentProvider) {760return false;761}762return this._currentProvider.options.type === AccessibleViewType.Help || this._currentProvider.options.language === 'markdown' || this._currentProvider.options.language === undefined || (this._currentProvider instanceof AccessibleContentProvider && !!this._currentProvider.getSymbols?.());763}764765private _updateLastProvider(): AccesibleViewContentProvider | undefined {766const provider = this._currentProvider;767if (!provider) {768return;769}770const lastProvider = provider instanceof AccessibleContentProvider ? new AccessibleContentProvider(771provider.id,772provider.options,773provider.provideContent.bind(provider),774provider.onClose.bind(provider),775provider.verbositySettingKey,776provider.onOpen?.bind(provider),777provider.actions,778provider.provideNextContent?.bind(provider),779provider.providePreviousContent?.bind(provider),780provider.onDidChangeContent?.bind(provider),781provider.onKeyDown?.bind(provider),782provider.getSymbols?.bind(provider),783) : new ExtensionContentProvider(784provider.id,785provider.options,786provider.provideContent.bind(provider),787provider.onClose.bind(provider),788provider.onOpen?.bind(provider),789provider.provideNextContent?.bind(provider),790provider.providePreviousContent?.bind(provider),791provider.actions,792provider.onDidChangeContent?.bind(provider),793);794return lastProvider;795}796797public showAccessibleViewHelp(): void {798const lastProvider = this._updateLastProvider();799if (!lastProvider) {800return;801}802let accessibleViewHelpProvider;803if (lastProvider instanceof AccessibleContentProvider) {804accessibleViewHelpProvider = new AccessibleContentProvider(805lastProvider.id,806{ type: AccessibleViewType.Help },807() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),808() => {809this._contextViewService.hideContextView();810// HACK: Delay to allow the context view to hide #207638811queueMicrotask(() => this.show(lastProvider));812},813lastProvider.verbositySettingKey814);815} else {816accessibleViewHelpProvider = new ExtensionContentProvider(817lastProvider.id,818{ type: AccessibleViewType.Help },819() => lastProvider.options.customHelp ? lastProvider?.options.customHelp() : this._accessibleViewHelpDialogContent(this._goToSymbolsSupported()),820() => {821this._contextViewService.hideContextView();822// HACK: Delay to allow the context view to hide #207638823queueMicrotask(() => this.show(lastProvider));824},825);826}827this._contextViewService.hideContextView();828// HACK: Delay to allow the context view to hide #186514829if (accessibleViewHelpProvider) {830queueMicrotask(() => this.show(accessibleViewHelpProvider, undefined, true));831}832}833834private _accessibleViewHelpDialogContent(providerHasSymbols?: boolean): string {835const navigationHint = this._navigationHint();836const goToSymbolHint = this._goToSymbolHint(providerHasSymbols);837const toolbarHint = localize('toolbar', "Navigate to the toolbar (Shift+Tab).");838const chatHints = this._getChatHints();839840let hint = localize('intro', "In the accessible view, you can:\n");841if (navigationHint) {842hint += ' - ' + navigationHint + '\n';843}844if (goToSymbolHint) {845hint += ' - ' + goToSymbolHint + '\n';846}847if (toolbarHint) {848hint += ' - ' + toolbarHint + '\n';849}850if (chatHints) {851hint += chatHints;852}853return hint;854}855856private _getChatHints(): string | undefined {857if (this._currentProvider?.id !== AccessibleViewProviderId.PanelChat && this._currentProvider?.id !== AccessibleViewProviderId.QuickChat) {858return;859}860return [localize('insertAtCursor', " - Insert the code block at the cursor{0}.", '<keybinding:workbench.action.chat.insertCodeBlock>'),861localize('insertIntoNewFile', " - Insert the code block into a new file{0}.", '<keybinding:workbench.action.chat.insertIntoNewFile>'),862localize('runInTerminal', " - Run the code block in the terminal{0}.\n", '<keybinding:workbench.action.chat.runInTerminal>')].join('\n');863}864865private _navigationHint(): string {866return localize('accessibleViewNextPreviousHint', "Show the next item{0} or previous item{1}.", `<keybinding:${AccessibilityCommandId.ShowNext}`, `<keybinding:${AccessibilityCommandId.ShowPrevious}>`);867}868869private _disableVerbosityHint(provider: AccesibleViewContentProvider): string {870if (provider.options.type === AccessibleViewType.Help && this._verbosityEnabled()) {871return localize('acessibleViewDisableHint', "\nDisable accessibility verbosity for this feature{0}.", `<keybinding:${AccessibilityCommandId.DisableVerbosityHint}>`);872}873return '';874}875876private _goToSymbolHint(providerHasSymbols?: boolean): string | undefined {877if (!providerHasSymbols) {878return;879}880return localize('goToSymbolHint', 'Go to a symbol{0}.', `<keybinding:${AccessibilityCommandId.GoToSymbol}>`);881}882883private _configureUnassignedKbHint(): string {884const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureKeybindings)?.getAriaLabel();885const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Unassigned Keybindings.';886return localize('configureKb', '\nConfigure keybindings for commands that lack them {0}.', keybindingToConfigureQuickPick);887}888889private _configureAssignedKbHint(): string {890const configureKb = this._keybindingService.lookupKeybinding(AccessibilityCommandId.AccessibilityHelpConfigureAssignedKeybindings)?.getAriaLabel();891const keybindingToConfigureQuickPick = configureKb ? '(' + configureKb + ')' : 'by assigning a keybinding to the command Accessibility Help Configure Assigned Keybindings.';892return localize('configureKbAssigned', '\nConfigure keybindings for commands that already have assignments {0}.', keybindingToConfigureQuickPick);893}894895private _screenReaderModeHint(provider: AccesibleViewContentProvider): string {896const accessibilitySupport = this._accessibilityService.isScreenReaderOptimized();897let screenReaderModeHint = '';898const turnOnMessage = (899isMacintosh900? AccessibilityHelpNLS.changeConfigToOnMac901: AccessibilityHelpNLS.changeConfigToOnWinLinux902);903if (accessibilitySupport && provider.id === AccessibleViewProviderId.Editor) {904screenReaderModeHint = AccessibilityHelpNLS.auto_on;905screenReaderModeHint += '\n';906} else if (!accessibilitySupport) {907screenReaderModeHint = AccessibilityHelpNLS.auto_off + '\n' + turnOnMessage;908screenReaderModeHint += '\n';909}910return screenReaderModeHint;911}912913private _exitDialogHint(provider: AccesibleViewContentProvider): string {914return this._verbosityEnabled() && !provider.options.position ? localize('exit', '\nExit this dialog (Escape).') : '';915}916917private _readMoreHint(provider: AccesibleViewContentProvider): string {918return provider.options.readMoreUrl ? localize("openDoc", "\nOpen a browser window with more information related to accessibility{0}.", `<keybinding:${AccessibilityCommandId.AccessibilityHelpOpenHelpLink}>`) : '';919}920}921922export class AccessibleViewService extends Disposable implements IAccessibleViewService {923declare readonly _serviceBrand: undefined;924private _accessibleView: AccessibleView | undefined;925926constructor(927@IInstantiationService private readonly _instantiationService: IInstantiationService,928@IConfigurationService private readonly _configurationService: IConfigurationService,929@IKeybindingService private readonly _keybindingService: IKeybindingService930) {931super();932}933934show(provider: AccesibleViewContentProvider, position?: Position): void {935if (!this._accessibleView) {936this._accessibleView = this._register(this._instantiationService.createInstance(AccessibleView));937}938this._accessibleView.show(provider, undefined, undefined, position);939}940configureKeybindings(unassigned: boolean): void {941this._accessibleView?.configureKeybindings(unassigned);942}943openHelpLink(): void {944this._accessibleView?.openHelpLink();945}946showLastProvider(id: AccessibleViewProviderId): void {947this._accessibleView?.showLastProvider(id);948}949next(): void {950this._accessibleView?.next();951}952previous(): void {953this._accessibleView?.previous();954}955goToSymbol(): void {956this._accessibleView?.goToSymbol();957}958getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {959if (!this._configurationService.getValue(verbositySettingKey)) {960return null;961}962const keybinding = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibleView)?.getAriaLabel();963let hint = null;964if (keybinding) {965hint = localize('acessibleViewHint', "Inspect this in the accessible view with {0}", keybinding);966} else {967hint = localize('acessibleViewHintNoKbEither', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.");968}969return hint;970}971disableHint(): void {972this._accessibleView?.disableHint();973}974showAccessibleViewHelp(): void {975this._accessibleView?.showAccessibleViewHelp();976}977getPosition(id: AccessibleViewProviderId): Position | undefined {978return this._accessibleView?.getPosition(id) ?? undefined;979}980getLastPosition(): Position | undefined {981const lastLine = this._accessibleView?.editorWidget.getModel()?.getLineCount();982return lastLine !== undefined && lastLine > 0 ? new Position(lastLine, 1) : undefined;983}984setPosition(position: Position, reveal?: boolean, select?: boolean): void {985this._accessibleView?.setPosition(position, reveal, select);986}987getCodeBlockContext(): ICodeBlockActionContext | undefined {988return this._accessibleView?.getCodeBlockContext();989}990navigateToCodeBlock(type: 'next' | 'previous'): void {991this._accessibleView?.navigateToCodeBlock(type);992}993}994995class AccessibleViewSymbolQuickPick {996constructor(private _accessibleView: AccessibleView, @IQuickInputService private readonly _quickInputService: IQuickInputService) {997998}999show(provider: AccesibleViewContentProvider): void {1000const disposables = new DisposableStore();1001const quickPick = disposables.add(this._quickInputService.createQuickPick<IAccessibleViewSymbol>());1002quickPick.placeholder = localize('accessibleViewSymbolQuickPickPlaceholder', "Type to search symbols");1003quickPick.title = localize('accessibleViewSymbolQuickPickTitle', "Go to Symbol Accessible View");1004const picks = [];1005const symbols = this._accessibleView.getSymbols();1006if (!symbols) {1007return;1008}1009for (const symbol of symbols) {1010picks.push({1011label: symbol.label,1012ariaLabel: symbol.ariaLabel,1013firstListItem: symbol.firstListItem,1014lineNumber: symbol.lineNumber,1015endLineNumber: symbol.endLineNumber,1016markdownToParse: symbol.markdownToParse1017});1018}1019quickPick.canSelectMany = false;1020quickPick.items = picks;1021quickPick.show();1022disposables.add(quickPick.onDidAccept(() => {1023this._accessibleView.showSymbol(provider, quickPick.selectedItems[0]);1024quickPick.hide();1025}));1026disposables.add(quickPick.onDidHide(() => {1027if (quickPick.selectedItems.length === 0) {1028// this was escaped, so refocus the accessible view1029this._accessibleView.show(provider);1030}1031disposables.dispose();1032}));1033}1034}103510361037function shouldHide(event: KeyboardEvent, keybindingService: IKeybindingService, configurationService: IConfigurationService): boolean {1038if (!configurationService.getValue(AccessibilityWorkbenchSettingId.AccessibleViewCloseOnKeyPress)) {1039return false;1040}1041const standardKeyboardEvent = new StandardKeyboardEvent(event);1042const resolveResult = keybindingService.softDispatch(standardKeyboardEvent, standardKeyboardEvent.target);10431044const isValidChord = resolveResult.kind === ResultKind.MoreChordsNeeded;1045if (keybindingService.inChordMode || isValidChord) {1046return false;1047}1048return shouldHandleKey(event) && !event.ctrlKey && !event.altKey && !event.metaKey && !event.shiftKey;1049}10501051function shouldHandleKey(event: KeyboardEvent): boolean {1052return !!event.code.match(/^(Key[A-Z]|Digit[0-9]|Equal|Comma|Period|Slash|Quote|Backquote|Backslash|Minus|Semicolon|Space|Enter)$/);1053}105410551056