add command and logic to start/stop reading the current line with inlay hints, https://github.com/microsoft/vscode/issues/142532

This commit is contained in:
Johannes Rieken 2022-02-11 11:33:14 +01:00
parent 7823305faf
commit 6e5373e758
No known key found for this signature in database
GPG key ID: 96634B5AF12F8798
2 changed files with 217 additions and 4 deletions

View file

@ -0,0 +1,135 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as dom from 'vs/base/browser/dom';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
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 { Command } from 'vs/editor/common/languages';
import { InlayHintItem } from 'vs/editor/contrib/inlayHints/browser/inlayHints';
import { localize } from 'vs/nls';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Link } from 'vs/platform/opener/browser/link';
export class InlayHintsAccessibility {
static readonly IsReading = new RawContextKey<boolean>('isReadingLineWithInlayHints', false, { type: 'boolean', description: localize('isReadingLineWithInlayHints', "Whether the current line and its inlay hints are currently focused") });
private readonly _ariaElement: HTMLSpanElement;
private readonly _ctxIsReading: IContextKey<boolean>;
private _sessionDispoosables = new DisposableStore();
constructor(
private readonly _editor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService,
@IInstantiationService private readonly _instaService: IInstantiationService,
) {
this._ariaElement = document.createElement('span');
this._ariaElement.style.position = 'fixed';
this._ariaElement.className = 'inlayhint-accessibility-element';
this._ariaElement.tabIndex = 0;
this._ariaElement.setAttribute('aria-description', localize('description', "Code with Inlay Hint Information"));
this._ctxIsReading = InlayHintsAccessibility.IsReading.bindTo(contextKeyService);
}
dispose(): void {
this._sessionDispoosables.dispose();
this._ctxIsReading.reset();
this._ariaElement.remove();
}
reset(): void {
dom.clearNode(this._ariaElement);
this._sessionDispoosables.clear();
this._ctxIsReading.reset();
}
async read(line: number, hints: InlayHintItem[]) {
this._sessionDispoosables.clear();
if (!this._ariaElement.isConnected) {
this._editor.getDomNode()?.appendChild(this._ariaElement);
}
if (!this._editor.hasModel() || !this._ariaElement.isConnected) {
this._ctxIsReading.set(false);
return;
}
const cts = new CancellationTokenSource();
this._sessionDispoosables.add(cts);
for (let hint of hints) {
await hint.resolve(cts.token);
}
if (cts.token.isCancellationRequested) {
return;
}
const model = this._editor.getModel();
// const text = this._editor.getModel().getLineContent(line);
const newChildren: (string | HTMLElement)[] = [];
let start = 0;
for (const item of hints) {
// text
const part = model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: item.hint.position.column });
if (part.length > 0) {
newChildren.push(part);
start = item.hint.position.column - 1;
}
// hint
const em = document.createElement('em');
const { label } = item.hint;
if (typeof label === 'string') {
em.innerText = label;
} else {
for (let part of label) {
if (part.command) {
const link = this._instaService.createInstance(Link, em,
{ href: InlayHintsAccessibility._asCommandLink(part.command), label: part.label, title: part.command.title },
undefined
);
this._sessionDispoosables.add(link);
} else {
em.innerText += part.label;
}
}
}
newChildren.push(em);
}
// trailing text
newChildren.push(model.getValueInRange({ startLineNumber: line, startColumn: start + 1, endLineNumber: line, endColumn: Number.MAX_SAFE_INTEGER }));
dom.reset(this._ariaElement, ...newChildren);
this._ariaElement.focus();
this._ctxIsReading.set(true);
// reset on blur
this._sessionDispoosables.add(dom.addDisposableListener(this._ariaElement, 'focusout', () => {
this.reset();
}));
}
private static _asCommandLink(command: Command): string {
return URI.from({
scheme: Schemas.command,
path: command.id,
query: encodeURIComponent(JSON.stringify(command.arguments))
}).toString();
}
}

View file

@ -6,6 +6,7 @@
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';
@ -13,9 +14,11 @@ 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';
@ -24,10 +27,14 @@ 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 } from 'vs/platform/instantiation/common/instantiation';
import { createDecorator, IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
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';
@ -82,8 +89,8 @@ export class InlayHintsController implements IEditorContribution {
private static readonly _MAX_DECORATORS = 1500;
static get(editor: ICodeEditor) {
return editor.getContribution(InlayHintsController.ID) ?? undefined;
static get(editor: ICodeEditor): InlayHintsController | undefined {
return editor.getContribution<InlayHintsController>(InlayHintsController.ID) ?? undefined;
}
private readonly _disposables = new DisposableStore();
@ -91,6 +98,7 @@ export class InlayHintsController implements IEditorContribution {
private readonly _debounceInfo: IFeatureDebounceInformation;
private readonly _decorationsMetadata = new Map<string, { item: InlayHintItem; classNameRef: IDisposable }>();
private readonly _ruleFactory = new DynamicCssRules(this._editor);
private readonly _accessibility: InlayHintsAccessibility;
private _activeInlayHintPart?: RenderedInlayHintLabelPart;
@ -103,6 +111,7 @@ 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()));
@ -523,9 +532,35 @@ export class InlayHintsController implements IEditorContribution {
}
this._decorationsMetadata.clear();
}
}
// --- accessibility
startInlayHintsReading(): void {
if (!this._editor.hasModel()) {
return;
}
const line = this._editor.getPosition().lineNumber;
const set = new Set<languages.InlayHint>();
const items: 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);
}
}
if (set.size > 0) {
this._accessibility.read(line, items);
}
}
stopInlayHintsReading(): void {
this._accessibility.reset();
this._editor.focus();
}
}
// Prevents the view from potentially visible whitespace
function fixSpace(str: string): string {
@ -533,6 +568,49 @@ 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<languages.InlayHint[]> => {
const [uri, range] = args;