Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { CancellationTokenSource } from '../../../../base/common/cancellation.js';
8
import { KeyCode } from '../../../../base/common/keyCodes.js';
9
import { DisposableStore } from '../../../../base/common/lifecycle.js';
10
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
11
import { EditorAction2, EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';
12
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
13
import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js';
14
import { InlayHintItem, asCommandLink } from '../../../../editor/contrib/inlayHints/browser/inlayHints.js';
15
import { InlayHintsController } from '../../../../editor/contrib/inlayHints/browser/inlayHintsController.js';
16
import { localize, localize2 } from '../../../../nls.js';
17
import { registerAction2 } from '../../../../platform/actions/common/actions.js';
18
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
19
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
20
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
21
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
22
import { Link } from '../../../../platform/opener/browser/link.js';
23
24
25
export class InlayHintsAccessibility implements IEditorContribution {
26
27
static readonly IsReading = new RawContextKey<boolean>('isReadingLineWithInlayHints', false, { type: 'boolean', description: localize('isReadingLineWithInlayHints', "Whether the current line and its inlay hints are currently focused") });
28
29
static readonly ID: string = 'editor.contrib.InlayHintsAccessibility';
30
31
static get(editor: ICodeEditor): InlayHintsAccessibility | undefined {
32
return editor.getContribution<InlayHintsAccessibility>(InlayHintsAccessibility.ID) ?? undefined;
33
}
34
35
private readonly _ariaElement: HTMLSpanElement;
36
private readonly _ctxIsReading: IContextKey<boolean>;
37
38
private readonly _sessionDispoosables = new DisposableStore();
39
40
constructor(
41
private readonly _editor: ICodeEditor,
42
@IContextKeyService contextKeyService: IContextKeyService,
43
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,
44
@IInstantiationService private readonly _instaService: IInstantiationService,
45
) {
46
this._ariaElement = document.createElement('span');
47
this._ariaElement.style.position = 'fixed';
48
this._ariaElement.className = 'inlayhint-accessibility-element';
49
this._ariaElement.tabIndex = 0;
50
this._ariaElement.setAttribute('aria-description', localize('description', "Code with Inlay Hint Information"));
51
52
this._ctxIsReading = InlayHintsAccessibility.IsReading.bindTo(contextKeyService);
53
}
54
55
dispose(): void {
56
this._sessionDispoosables.dispose();
57
this._ctxIsReading.reset();
58
this._ariaElement.remove();
59
}
60
61
private _reset(): void {
62
dom.clearNode(this._ariaElement);
63
this._sessionDispoosables.clear();
64
this._ctxIsReading.reset();
65
}
66
67
private async _read(line: number, hints: InlayHintItem[]) {
68
69
this._sessionDispoosables.clear();
70
71
if (!this._ariaElement.isConnected) {
72
this._editor.getDomNode()?.appendChild(this._ariaElement);
73
}
74
75
if (!this._editor.hasModel() || !this._ariaElement.isConnected) {
76
this._ctxIsReading.set(false);
77
return;
78
}
79
80
const cts = new CancellationTokenSource();
81
this._sessionDispoosables.add(cts);
82
83
for (const hint of hints) {
84
await hint.resolve(cts.token);
85
}
86
87
if (cts.token.isCancellationRequested) {
88
return;
89
}
90
const model = this._editor.getModel();
91
// const text = this._editor.getModel().getLineContent(line);
92
const newChildren: (string | HTMLElement)[] = [];
93
94
let start = 0;
95
let tooLongToRead = false;
96
97
for (const item of hints) {
98
99
// text
100
const part = model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: item.hint.position.column });
101
if (part.length > 0) {
102
newChildren.push(part);
103
start = item.hint.position.column - 1;
104
}
105
106
// check length
107
if (start > 750) {
108
newChildren.push('…');
109
tooLongToRead = true;
110
break;
111
}
112
113
// hint
114
const em = document.createElement('em');
115
const { label } = item.hint;
116
if (typeof label === 'string') {
117
em.innerText = label;
118
} else {
119
for (const part of label) {
120
if (part.command) {
121
const link = this._instaService.createInstance(Link, em,
122
{ href: asCommandLink(part.command), label: part.label, title: part.command.title },
123
undefined
124
);
125
this._sessionDispoosables.add(link);
126
127
} else {
128
em.innerText += part.label;
129
}
130
}
131
}
132
newChildren.push(em);
133
}
134
135
// trailing text
136
if (!tooLongToRead) {
137
newChildren.push(model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: Number.MAX_SAFE_INTEGER }));
138
}
139
140
dom.reset(this._ariaElement, ...newChildren);
141
this._ariaElement.focus();
142
this._ctxIsReading.set(true);
143
144
// reset on blur
145
this._sessionDispoosables.add(dom.addDisposableListener(this._ariaElement, 'focusout', () => {
146
this._reset();
147
}));
148
}
149
150
151
152
startInlayHintsReading(): void {
153
if (!this._editor.hasModel()) {
154
return;
155
}
156
const line = this._editor.getPosition().lineNumber;
157
const hints = InlayHintsController.get(this._editor)?.getInlayHintsForLine(line);
158
if (!hints || hints.length === 0) {
159
this._accessibilitySignalService.playSignal(AccessibilitySignal.noInlayHints);
160
} else {
161
this._read(line, hints);
162
}
163
}
164
165
stopInlayHintsReading(): void {
166
this._reset();
167
this._editor.focus();
168
}
169
}
170
171
172
registerAction2(class StartReadHints extends EditorAction2 {
173
174
constructor() {
175
super({
176
id: 'inlayHints.startReadingLineWithHint',
177
title: localize2('read.title', "Read Line with Inlay Hints"),
178
precondition: EditorContextKeys.hasInlayHintsProvider,
179
f1: true
180
});
181
}
182
183
runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {
184
const ctrl = InlayHintsAccessibility.get(editor);
185
ctrl?.startInlayHintsReading();
186
}
187
});
188
189
registerAction2(class StopReadHints extends EditorAction2 {
190
191
constructor() {
192
super({
193
id: 'inlayHints.stopReadingLineWithHint',
194
title: localize2('stop.title', "Stop Inlay Hints Reading"),
195
precondition: InlayHintsAccessibility.IsReading,
196
f1: true,
197
keybinding: {
198
weight: KeybindingWeight.EditorContrib,
199
primary: KeyCode.Escape
200
}
201
});
202
}
203
204
runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) {
205
const ctrl = InlayHintsAccessibility.get(editor);
206
ctrl?.stopInlayHintsReading();
207
}
208
});
209
210
registerEditorContribution(InlayHintsAccessibility.ID, InlayHintsAccessibility, EditorContributionInstantiation.Lazy);
211
212