Skip to content

Commit

Permalink
add command and logic to start/stop reading the current line with inl…
Browse files Browse the repository at this point in the history
…ay hints, #142532
  • Loading branch information
jrieken committed Feb 11, 2022
1 parent 7823305 commit 6e5373e
Show file tree
Hide file tree
Showing 2 changed files with 217 additions and 4 deletions.
135 changes: 135 additions & 0 deletions src/vs/editor/contrib/inlayHints/browser/inlayHintsAccessibility.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
86 changes: 82 additions & 4 deletions src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@
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';
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';
Expand All @@ -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';
Expand Down Expand Up @@ -82,15 +89,16 @@ 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();
private readonly _sessionDisposables = new DisposableStore();
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;

Expand All @@ -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()));
Expand Down Expand Up @@ -523,16 +532,85 @@ 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 {
const noBreakWhitespace = '\xa0';
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;
Expand Down

0 comments on commit 6e5373e

Please sign in to comment.