Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.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 { disposableTimeout } from '../../../../base/common/async.js';
7
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
8
import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise, observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observable.js';
9
import { isDefined } from '../../../../base/common/types.js';
10
import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
11
import { Position } from '../../../../editor/common/core/position.js';
12
import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';
13
import { ITextModel } from '../../../../editor/common/model.js';
14
import { FoldingController } from '../../../../editor/contrib/folding/browser/folding.js';
15
import { AccessibilityModality, AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
16
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';
18
import { IWorkbenchContribution } from '../../../common/contributions.js';
19
import { IEditorService } from '../../../services/editor/common/editorService.js';
20
import { IDebugService } from '../../debug/common/debug.js';
21
22
export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution {
23
private readonly _textProperties: TextProperty[];
24
25
private readonly _someAccessibilitySignalIsEnabled;
26
27
private readonly _activeEditorObservable;
28
29
constructor(
30
@IEditorService private readonly _editorService: IEditorService,
31
@IInstantiationService private readonly _instantiationService: IInstantiationService,
32
@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService
33
) {
34
super();
35
this._textProperties = [
36
this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.errorAtPosition, AccessibilitySignal.errorOnLine, MarkerSeverity.Error),
37
this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.warningAtPosition, AccessibilitySignal.warningOnLine, MarkerSeverity.Warning),
38
this._instantiationService.createInstance(FoldedAreaTextProperty),
39
this._instantiationService.createInstance(BreakpointTextProperty),
40
];
41
this._someAccessibilitySignalIsEnabled = derived(this, reader =>
42
this._textProperties
43
.flatMap(p => [p.lineSignal, p.positionSignal])
44
.filter(isDefined)
45
.some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader))
46
);
47
this._activeEditorObservable = observableFromEvent(this,
48
this._editorService.onDidActiveEditorChange,
49
(_) => {
50
const activeTextEditorControl = this._editorService.activeTextEditorControl;
51
52
const editor = isDiffEditor(activeTextEditorControl)
53
? activeTextEditorControl.getOriginalEditor()
54
: isCodeEditor(activeTextEditorControl)
55
? activeTextEditorControl
56
: undefined;
57
58
return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined;
59
}
60
);
61
62
this._register(autorunWithStore((reader, store) => {
63
/** @description updateSignalsEnabled */
64
if (!this._someAccessibilitySignalIsEnabled.read(reader)) {
65
return;
66
}
67
const activeEditor = this._activeEditorObservable.read(reader);
68
if (activeEditor) {
69
this._registerAccessibilitySignalsForEditor(activeEditor.editor, activeEditor.model, store);
70
}
71
}));
72
}
73
74
private _registerAccessibilitySignalsForEditor(editor: ICodeEditor, editorModel: ITextModel, store: DisposableStore): void {
75
let lastLine = -1;
76
const ignoredLineSignalsForCurrentLine = new Set<TextProperty>();
77
78
const timeouts = store.add(new DisposableStore());
79
80
const propertySources = this._textProperties.map(p => ({ source: p.createSource(editor, editorModel), property: p }));
81
82
const didType = wasEventTriggeredRecently(editor.onDidChangeModelContent, 100, store);
83
84
store.add(editor.onDidChangeCursorPosition(args => {
85
timeouts.clear();
86
87
if (
88
args &&
89
args.reason !== CursorChangeReason.Explicit &&
90
args.reason !== CursorChangeReason.NotSet
91
) {
92
// Ignore cursor changes caused by navigation (e.g. which happens when execution is paused).
93
ignoredLineSignalsForCurrentLine.clear();
94
return;
95
}
96
97
const trigger = (property: TextProperty, source: TextPropertySource, mode: 'line' | 'positional') => {
98
const signal = mode === 'line' ? property.lineSignal : property.positionSignal;
99
if (
100
!signal
101
|| !this._accessibilitySignalService.getEnabledState(signal, false).value
102
|| !source.isPresent(position, mode, undefined)
103
) {
104
return;
105
}
106
107
for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) {
108
if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) {
109
const delay = this._accessibilitySignalService.getDelayMs(signal, modality, mode) + (didType.get() ? 1000 : 0);
110
111
timeouts.add(disposableTimeout(() => {
112
if (source.isPresent(position, mode, undefined)) {
113
if (!(mode === 'line') || !ignoredLineSignalsForCurrentLine.has(property)) {
114
this._accessibilitySignalService.playSignal(signal, { modality });
115
}
116
ignoredLineSignalsForCurrentLine.add(property);
117
}
118
}, delay));
119
}
120
}
121
};
122
123
// React to cursor changes
124
const position = args.position;
125
const lineNumber = position.lineNumber;
126
if (lineNumber !== lastLine) {
127
ignoredLineSignalsForCurrentLine.clear();
128
lastLine = lineNumber;
129
for (const p of propertySources) {
130
trigger(p.property, p.source, 'line');
131
}
132
}
133
for (const p of propertySources) {
134
trigger(p.property, p.source, 'positional');
135
}
136
137
// React to property state changes for the current cursor position
138
for (const s of propertySources) {
139
if (
140
![s.property.lineSignal, s.property.positionSignal]
141
.some(s => s && this._accessibilitySignalService.getEnabledState(s, false).value)
142
) {
143
return;
144
}
145
146
let lastValueAtPosition: boolean | undefined = undefined;
147
let lastValueOnLine: boolean | undefined = undefined;
148
timeouts.add(autorun(reader => {
149
const newValueAtPosition = s.source.isPresentAtPosition(args.position, reader);
150
const newValueOnLine = s.source.isPresentOnLine(args.position.lineNumber, reader);
151
152
if (lastValueAtPosition !== undefined && lastValueAtPosition !== undefined) {
153
if (!lastValueAtPosition && newValueAtPosition) {
154
trigger(s.property, s.source, 'positional');
155
}
156
if (!lastValueOnLine && newValueOnLine) {
157
trigger(s.property, s.source, 'line');
158
}
159
}
160
161
lastValueAtPosition = newValueAtPosition;
162
lastValueOnLine = newValueOnLine;
163
}));
164
}
165
}));
166
}
167
}
168
169
interface TextProperty {
170
readonly positionSignal?: AccessibilitySignal;
171
readonly lineSignal?: AccessibilitySignal;
172
readonly debounceWhileTyping?: boolean;
173
createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource;
174
}
175
176
class TextPropertySource {
177
public static notPresent = new TextPropertySource({ isPresentAtPosition: () => false, isPresentOnLine: () => false });
178
179
public readonly isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean;
180
public readonly isPresentAtPosition: (position: Position, reader: IReader | undefined) => boolean;
181
182
constructor(options: {
183
isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean;
184
isPresentAtPosition?: (position: Position, reader: IReader | undefined) => boolean;
185
}) {
186
this.isPresentOnLine = options.isPresentOnLine;
187
this.isPresentAtPosition = options.isPresentAtPosition ?? (() => false);
188
}
189
190
public isPresent(position: Position, mode: 'line' | 'positional', reader: IReader | undefined): boolean {
191
return mode === 'line' ? this.isPresentOnLine(position.lineNumber, reader) : this.isPresentAtPosition(position, reader);
192
}
193
}
194
195
class MarkerTextProperty implements TextProperty {
196
public readonly debounceWhileTyping = true;
197
constructor(
198
public readonly positionSignal: AccessibilitySignal,
199
public readonly lineSignal: AccessibilitySignal,
200
private readonly severity: MarkerSeverity,
201
@IMarkerService private readonly markerService: IMarkerService,
202
203
) { }
204
205
createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource {
206
const obs = observableSignalFromEvent('onMarkerChanged', this.markerService.onMarkerChanged);
207
return new TextPropertySource({
208
isPresentAtPosition: (position, reader) => {
209
obs.read(reader);
210
const hasMarker = this.markerService
211
.read({ resource: model.uri })
212
.some(
213
(m) =>
214
m.severity === this.severity &&
215
m.startLineNumber <= position.lineNumber &&
216
position.lineNumber <= m.endLineNumber &&
217
m.startColumn <= position.column &&
218
position.column <= m.endColumn
219
);
220
return hasMarker;
221
},
222
isPresentOnLine: (lineNumber, reader) => {
223
obs.read(reader);
224
const hasMarker = this.markerService
225
.read({ resource: model.uri })
226
.some(
227
(m) =>
228
m.severity === this.severity &&
229
m.startLineNumber <= lineNumber &&
230
lineNumber <= m.endLineNumber
231
);
232
return hasMarker;
233
}
234
});
235
}
236
}
237
238
class FoldedAreaTextProperty implements TextProperty {
239
public readonly lineSignal = AccessibilitySignal.foldedArea;
240
241
createSource(editor: ICodeEditor, _model: ITextModel): TextPropertySource {
242
const foldingController = FoldingController.get(editor);
243
if (!foldingController) { return TextPropertySource.notPresent; }
244
245
const foldingModel = observableFromPromise(foldingController.getFoldingModel() ?? Promise.resolve(undefined));
246
return new TextPropertySource({
247
isPresentOnLine(lineNumber, reader): boolean {
248
const m = foldingModel.read(reader);
249
const regionAtLine = m.value?.getRegionAtLine(lineNumber);
250
const hasFolding = !regionAtLine
251
? false
252
: regionAtLine.isCollapsed &&
253
regionAtLine.startLineNumber === lineNumber;
254
return hasFolding;
255
}
256
});
257
}
258
}
259
260
class BreakpointTextProperty implements TextProperty {
261
public readonly lineSignal = AccessibilitySignal.break;
262
263
constructor(@IDebugService private readonly debugService: IDebugService) { }
264
265
createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource {
266
const signal = observableSignalFromEvent('onDidChangeBreakpoints', this.debugService.getModel().onDidChangeBreakpoints);
267
const debugService = this.debugService;
268
return new TextPropertySource({
269
isPresentOnLine(lineNumber, reader): boolean {
270
signal.read(reader);
271
const breakpoints = debugService
272
.getModel()
273
.getBreakpoints({ uri: model.uri, lineNumber });
274
const hasBreakpoints = breakpoints.length > 0;
275
return hasBreakpoints;
276
}
277
});
278
}
279
}
280
281