diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 34cbf9b0f59..305653d159c 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -98,6 +98,10 @@ "name": "vs/workbench/contrib/issue", "project": "vscode-workbench" }, + { + "name": "vs/workbench/contrib/inlayHints", + "project": "vscode-workbench" + }, { "name": "vs/workbench/contrib/interactive", "project": "vscode-workbench" diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 5a1975a5ab3..d9831b217ef 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -6,7 +6,6 @@ import { RunOnceScheduler } from 'vs/base/common/async'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; import { onUnexpectedError } from 'vs/base/common/errors'; -import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { LRUCache } from 'vs/base/common/map'; import { IRange } from 'vs/base/common/range'; @@ -14,11 +13,9 @@ import { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; import { IActiveCodeEditor, ICodeEditor, IEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { ClassNameReference, CssProperties, DynamicCssRules } from 'vs/editor/browser/editorDom'; -import { EditorAction2 } from 'vs/editor/browser/editorExtensions'; import { EditorOption, EDITOR_FONT_DEFAULTS } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; -import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import * as languages from 'vs/editor/common/languages'; import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel, TrackedRangeStickiness } from 'vs/editor/common/model'; import { ModelDecorationInjectedTextOptions } from 'vs/editor/common/model/textModel'; @@ -27,14 +24,10 @@ import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeat import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { ClickLinkGesture, ClickLinkMouseEvent } from 'vs/editor/contrib/gotoSymbol/browser/link/clickLinkGesture'; import { InlayHintAnchor, InlayHintItem, InlayHintsFragments } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; -import { InlayHintsAccessibility } from 'vs/editor/contrib/inlayHints/browser/inlayHintsAccessibility'; import { goToDefinitionWithLocation, showGoToContextMenu } from 'vs/editor/contrib/inlayHints/browser/inlayHintsLocations'; -import { localize } from 'vs/nls'; -import { registerAction2 } from 'vs/platform/actions/common/actions'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; -import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; -import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; +import { createDecorator, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { INotificationService, Severity } from 'vs/platform/notification/common/notification'; import * as colors from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; @@ -98,7 +91,6 @@ export class InlayHintsController implements IEditorContribution { private readonly _debounceInfo: IFeatureDebounceInformation; private readonly _decorationsMetadata = new Map(); private readonly _ruleFactory = new DynamicCssRules(this._editor); - private readonly _accessibility: InlayHintsAccessibility; private _activeInlayHintPart?: RenderedInlayHintLabelPart; @@ -111,7 +103,6 @@ export class InlayHintsController implements IEditorContribution { @INotificationService private readonly _notificationService: INotificationService, @IInstantiationService private readonly _instaService: IInstantiationService, ) { - this._accessibility = _instaService.createInstance(InlayHintsAccessibility, _editor); this._debounceInfo = _featureDebounce.for(_languageFeaturesService.inlayHintsProvider, 'InlayHint', { min: 25 }); this._disposables.add(_languageFeaturesService.inlayHintsProvider.onDidChange(() => this._update())); this._disposables.add(_editor.onDidChangeModel(() => this._update())); @@ -536,28 +527,20 @@ export class InlayHintsController implements IEditorContribution { // --- accessibility - startInlayHintsReading(): void { + getInlayHintsForLine(line: number): InlayHintItem[] { if (!this._editor.hasModel()) { - return; + return []; } - const line = this._editor.getPosition().lineNumber; const set = new Set(); - const items: InlayHintItem[] = []; + const result: InlayHintItem[] = []; for (let deco of this._editor.getLineDecorations(line)) { const data = this._decorationsMetadata.get(deco.id); if (data && !set.has(data.item.hint)) { set.add(data.item.hint); - items.push(data.item); + result.push(data.item); } } - if (set.size > 0) { - this._accessibility.read(line, items); - } - } - - stopInlayHintsReading(): void { - this._accessibility.reset(); - this._editor.focus(); + return result; } } @@ -568,47 +551,6 @@ function fixSpace(str: string): string { return str.replace(/[ \t]/g, noBreakWhitespace); } -registerAction2(class StartReadHints extends EditorAction2 { - - constructor() { - super({ - id: 'inlayHints.startReadingLineWithHint', - title: localize('read.title', 'Read Line With Inline Hints'), - precondition: EditorContextKeys.hasInlayHintsProvider, - f1: true - }); - } - - runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { - const ctrl = InlayHintsController.get(editor); - if (ctrl) { - ctrl.startInlayHintsReading(); - } - } -}); - -registerAction2(class StopReadHints extends EditorAction2 { - - constructor() { - super({ - id: 'inlayHints.stopReadingLineWithHint', - title: localize('stop.title', 'Stop Inlay Hints Reading'), - precondition: InlayHintsAccessibility.IsReading, - f1: true, - keybinding: { - weight: KeybindingWeight.EditorContrib, - primary: KeyCode.Escape - } - }); - } - - runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { - const ctrl = InlayHintsController.get(editor); - if (ctrl) { - ctrl.stopInlayHintsReading(); - } - } -}); CommandsRegistry.registerCommand('_executeInlayHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts index 235d1a43b89..185d71708cc 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCueService.ts @@ -165,6 +165,12 @@ export class AudioCue { settingsKey: 'audioCues.debuggerStoppedOnBreakpoint', }); + public static readonly noInlayHints = AudioCue.register({ + name: localize('audioClues.noInlayHints', 'Line has no Inlay Hints'), + sound: Sound.error, + settingsKey: 'audioClues.noInlayHints' + }); + private constructor( public readonly sound: Sound, public readonly name: string, diff --git a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts index 5bf6ffe5a87..4fe4dd3d18a 100644 --- a/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts +++ b/src/vs/workbench/contrib/audioCues/browser/audioCues.contribution.ts @@ -61,6 +61,10 @@ Registry.as(ConfigurationExtensions.Configuration).regis 'description': localize('audioCues.debuggerStoppedOnBreakpoint', "Plays a sound when the debugger stopped on a breakpoint."), ...audioCueFeatureBase, }, + 'audioCues.noInlayHints': { + 'description': localize('audioCues.noInlayHints', "Plays a sound when trying to read a line with inlay hints that has no inlay hints."), + ...audioCueFeatureBase, + }, } }); diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsAccessibility.ts b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts similarity index 62% rename from src/vs/editor/contrib/inlayHints/browser/inlayHintsAccessibility.ts rename to src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts index 5d20d44e950..2ffe6b8f280 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsAccessibility.ts +++ b/src/vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty.ts @@ -5,31 +5,45 @@ import * as dom from 'vs/base/browser/dom'; import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { KeyCode } from 'vs/base/common/keyCodes'; import { DisposableStore } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { URI } from 'vs/base/common/uri'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction2, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { IEditorContribution } from 'vs/editor/common/editorCommon'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { Command } from 'vs/editor/common/languages'; import { InlayHintItem } from 'vs/editor/contrib/inlayHints/browser/inlayHints'; +import { InlayHintsController } from 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; import { localize } from 'vs/nls'; +import { registerAction2 } from 'vs/platform/actions/common/actions'; import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; +import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { Link } from 'vs/platform/opener/browser/link'; +import { AudioCue, IAudioCueService } from 'vs/workbench/contrib/audioCues/browser/audioCueService'; -export class InlayHintsAccessibility { +export class InlayHintsAccessibility implements IEditorContribution { static readonly IsReading = new RawContextKey('isReadingLineWithInlayHints', false, { type: 'boolean', description: localize('isReadingLineWithInlayHints', "Whether the current line and its inlay hints are currently focused") }); + static readonly ID: string = 'editor.contrib.InlayHintsAccessibility'; + + static get(editor: ICodeEditor): InlayHintsAccessibility | undefined { + return editor.getContribution(InlayHintsAccessibility.ID) ?? undefined; + } + private readonly _ariaElement: HTMLSpanElement; private readonly _ctxIsReading: IContextKey; - private _sessionDispoosables = new DisposableStore(); constructor( private readonly _editor: ICodeEditor, @IContextKeyService contextKeyService: IContextKeyService, + @IAudioCueService private readonly _audioCueService: IAudioCueService, @IInstantiationService private readonly _instaService: IInstantiationService, ) { this._ariaElement = document.createElement('span'); @@ -47,13 +61,13 @@ export class InlayHintsAccessibility { this._ariaElement.remove(); } - reset(): void { + private _reset(): void { dom.clearNode(this._ariaElement); this._sessionDispoosables.clear(); this._ctxIsReading.reset(); } - async read(line: number, hints: InlayHintItem[]) { + private async _(line: number, hints: InlayHintItem[]) { this._sessionDispoosables.clear(); @@ -132,7 +146,7 @@ export class InlayHintsAccessibility { // reset on blur this._sessionDispoosables.add(dom.addDisposableListener(this._ariaElement, 'focusout', () => { - this.reset(); + this._reset(); })); } @@ -143,4 +157,68 @@ export class InlayHintsAccessibility { query: encodeURIComponent(JSON.stringify(command.arguments)) }).toString(); } + + + startInlayHintsReading(): void { + if (!this._editor.hasModel()) { + return; + } + const line = this._editor.getPosition().lineNumber; + const hints = InlayHintsController.get(this._editor)?.getInlayHintsForLine(line); + if (!hints || hints.length === 0) { + this._audioCueService.playAudioCue(AudioCue.noInlayHints); + } else { + this._(line, hints); + } + } + + stopInlayHintsReading(): void { + this._reset(); + this._editor.focus(); + } } + + +registerAction2(class StartReadHints extends EditorAction2 { + + constructor() { + super({ + id: 'inlayHints.startReadingLineWithHint', + title: localize('read.title', 'Read Line With Inline Hints'), + precondition: EditorContextKeys.hasInlayHintsProvider, + f1: true + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { + const ctrl = InlayHintsAccessibility.get(editor); + if (ctrl) { + ctrl.startInlayHintsReading(); + } + } +}); + +registerAction2(class StopReadHints extends EditorAction2 { + + constructor() { + super({ + id: 'inlayHints.stopReadingLineWithHint', + title: localize('stop.title', 'Stop Inlay Hints Reading'), + precondition: InlayHintsAccessibility.IsReading, + f1: true, + keybinding: { + weight: KeybindingWeight.EditorContrib, + primary: KeyCode.Escape + } + }); + } + + runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor) { + const ctrl = InlayHintsAccessibility.get(editor); + if (ctrl) { + ctrl.stopInlayHintsReading(); + } + } +}); + +registerEditorContribution(InlayHintsAccessibility.ID, InlayHintsAccessibility); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index f7902895c08..6880a01741a 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -269,6 +269,9 @@ import 'vs/workbench/contrib/snippets/browser/tabCompletion'; // Formatter Help import 'vs/workbench/contrib/format/browser/format.contribution'; +// Inlay Hint Accessibility +import 'vs/workbench/contrib/inlayHints/browser/inlayHintsAccessibilty'; + // Themes import 'vs/workbench/contrib/themes/browser/themes.contribution';