mirror of
https://github.com/Microsoft/vscode
synced 2024-08-28 13:29:44 +00:00
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:
parent
7823305faf
commit
6e5373e758
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue