Adding accessibility help for verbose hover (#212783)

* adding code in order to provide accessibility help for the hover

* adding the description of the possible commands that can be used

* reusing the method

* joining the content and changing the text in the help view

* polishing the code

* removing the question mark

* changing the import

* removing the setting ID from imports

* adding code in order to update when the hover updates

* adding methods to service

* adding code in order to dispose the accessible hover view

* fixing bug

* polishing the code

* checking that action not supported for the early return

* using disposable store instead

* using the appropriate string

* polishing the code

* using instead the type help and the resolved keybindings

* hiding also on the `onDidBlurEditorWidget` firing

* Revert "using instead the type help and the resolved keybindings"

This reverts commit 1f450dd535.

* use hover accessible view, provide custom help

* Revert "Revert "using instead the type help and the resolved keybindings""

This reverts commit 12f0cf6143.

* add HoverAccessibilityHelp, BaseHoverAccessibleViewProvider

* polishing the code

* polishing the code

* provide content at a specific index from the hover accessibility help provider

* introducing method _initializeOptions

* using readonly where possible

* using public everywhere

* using a getter for the actions

---------

Co-authored-by: meganrogge <megan.rogge@microsoft.com>
This commit is contained in:
Aiday Marlen Kyzy 2024-05-28 09:44:23 +02:00 committed by GitHub
parent 49eedd7d51
commit 7f55a08120
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 394 additions and 92 deletions

View file

@ -23,6 +23,7 @@ import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHover
import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer';
import { ContentHoverVisibleData, HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes';
import { EditorHoverStatusBar } from 'vs/editor/contrib/hover/browser/contentHoverStatusBar';
import { Emitter } from 'vs/base/common/event';
export class ContentHoverController extends Disposable implements IHoverWidget {
@ -35,6 +36,9 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined;
private readonly _hoverOperation: HoverOperation<IHoverPart>;
private readonly _onContentsChanged = this._register(new Emitter<void>());
public readonly onContentsChanged = this._onContentsChanged.event;
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ -214,7 +218,7 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
fragment,
statusBar,
setColorPicker: (widget) => colorPicker = widget,
onContentsChanged: () => this._widget.onContentsChanged(),
onContentsChanged: () => this._doOnContentsChanged(),
setMinimumDimensions: (dimensions: dom.Dimension) => this._widget.setMinimumDimensions(dimensions),
hide: () => this.hide()
};
@ -261,6 +265,11 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
}
}
private _doOnContentsChanged(): void {
this._onContentsChanged.fire();
this._widget.onContentsChanged();
}
private static readonly _DECORATION_OPTIONS = ModelDecorationOptions.register({
description: 'content-hover-highlight',
className: 'hoverHighlight'
@ -351,8 +360,20 @@ export class ContentHoverController extends Disposable implements IHoverWidget {
this._startShowingOrUpdateHover(new HoverRangeAnchor(0, range, undefined, undefined), mode, source, focus, null);
}
public async updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): Promise<void> {
this._markdownHoverParticipant?.updateFocusedMarkdownHoverPartVerbosityLevel(action);
public async updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): Promise<void> {
this._markdownHoverParticipant?.updateMarkdownHoverVerbosityLevel(action, index, focus);
}
public focusedMarkdownHoverIndex(): number {
return this._markdownHoverParticipant?.focusedMarkdownHoverIndex() ?? -1;
}
public markdownHoverContentAtIndex(index: number): string {
return this._markdownHoverParticipant?.markdownHoverContentAtIndex(index) ?? '';
}
public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
return this._markdownHoverParticipant?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false;
}
public getWidgetContent(): string | undefined {

View file

@ -2,48 +2,252 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { localize } from 'vs/nls';
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
import { AccessibleViewType, AccessibleViewProviderId } from 'vs/platform/accessibility/browser/accessibleView';
import { AccessibleViewType, AccessibleViewProviderId, AdvancedContentProvider, IAccessibleViewContentProvider, IAccessibleViewOptions } from 'vs/platform/accessibility/browser/accessibleView';
import { IAccessibleViewImplentation } from 'vs/platform/accessibility/browser/accessibleViewRegistry';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IHoverService } from 'vs/platform/hover/browser/hover';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { HoverVerbosityAction } from 'vs/editor/common/languages';
import { DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_LABEL, INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_LABEL } from 'vs/editor/contrib/hover/browser/hoverActionIds';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { Action, IAction } from 'vs/base/common/actions';
import { ThemeIcon } from 'vs/base/common/themables';
import { Codicon } from 'vs/base/common/codicons';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
namespace HoverAccessibilityHelpNLS {
export const intro = localize('intro', "The hover widget is focused. Press the Tab key to cycle through the hover parts.");
export const increaseVerbosity = localize('increaseVerbosity', "- The focused hover part verbosity level can be increased with the Increase Hover Verbosity command<keybinding:{0}>.", INCREASE_HOVER_VERBOSITY_ACTION_ID);
export const decreaseVerbosity = localize('decreaseVerbosity', "- The focused hover part verbosity level can be decreased with the Decrease Hover Verbosity command<keybinding:{0}>.", DECREASE_HOVER_VERBOSITY_ACTION_ID);
export const hoverContent = localize('contentHover', "The last focused hover content is the following.");
}
export class HoverAccessibleView implements IAccessibleViewImplentation {
readonly type = AccessibleViewType.View;
readonly priority = 95;
readonly name = 'hover';
readonly when = EditorContextKeys.hoverFocused;
getProvider(accessor: ServicesAccessor) {
public readonly type = AccessibleViewType.View;
public readonly priority = 95;
public readonly name = 'hover';
public readonly when = EditorContextKeys.hoverFocused;
private _provider: HoverAccessibleViewProvider | undefined;
getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const codeEditorService = accessor.get(ICodeEditorService);
const editor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
const editorHoverContent = editor ? HoverController.get(editor)?.getWidgetContent() ?? undefined : undefined;
if (!editor || !editorHoverContent) {
const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!codeEditor) {
throw new Error('No active or focused code editor');
}
const hoverController = HoverController.get(codeEditor);
if (!hoverController) {
return;
}
return {
id: AccessibleViewProviderId.Hover,
verbositySettingKey: 'accessibility.verbosity.hover',
provideContent() { return editorHoverContent; },
onClose() {
HoverController.get(editor)?.focus();
},
options: {
language: editor?.getModel()?.getLanguageId() ?? 'typescript',
type: AccessibleViewType.View
}
};
this._provider = accessor.get(IInstantiationService).createInstance(HoverAccessibleViewProvider, codeEditor, hoverController);
return this._provider;
}
dispose(): void {
this._provider?.dispose();
}
}
export class HoverAccessibilityHelp implements IAccessibleViewImplentation {
public readonly priority = 100;
public readonly name = 'hover';
public readonly type = AccessibleViewType.Help;
public readonly when = EditorContextKeys.hoverVisible;
private _provider: HoverAccessibleViewProvider | undefined;
getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const codeEditorService = accessor.get(ICodeEditorService);
const codeEditor = codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor();
if (!codeEditor) {
throw new Error('No active or focused code editor');
}
const hoverController = HoverController.get(codeEditor);
if (!hoverController) {
return;
}
return accessor.get(IInstantiationService).createInstance(HoverAccessibilityHelpProvider, hoverController);
}
dispose(): void {
this._provider?.dispose();
}
}
abstract class BaseHoverAccessibleViewProvider extends Disposable implements IAccessibleViewContentProvider {
abstract provideContent(): string;
abstract options: IAccessibleViewOptions;
public readonly id = AccessibleViewProviderId.Hover;
public readonly verbositySettingKey = 'accessibility.verbosity.hover';
private readonly _onDidChangeContent: Emitter<void> = this._register(new Emitter<void>());
public readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
protected _markdownHoverFocusedIndex: number = -1;
private _onHoverContentsChanged: IDisposable | undefined;
constructor(
protected readonly _hoverController: HoverController,
) {
super();
}
public onOpen(): void {
if (!this._hoverController) {
return;
}
this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = true;
this._markdownHoverFocusedIndex = this._hoverController.focusedMarkdownHoverIndex();
this._onHoverContentsChanged = this._register(this._hoverController.onHoverContentsChanged(() => {
this._onDidChangeContent.fire();
}));
}
public onClose(): void {
if (!this._hoverController) {
return;
}
this._markdownHoverFocusedIndex = -1;
this._hoverController.focus();
this._hoverController.shouldKeepOpenOnEditorMouseMoveOrLeave = false;
this._onHoverContentsChanged?.dispose();
}
}
export class HoverAccessibilityHelpProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider {
public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.Help };
constructor(
hoverController: HoverController,
) {
super(hoverController);
}
provideContent(): string {
return this.provideContentAtIndex(this._markdownHoverFocusedIndex);
}
provideContentAtIndex(index: number): string {
const content: string[] = [];
content.push(HoverAccessibilityHelpNLS.intro);
content.push(...this._descriptionsOfVerbosityActionsForIndex(index));
content.push(...this._descriptionOfFocusedMarkdownHoverAtIndex(index));
return content.join('\n');
}
private _descriptionsOfVerbosityActionsForIndex(index: number): string[] {
const content: string[] = [];
const descriptionForIncreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Increase, index);
if (descriptionForIncreaseAction !== undefined) {
content.push(descriptionForIncreaseAction);
}
const descriptionForDecreaseAction = this._descriptionOfVerbosityActionForIndex(HoverVerbosityAction.Decrease, index);
if (descriptionForDecreaseAction !== undefined) {
content.push(descriptionForDecreaseAction);
}
return content;
}
private _descriptionOfVerbosityActionForIndex(action: HoverVerbosityAction, index: number): string | undefined {
const isActionSupported = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action);
if (!isActionSupported) {
return;
}
switch (action) {
case HoverVerbosityAction.Increase:
return HoverAccessibilityHelpNLS.increaseVerbosity;
case HoverVerbosityAction.Decrease:
return HoverAccessibilityHelpNLS.decreaseVerbosity;
}
}
protected _descriptionOfFocusedMarkdownHoverAtIndex(index: number): string[] {
const content: string[] = [];
const hoverContent = this._hoverController.markdownHoverContentAtIndex(index);
if (hoverContent) {
content.push('\n' + HoverAccessibilityHelpNLS.hoverContent);
content.push('\n' + hoverContent);
}
return content;
}
}
export class HoverAccessibleViewProvider extends BaseHoverAccessibleViewProvider implements IAccessibleViewContentProvider {
public readonly options: IAccessibleViewOptions = { type: AccessibleViewType.View };
constructor(
private readonly _editor: ICodeEditor,
hoverController: HoverController,
) {
super(hoverController);
this._initializeOptions(this._editor, hoverController);
}
public provideContent(): string {
const hoverContent = this._hoverController.markdownHoverContentAtIndex(this._markdownHoverFocusedIndex);
return hoverContent.length > 0 ? hoverContent : HoverAccessibilityHelpNLS.intro;
}
public get actions(): IAction[] {
const actions: IAction[] = [];
actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Increase));
actions.push(this._getActionFor(this._editor, HoverVerbosityAction.Decrease));
return actions;
}
private _getActionFor(editor: ICodeEditor, action: HoverVerbosityAction): IAction {
let actionId: string;
let accessibleActionId: string;
let actionLabel: string;
let actionCodicon: ThemeIcon;
switch (action) {
case HoverVerbosityAction.Increase:
actionId = INCREASE_HOVER_VERBOSITY_ACTION_ID;
accessibleActionId = INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID;
actionLabel = INCREASE_HOVER_VERBOSITY_ACTION_LABEL;
actionCodicon = Codicon.add;
break;
case HoverVerbosityAction.Decrease:
actionId = DECREASE_HOVER_VERBOSITY_ACTION_ID;
accessibleActionId = DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID;
actionLabel = DECREASE_HOVER_VERBOSITY_ACTION_LABEL;
actionCodicon = Codicon.remove;
break;
}
const actionEnabled = this._hoverController.doesMarkdownHoverAtIndexSupportVerbosityAction(this._markdownHoverFocusedIndex, action);
return new Action(accessibleActionId, actionLabel, ThemeIcon.asClassName(actionCodicon), actionEnabled, () => {
editor.getAction(actionId)?.run({ index: this._markdownHoverFocusedIndex, focus: false });
});
}
private _initializeOptions(editor: ICodeEditor, hoverController: HoverController): void {
const helpProvider = this._register(new HoverAccessibilityHelpProvider(hoverController));
this.options.language = editor.getModel()?.getLanguageId();
this.options.customHelp = () => { return helpProvider.provideContentAtIndex(this._markdownHoverFocusedIndex); };
}
}
export class ExtHoverAccessibleView implements IAccessibleViewImplentation {
readonly type = AccessibleViewType.View;
readonly priority = 90;
readonly name = 'extension-hover';
getProvider(accessor: ServicesAccessor) {
public readonly type = AccessibleViewType.View;
public readonly priority = 90;
public readonly name = 'extension-hover';
getProvider(accessor: ServicesAccessor): AdvancedContentProvider | undefined {
const contextViewService = accessor.get(IContextViewService);
const contextViewElement = contextViewService.getContextViewElement();
const extensionHoverContent = contextViewElement?.textContent ?? undefined;
@ -63,4 +267,6 @@ export class ExtHoverAccessibleView implements IAccessibleViewImplentation {
options: { language: 'typescript', type: AccessibleViewType.View }
};
}
dispose() { }
}

View file

@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
export const SHOW_OR_FOCUS_HOVER_ACTION_ID = 'editor.action.showHover';
export const SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID = 'editor.action.showDefinitionPreviewHover';
@ -14,4 +15,8 @@ export const PAGE_DOWN_HOVER_ACTION_ID = 'editor.action.pageDownHover';
export const GO_TO_TOP_HOVER_ACTION_ID = 'editor.action.goToTopHover';
export const GO_TO_BOTTOM_HOVER_ACTION_ID = 'editor.action.goToBottomHover';
export const INCREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.increaseHoverVerbosityLevel';
export const INCREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.increaseHoverVerbosityLevelFromAccessibleView';
export const INCREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'increaseHoverVerbosityLevel', comment: ['Label for action that will increase the hover verbosity level.'] }, "Increase Hover Verbosity Level");
export const DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel';
export const DECREASE_HOVER_VERBOSITY_ACCESSIBLE_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevelFromAccessibleView';
export const DECREASE_HOVER_VERBOSITY_ACTION_LABEL = nls.localize({ key: 'decreaseHoverVerbosityLevel', comment: ['Label for action that will decrease the hover verbosity level.'] }, "Decrease Hover Verbosity Level");

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DECREASE_HOVER_VERBOSITY_ACTION_ID, GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, PAGE_DOWN_HOVER_ACTION_ID, PAGE_UP_HOVER_ACTION_ID, SCROLL_DOWN_HOVER_ACTION_ID, SCROLL_LEFT_HOVER_ACTION_ID, SCROLL_RIGHT_HOVER_ACTION_ID, SCROLL_UP_HOVER_ACTION_ID, SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds';
import { DECREASE_HOVER_VERBOSITY_ACTION_ID, DECREASE_HOVER_VERBOSITY_ACTION_LABEL, GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_LABEL, PAGE_DOWN_HOVER_ACTION_ID, PAGE_UP_HOVER_ACTION_ID, SCROLL_DOWN_HOVER_ACTION_ID, SCROLL_LEFT_HOVER_ACTION_ID, SCROLL_RIGHT_HOVER_ACTION_ID, SCROLL_UP_HOVER_ACTION_ID, SHOW_DEFINITION_PREVIEW_HOVER_ACTION_ID, SHOW_OR_FOCUS_HOVER_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds';
import { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions';
@ -425,17 +425,14 @@ export class IncreaseHoverVerbosityLevel extends EditorAction {
constructor() {
super({
id: INCREASE_HOVER_VERBOSITY_ACTION_ID,
label: nls.localize({
key: 'increaseHoverVerbosityLevel',
comment: ['Label for action that will increase the hover verbosity level.']
}, "Increase Hover Verbosity Level"),
label: INCREASE_HOVER_VERBOSITY_ACTION_LABEL,
alias: 'Increase Hover Verbosity Level',
precondition: EditorContextKeys.hoverFocused
precondition: EditorContextKeys.hoverVisible
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor): void {
HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase);
public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void {
HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase, args?.index, args?.focus);
}
}
@ -444,16 +441,13 @@ export class DecreaseHoverVerbosityLevel extends EditorAction {
constructor() {
super({
id: DECREASE_HOVER_VERBOSITY_ACTION_ID,
label: nls.localize({
key: 'decreaseHoverVerbosityLevel',
comment: ['Label for action that will decrease the hover verbosity level.']
}, "Decrease Hover Verbosity Level"),
label: DECREASE_HOVER_VERBOSITY_ACTION_LABEL,
alias: 'Decrease Hover Verbosity Level',
precondition: EditorContextKeys.hoverFocused
precondition: EditorContextKeys.hoverVisible
});
}
public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void {
HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease);
public run(accessor: ServicesAccessor, editor: ICodeEditor, args?: { index: number; focus: boolean }): void {
HoverController.get(editor)?.updateMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease, args?.index, args?.focus);
}
}

View file

@ -13,7 +13,7 @@ import { MarkerHoverParticipant } from 'vs/editor/contrib/hover/browser/markerHo
import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController';
import 'vs/css!./hover';
import { AccessibleViewRegistry } from 'vs/platform/accessibility/browser/accessibleViewRegistry';
import { ExtHoverAccessibleView, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews';
import { ExtHoverAccessibleView, HoverAccessibilityHelp, HoverAccessibleView } from 'vs/editor/contrib/hover/browser/hoverAccessibleViews';
registerEditorContribution(HoverController.ID, HoverController, EditorContributionInstantiation.BeforeFirstInteraction);
registerEditorAction(ShowOrFocusHoverAction);
@ -41,4 +41,5 @@ registerThemingParticipant((theme, collector) => {
}
});
AccessibleViewRegistry.register(new HoverAccessibleView());
AccessibleViewRegistry.register(new HoverAccessibilityHelp());
AccessibleViewRegistry.register(new ExtHoverAccessibleView());

View file

@ -23,6 +23,7 @@ import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHover
import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController';
import 'vs/css!./hover';
import { MarginHoverWidget } from 'vs/editor/contrib/hover/browser/marginHoverWidget';
import { Emitter } from 'vs/base/common/event';
// sticky hover widget which doesn't disappear on focus out and such
const _sticky = false
@ -47,8 +48,13 @@ const enum HoverWidgetType {
export class HoverController extends Disposable implements IEditorContribution {
private readonly _onHoverContentsChanged = this._register(new Emitter<void>());
public readonly onHoverContentsChanged = this._onHoverContentsChanged.event;
public static readonly ID = 'editor.contrib.hover';
public shouldKeepOpenOnEditorMouseMoveOrLeave: boolean = false;
private readonly _listenersStore = new DisposableStore();
private _glyphWidget: MarginHoverWidget | undefined;
@ -174,6 +180,9 @@ export class HoverController extends Disposable implements IEditorContribution {
}
private _onEditorMouseLeave(mouseEvent: IPartialEditorMouseEvent): void {
if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) {
return;
}
this._cancelScheduler();
@ -223,6 +232,9 @@ export class HoverController extends Disposable implements IEditorContribution {
}
private _onEditorMouseMove(mouseEvent: IEditorMouseEvent): void {
if (this.shouldKeepOpenOnEditorMouseMoveOrLeave) {
return;
}
this._mouseMoveEvent = mouseEvent;
if (this._contentWidget?.isFocused || this._contentWidget?.isResizing) {
@ -374,6 +386,7 @@ export class HoverController extends Disposable implements IEditorContribution {
private _getOrCreateContentWidget(): ContentHoverController {
if (!this._contentWidget) {
this._contentWidget = this._instantiationService.createInstance(ContentHoverController, this._editor);
this._listenersStore.add(this._contentWidget.onContentsChanged(() => this._onHoverContentsChanged.fire()));
}
return this._contentWidget;
}
@ -404,8 +417,20 @@ export class HoverController extends Disposable implements IEditorContribution {
return this._contentWidget?.widget.isResizing || false;
}
public updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): void {
this._getOrCreateContentWidget().updateFocusedMarkdownHoverVerbosityLevel(action);
public focusedMarkdownHoverIndex(): number {
return this._getOrCreateContentWidget().focusedMarkdownHoverIndex();
}
public markdownHoverContentAtIndex(index: number): string {
return this._getOrCreateContentWidget().markdownHoverContentAtIndex(index);
}
public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
return this._getOrCreateContentWidget().doesMarkdownHoverAtIndexSupportVerbosityAction(index, action);
}
public updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): void {
this._getOrCreateContentWidget().updateMarkdownHoverVerbosityLevel(action, index, focus);
}
public focus(): void {

View file

@ -191,8 +191,20 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant<Markdow
return this._renderedHoverParts;
}
public updateFocusedMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction) {
this._renderedHoverParts?.updateFocusedHoverPartVerbosityLevel(action);
public markdownHoverContentAtIndex(index: number): string {
return this._renderedHoverParts?.markdownHoverContentAtIndex(index) ?? '';
}
public focusedMarkdownHoverIndex(): number {
return this._renderedHoverParts?.focusedMarkdownHoverIndex() ?? 1;
}
public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
return this._renderedHoverParts?.doesMarkdownHoverAtIndexSupportVerbosityAction(index, action) ?? false;
}
public updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean) {
this._renderedHoverParts?.updateMarkdownHoverPartVerbosityLevel(action, index, focus);
}
}
@ -202,16 +214,10 @@ interface RenderedHoverPart {
hoverSource?: HoverSource;
}
interface FocusedHoverInfo {
hoverPartIndex: number;
// TODO@aiday-mar is this needed?
focusRemains: boolean;
}
class MarkdownRenderedHoverParts extends Disposable {
private _renderedHoverParts: RenderedHoverPart[];
private _hoverFocusInfo: FocusedHoverInfo = { hoverPartIndex: -1, focusRemains: false };
private _focusedHoverPartIndex: number = -1;
private _ongoingHoverOperations: Map<HoverProvider, { verbosityDelta: number; tokenSource: CancellationTokenSource }> = new Map();
constructor(
@ -281,18 +287,13 @@ class MarkdownRenderedHoverParts extends Disposable {
disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Increase, canIncreaseVerbosity));
disposables.add(this._renderHoverExpansionAction(actionsContainer, HoverVerbosityAction.Decrease, canDecreaseVerbosity));
const focusTracker = disposables.add(dom.trackFocus(renderedMarkdown));
disposables.add(focusTracker.onDidFocus(() => {
this._hoverFocusInfo = {
hoverPartIndex,
focusRemains: true
};
this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_IN, (event: Event) => {
event.stopPropagation();
this._focusedHoverPartIndex = hoverPartIndex;
}));
disposables.add(focusTracker.onDidBlur(() => {
if (this._hoverFocusInfo?.focusRemains) {
this._hoverFocusInfo.focusRemains = false;
return;
}
this._register(dom.addDisposableListener(renderedMarkdown, dom.EventType.FOCUS_OUT, (event: Event) => {
event.stopPropagation();
this._focusedHoverPartIndex = -1;
}));
return { renderedMarkdown, disposables, hoverSource };
}
@ -339,19 +340,19 @@ class MarkdownRenderedHoverParts extends Disposable {
return store;
}
actionElement.classList.add('enabled');
const actionFunction = () => this.updateFocusedHoverPartVerbosityLevel(action);
const actionFunction = () => this.updateMarkdownHoverPartVerbosityLevel(action);
store.add(new ClickAction(actionElement, actionFunction));
store.add(new KeyDownAction(actionElement, actionFunction, [KeyCode.Enter, KeyCode.Space]));
return store;
}
public async updateFocusedHoverPartVerbosityLevel(action: HoverVerbosityAction): Promise<void> {
public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number = -1, focus: boolean = true): Promise<void> {
const model = this._editor.getModel();
if (!model) {
return;
}
const hoverFocusedPartIndex = this._hoverFocusInfo.hoverPartIndex;
const hoverRenderedPart = this._getRenderedHoverPartAtIndex(hoverFocusedPartIndex);
const indexOfInterest = index !== -1 ? index : this._focusedHoverPartIndex;
const hoverRenderedPart = this._getRenderedHoverPartAtIndex(indexOfInterest);
if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) {
return;
}
@ -362,16 +363,35 @@ class MarkdownRenderedHoverParts extends Disposable {
}
const newHoverSource = new HoverSource(newHover, hoverSource.hoverProvider, hoverSource.hoverPosition);
const newHoverRenderedPart = this._renderHoverPart(
hoverFocusedPartIndex,
indexOfInterest,
newHover.contents,
newHoverSource,
this._onFinishedRendering
);
this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, newHoverRenderedPart);
this._focusOnHoverPartWithIndex(hoverFocusedPartIndex);
this._replaceRenderedHoverPartAtIndex(indexOfInterest, newHoverRenderedPart);
if (focus) {
this._focusOnHoverPartWithIndex(indexOfInterest);
}
this._onFinishedRendering();
}
public markdownHoverContentAtIndex(index: number): string {
const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index);
return hoverRenderedPart?.renderedMarkdown.innerText ?? '';
}
public focusedMarkdownHoverIndex(): number {
return this._focusedHoverPartIndex;
}
public doesMarkdownHoverAtIndexSupportVerbosityAction(index: number, action: HoverVerbosityAction): boolean {
const hoverRenderedPart = this._getRenderedHoverPartAtIndex(index);
if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) {
return false;
}
return true;
}
private async _fetchHover(hoverSource: HoverSource, model: ITextModel, action: HoverVerbosityAction): Promise<Hover | null | undefined> {
let verbosityDelta = action === HoverVerbosityAction.Increase ? 1 : -1;
const provider = hoverSource.hoverProvider;
@ -407,7 +427,6 @@ class MarkdownRenderedHoverParts extends Disposable {
private _focusOnHoverPartWithIndex(index: number): void {
this._renderedHoverParts[index].renderedMarkdown.focus();
this._hoverFocusInfo.focusRemains = true;
}
private _getRenderedHoverPartAtIndex(index: number): RenderedHoverPart | undefined {

View file

@ -141,9 +141,11 @@ export class AdvancedContentProvider implements IAccessibleViewContentProvider {
public provideContent: () => string,
public onClose: () => void,
public verbositySettingKey: string,
public onOpen?: () => void,
public actions?: IAction[],
public next?: () => void,
public previous?: () => void,
public onDidChangeContent?: Event<void>,
public onKeyDown?: (e: IKeyboardEvent) => void,
public getSymbols?: () => IAccessibleViewSymbol[],
public onDidRequestClearLastProvider?: Event<AccessibleViewProviderId>,
@ -157,9 +159,11 @@ export class ExtensionContentProvider implements IBasicContentProvider {
public options: IAccessibleViewOptions,
public provideContent: () => string,
public onClose: () => void,
public onOpen?: () => void,
public next?: () => void,
public previous?: () => void,
public actions?: IAction[],
public onDidChangeContent?: Event<void>,
) { }
}
@ -168,7 +172,9 @@ export interface IBasicContentProvider {
options: IAccessibleViewOptions;
onClose(): void;
provideContent(): string;
onOpen?(): void;
actions?: IAction[];
previous?(): void;
next?(): void;
onDidChangeContent?: Event<void>;
}

View file

@ -9,7 +9,7 @@ import { ContextKeyExpression } from 'vs/platform/contextkey/common/contextkey';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { alert } from 'vs/base/browser/ui/aria/aria';
export interface IAccessibleViewImplentation {
export interface IAccessibleViewImplentation extends IDisposable {
type: AccessibleViewType;
priority: number;
name: string;
@ -31,6 +31,7 @@ export const AccessibleViewRegistry = new class AccessibleViewRegistry {
if (idx !== -1) {
this._implementations.splice(idx, 1);
}
implementation.dispose();
}
};
}

View file

@ -92,6 +92,7 @@ export class NotificationAccessibleView implements IAccessibleViewImplentation {
}
return getProvider();
}
dispose() { }
}

View file

@ -245,10 +245,13 @@ export class AccessibleView extends Disposable {
if (!provider) {
return;
}
provider.onOpen?.();
let viewContainer: HTMLElement | undefined;
const delegate: IContextViewDelegate = {
getAnchor: () => { return { x: (getActiveWindow().innerWidth / 2) - ((Math.min(this._layoutService.activeContainerDimension.width * 0.62 /* golden cut */, DIMENSIONS.MAX_WIDTH)) / 2), y: this._layoutService.activeContainerOffset.quickPickTop }; },
render: (container) => {
container.classList.add('accessible-view-container');
viewContainer = container;
viewContainer.classList.add('accessible-view-container');
return this._render(provider, container, showAccessibleViewHelp);
},
onHide: () => {
@ -289,6 +292,11 @@ export class AccessibleView extends Disposable {
if (provider instanceof ExtensionContentProvider) {
this._storageService.store(`${ACCESSIBLE_VIEW_SHOWN_STORAGE_PREFIX}${provider.id}`, true, StorageScope.APPLICATION, StorageTarget.USER);
}
if (provider.onDidChangeContent) {
this._register(provider.onDidChangeContent(() => {
if (viewContainer) { this._render(provider, viewContainer, showAccessibleViewHelp); }
}));
}
}
previous(): void {
@ -559,9 +567,9 @@ export class AccessibleView extends Disposable {
});
this._updateToolbar(this._currentProvider.actions, provider.options.type);
const hide = (e: KeyboardEvent | IKeyboardEvent): void => {
const hide = (e?: KeyboardEvent | IKeyboardEvent): void => {
provider.onClose();
e.stopPropagation();
e?.stopPropagation();
this._contextViewService.hideContextView();
this._updateContextKeys(provider, false);
this._lastProvider = undefined;
@ -592,7 +600,7 @@ export class AccessibleView extends Disposable {
}));
disposableStore.add(this._editorWidget.onDidBlurEditorWidget(() => {
if (!isActiveElement(this._toolbar.getElement())) {
this._contextViewService.hideContextView();
hide();
}
}));
disposableStore.add(this._editorWidget.onDidContentSizeChange(() => this._layout()));
@ -648,21 +656,25 @@ export class AccessibleView extends Disposable {
provider.id,
provider.options,
provider.provideContent.bind(provider),
provider.onClose,
provider.onClose.bind(provider),
provider.verbositySettingKey,
provider.onOpen?.bind(provider),
provider.actions,
provider.next,
provider.previous,
provider.onKeyDown,
provider.getSymbols,
provider.next?.bind(provider),
provider.previous?.bind(provider),
provider.onDidChangeContent?.bind(provider),
provider.onKeyDown?.bind(provider),
provider.getSymbols?.bind(provider),
) : new ExtensionContentProvider(
provider.id,
provider.options,
provider.provideContent.bind(provider),
provider.onClose,
provider.next,
provider.previous,
provider.actions
provider.onClose.bind(provider),
provider.onOpen?.bind(provider),
provider.next?.bind(provider),
provider.previous?.bind(provider),
provider.actions,
provider.onDidChangeContent?.bind(provider),
);
return lastProvider;
}

View file

@ -56,7 +56,8 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService,
() => content,
() => viewsService.openView(viewDescriptor.id, true),
);
}
},
dispose: () => { },
}));
disposableStore.add(keybindingService.onDidUpdateKeybindings(() => {

View file

@ -25,6 +25,7 @@ export class ChatAccessibilityHelp implements IAccessibleViewImplentation {
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat');
}
dispose() { }
}
export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat'): string {

View file

@ -95,4 +95,5 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation {
};
}
}
dispose() { }
}

