Path: blob/main/src/vs/workbench/contrib/accessibilitySignals/browser/editorTextPropertySignalsContribution.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 { disposableTimeout } from '../../../../base/common/async.js';6import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';7import { IReader, autorun, autorunWithStore, derived, observableFromEvent, observableFromPromise, observableFromValueWithChangeEvent, observableSignalFromEvent, wasEventTriggeredRecently } from '../../../../base/common/observable.js';8import { isDefined } from '../../../../base/common/types.js';9import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';10import { Position } from '../../../../editor/common/core/position.js';11import { CursorChangeReason } from '../../../../editor/common/cursorEvents.js';12import { ITextModel } from '../../../../editor/common/model.js';13import { FoldingController } from '../../../../editor/contrib/folding/browser/folding.js';14import { AccessibilityModality, AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';15import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';16import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js';17import { IWorkbenchContribution } from '../../../common/contributions.js';18import { IEditorService } from '../../../services/editor/common/editorService.js';19import { IDebugService } from '../../debug/common/debug.js';2021export class EditorTextPropertySignalsContribution extends Disposable implements IWorkbenchContribution {22private readonly _textProperties: TextProperty[];2324private readonly _someAccessibilitySignalIsEnabled;2526private readonly _activeEditorObservable;2728constructor(29@IEditorService private readonly _editorService: IEditorService,30@IInstantiationService private readonly _instantiationService: IInstantiationService,31@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService32) {33super();34this._textProperties = [35this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.errorAtPosition, AccessibilitySignal.errorOnLine, MarkerSeverity.Error),36this._instantiationService.createInstance(MarkerTextProperty, AccessibilitySignal.warningAtPosition, AccessibilitySignal.warningOnLine, MarkerSeverity.Warning),37this._instantiationService.createInstance(FoldedAreaTextProperty),38this._instantiationService.createInstance(BreakpointTextProperty),39];40this._someAccessibilitySignalIsEnabled = derived(this, reader =>41this._textProperties42.flatMap(p => [p.lineSignal, p.positionSignal])43.filter(isDefined)44.some(signal => observableFromValueWithChangeEvent(this, this._accessibilitySignalService.getEnabledState(signal, false)).read(reader))45);46this._activeEditorObservable = observableFromEvent(this,47this._editorService.onDidActiveEditorChange,48(_) => {49const activeTextEditorControl = this._editorService.activeTextEditorControl;5051const editor = isDiffEditor(activeTextEditorControl)52? activeTextEditorControl.getOriginalEditor()53: isCodeEditor(activeTextEditorControl)54? activeTextEditorControl55: undefined;5657return editor && editor.hasModel() ? { editor, model: editor.getModel() } : undefined;58}59);6061this._register(autorunWithStore((reader, store) => {62/** @description updateSignalsEnabled */63if (!this._someAccessibilitySignalIsEnabled.read(reader)) {64return;65}66const activeEditor = this._activeEditorObservable.read(reader);67if (activeEditor) {68this._registerAccessibilitySignalsForEditor(activeEditor.editor, activeEditor.model, store);69}70}));71}7273private _registerAccessibilitySignalsForEditor(editor: ICodeEditor, editorModel: ITextModel, store: DisposableStore): void {74let lastLine = -1;75const ignoredLineSignalsForCurrentLine = new Set<TextProperty>();7677const timeouts = store.add(new DisposableStore());7879const propertySources = this._textProperties.map(p => ({ source: p.createSource(editor, editorModel), property: p }));8081const didType = wasEventTriggeredRecently(editor.onDidChangeModelContent, 100, store);8283store.add(editor.onDidChangeCursorPosition(args => {84timeouts.clear();8586if (87args &&88args.reason !== CursorChangeReason.Explicit &&89args.reason !== CursorChangeReason.NotSet90) {91// Ignore cursor changes caused by navigation (e.g. which happens when execution is paused).92ignoredLineSignalsForCurrentLine.clear();93return;94}9596const trigger = (property: TextProperty, source: TextPropertySource, mode: 'line' | 'positional') => {97const signal = mode === 'line' ? property.lineSignal : property.positionSignal;98if (99!signal100|| !this._accessibilitySignalService.getEnabledState(signal, false).value101|| !source.isPresent(position, mode, undefined)102) {103return;104}105106for (const modality of ['sound', 'announcement'] as AccessibilityModality[]) {107if (this._accessibilitySignalService.getEnabledState(signal, false, modality).value) {108const delay = this._accessibilitySignalService.getDelayMs(signal, modality, mode) + (didType.get() ? 1000 : 0);109110timeouts.add(disposableTimeout(() => {111if (source.isPresent(position, mode, undefined)) {112if (!(mode === 'line') || !ignoredLineSignalsForCurrentLine.has(property)) {113this._accessibilitySignalService.playSignal(signal, { modality });114}115ignoredLineSignalsForCurrentLine.add(property);116}117}, delay));118}119}120};121122// React to cursor changes123const position = args.position;124const lineNumber = position.lineNumber;125if (lineNumber !== lastLine) {126ignoredLineSignalsForCurrentLine.clear();127lastLine = lineNumber;128for (const p of propertySources) {129trigger(p.property, p.source, 'line');130}131}132for (const p of propertySources) {133trigger(p.property, p.source, 'positional');134}135136// React to property state changes for the current cursor position137for (const s of propertySources) {138if (139![s.property.lineSignal, s.property.positionSignal]140.some(s => s && this._accessibilitySignalService.getEnabledState(s, false).value)141) {142return;143}144145let lastValueAtPosition: boolean | undefined = undefined;146let lastValueOnLine: boolean | undefined = undefined;147timeouts.add(autorun(reader => {148const newValueAtPosition = s.source.isPresentAtPosition(args.position, reader);149const newValueOnLine = s.source.isPresentOnLine(args.position.lineNumber, reader);150151if (lastValueAtPosition !== undefined && lastValueAtPosition !== undefined) {152if (!lastValueAtPosition && newValueAtPosition) {153trigger(s.property, s.source, 'positional');154}155if (!lastValueOnLine && newValueOnLine) {156trigger(s.property, s.source, 'line');157}158}159160lastValueAtPosition = newValueAtPosition;161lastValueOnLine = newValueOnLine;162}));163}164}));165}166}167168interface TextProperty {169readonly positionSignal?: AccessibilitySignal;170readonly lineSignal?: AccessibilitySignal;171readonly debounceWhileTyping?: boolean;172createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource;173}174175class TextPropertySource {176public static notPresent = new TextPropertySource({ isPresentAtPosition: () => false, isPresentOnLine: () => false });177178public readonly isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean;179public readonly isPresentAtPosition: (position: Position, reader: IReader | undefined) => boolean;180181constructor(options: {182isPresentOnLine: (lineNumber: number, reader: IReader | undefined) => boolean;183isPresentAtPosition?: (position: Position, reader: IReader | undefined) => boolean;184}) {185this.isPresentOnLine = options.isPresentOnLine;186this.isPresentAtPosition = options.isPresentAtPosition ?? (() => false);187}188189public isPresent(position: Position, mode: 'line' | 'positional', reader: IReader | undefined): boolean {190return mode === 'line' ? this.isPresentOnLine(position.lineNumber, reader) : this.isPresentAtPosition(position, reader);191}192}193194class MarkerTextProperty implements TextProperty {195public readonly debounceWhileTyping = true;196constructor(197public readonly positionSignal: AccessibilitySignal,198public readonly lineSignal: AccessibilitySignal,199private readonly severity: MarkerSeverity,200@IMarkerService private readonly markerService: IMarkerService,201202) { }203204createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource {205const obs = observableSignalFromEvent('onMarkerChanged', this.markerService.onMarkerChanged);206return new TextPropertySource({207isPresentAtPosition: (position, reader) => {208obs.read(reader);209const hasMarker = this.markerService210.read({ resource: model.uri })211.some(212(m) =>213m.severity === this.severity &&214m.startLineNumber <= position.lineNumber &&215position.lineNumber <= m.endLineNumber &&216m.startColumn <= position.column &&217position.column <= m.endColumn218);219return hasMarker;220},221isPresentOnLine: (lineNumber, reader) => {222obs.read(reader);223const hasMarker = this.markerService224.read({ resource: model.uri })225.some(226(m) =>227m.severity === this.severity &&228m.startLineNumber <= lineNumber &&229lineNumber <= m.endLineNumber230);231return hasMarker;232}233});234}235}236237class FoldedAreaTextProperty implements TextProperty {238public readonly lineSignal = AccessibilitySignal.foldedArea;239240createSource(editor: ICodeEditor, _model: ITextModel): TextPropertySource {241const foldingController = FoldingController.get(editor);242if (!foldingController) { return TextPropertySource.notPresent; }243244const foldingModel = observableFromPromise(foldingController.getFoldingModel() ?? Promise.resolve(undefined));245return new TextPropertySource({246isPresentOnLine(lineNumber, reader): boolean {247const m = foldingModel.read(reader);248const regionAtLine = m.value?.getRegionAtLine(lineNumber);249const hasFolding = !regionAtLine250? false251: regionAtLine.isCollapsed &&252regionAtLine.startLineNumber === lineNumber;253return hasFolding;254}255});256}257}258259class BreakpointTextProperty implements TextProperty {260public readonly lineSignal = AccessibilitySignal.break;261262constructor(@IDebugService private readonly debugService: IDebugService) { }263264createSource(editor: ICodeEditor, model: ITextModel): TextPropertySource {265const signal = observableSignalFromEvent('onDidChangeBreakpoints', this.debugService.getModel().onDidChangeBreakpoints);266const debugService = this.debugService;267return new TextPropertySource({268isPresentOnLine(lineNumber, reader): boolean {269signal.read(reader);270const breakpoints = debugService271.getModel()272.getBreakpoints({ uri: model.uri, lineNumber });273const hasBreakpoints = breakpoints.length > 0;274return hasBreakpoints;275}276});277}278}279280281