Path: blob/main/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.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 * as dom from '../../../../base/browser/dom.js';6import { CancellationTokenSource } from '../../../../base/common/cancellation.js';7import { KeyCode } from '../../../../base/common/keyCodes.js';8import { DisposableStore } from '../../../../base/common/lifecycle.js';9import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';10import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';11import { IEditorContribution } from '../../../../editor/common/editorCommon.js';12import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';13import { InlayHintItem, asCommandLink } from '../../../../editor/contrib/inlayHints/browser/inlayHints.js';14import { InlayHintsController } from '../../../../editor/contrib/inlayHints/browser/inlayHintsController.js';15import { localize, localize2 } from '../../../../nls.js';16import { registerAction2 } from '../../../../platform/actions/common/actions.js';17import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';18import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';19import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';20import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';21import { Link } from '../../../../platform/opener/browser/link.js';222324export class InlayHintsAccessibility implements IEditorContribution {2526static readonly IsReading = new RawContextKey<boolean>('isReadingLineWithInlayHints', false, { type: 'boolean', description: localize('isReadingLineWithInlayHints', "Whether the current line and its inlay hints are currently focused") });2728static readonly ID: string = 'editor.contrib.InlayHintsAccessibility';2930static get(editor: ICodeEditor): InlayHintsAccessibility | undefined {31return editor.getContribution<InlayHintsAccessibility>(InlayHintsAccessibility.ID) ?? undefined;32}3334private readonly _ariaElement: HTMLSpanElement;35private readonly _ctxIsReading: IContextKey<boolean>;3637private readonly _sessionDispoosables = new DisposableStore();3839constructor(40private readonly _editor: ICodeEditor,41@IContextKeyService contextKeyService: IContextKeyService,42@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,43@IInstantiationService private readonly _instaService: IInstantiationService,44) {45this._ariaElement = document.createElement('span');46this._ariaElement.style.position = 'fixed';47this._ariaElement.className = 'inlayhint-accessibility-element';48this._ariaElement.tabIndex = 0;49this._ariaElement.setAttribute('aria-description', localize('description', "Code with Inlay Hint Information"));5051this._ctxIsReading = InlayHintsAccessibility.IsReading.bindTo(contextKeyService);52}5354dispose(): void {55this._sessionDispoosables.dispose();56this._ctxIsReading.reset();57this._ariaElement.remove();58}5960private _reset(): void {61dom.clearNode(this._ariaElement);62this._sessionDispoosables.clear();63this._ctxIsReading.reset();64}6566private async _read(line: number, hints: InlayHintItem[]) {6768this._sessionDispoosables.clear();6970if (!this._ariaElement.isConnected) {71this._editor.getDomNode()?.appendChild(this._ariaElement);72}7374if (!this._editor.hasModel() || !this._ariaElement.isConnected) {75this._ctxIsReading.set(false);76return;77}7879const cts = new CancellationTokenSource();80this._sessionDispoosables.add(cts);8182for (const hint of hints) {83await hint.resolve(cts.token);84}8586if (cts.token.isCancellationRequested) {87return;88}89const model = this._editor.getModel();90// const text = this._editor.getModel().getLineContent(line);91const newChildren: (string | HTMLElement)[] = [];9293let start = 0;94let tooLongToRead = false;9596for (const item of hints) {9798// text99const part = model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: item.hint.position.column });100if (part.length > 0) {101newChildren.push(part);102start = item.hint.position.column - 1;103}104105// check length106if (start > 750) {107newChildren.push('…');108tooLongToRead = true;109break;110}111112// hint113const em = document.createElement('em');114const { label } = item.hint;115if (typeof label === 'string') {116em.innerText = label;117} else {118for (const part of label) {119if (part.command) {120const link = this._instaService.createInstance(Link, em,121{ href: asCommandLink(part.command), label: part.label, title: part.command.title },122undefined123);124this._sessionDispoosables.add(link);125126} else {127em.innerText += part.label;128}129}130}131newChildren.push(em);132}133134// trailing text135if (!tooLongToRead) {136newChildren.push(model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: Number.MAX_SAFE_INTEGER }));137}138139dom.reset(this._ariaElement, ...newChildren);140this._ariaElement.focus();141this._ctxIsReading.set(true);142143// reset on blur144this._sessionDispoosables.add(dom.addDisposableListener(this._ariaElement, 'focusout', () => {145this._reset();146}));147}148149150151startInlayHintsReading(): void {152if (!this._editor.hasModel()) {153return;154}155const line = this._editor.getPosition().lineNumber;156const hints = InlayHintsController.get(this._editor)?.getInlayHintsForLine(line);157if (!hints || hints.length === 0) {158this._accessibilitySignalService.playSignal(AccessibilitySignal.noInlayHints);159} else {160this._read(line, hints);161}162}163164stopInlayHintsReading(): void {165this._reset();166this._editor.focus();167}168}169170171registerAction2(class StartReadHints extends EditorAction2 {172173constructor() {174super({175id: 'inlayHints.startReadingLineWithHint',176title: localize2('read.title', "Read Line with Inlay Hints"),177precondition: EditorContextKeys.hasInlayHintsProvider,178f1: true179});180}181182runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {183const ctrl = InlayHintsAccessibility.get(editor);184ctrl?.startInlayHintsReading();185}186});187188registerAction2(class StopReadHints extends EditorAction2 {189190constructor() {191super({192id: 'inlayHints.stopReadingLineWithHint',193title: localize2('stop.title', "Stop Inlay Hints Reading"),194precondition: InlayHintsAccessibility.IsReading,195f1: true,196keybinding: {197weight: KeybindingWeight.EditorContrib,198primary: KeyCode.Escape199}200});201}202203runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {204const ctrl = InlayHintsAccessibility.get(editor);205ctrl?.stopInlayHintsReading();206}207});208209registerEditorContribution(InlayHintsAccessibility.ID, InlayHintsAccessibility, EditorContributionInstantiation.Lazy);210211212