View file

@ -61,4 +61,5 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation
options: { type: AccessibleViewType.Help }
};
}
dispose() { }
}

View file

@ -48,4 +48,5 @@ export class CommentsAccessibilityHelp implements IAccessibleViewImplentation {
getProvider(accessor: ServicesAccessor) {
return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider);
}
dispose() { }
}

View file

@ -23,4 +23,5 @@ export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation
}
return getChatAccessibilityHelpProvider(accessor, codeEditor, 'inlineChat');
}
dispose() { }
}

View file

@ -42,4 +42,5 @@ export class InlineChatAccessibleView implements IAccessibleViewImplentation {
options: { type: AccessibleViewType.View }
};
}
dispose() { }
}

View file

@ -28,6 +28,7 @@ export class NotebookAccessibilityHelp implements IAccessibleViewImplentation {
}
return;
}
dispose() { }
}

View file

@ -20,6 +20,7 @@ export class NotebookAccessibleView implements IAccessibleViewImplentation {
const editorService = accessor.get(IEditorService);
return showAccessibleOutput(editorService);
}
dispose() { }
}

View file

@ -35,6 +35,7 @@ export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentatio
options: { type: AccessibleViewType.Help }
};
}
dispose() { }
}
export function getAccessibilityHelpText(accessor: ServicesAccessor): string {

View file

@ -33,4 +33,5 @@ export class TerminalInlineChatAccessibleView implements IAccessibleViewImplenta
options: { type: AccessibleViewType.View }
};
}
dispose() { }
}