diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 20c441162b5cf..81ad7b790edab 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -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; + private readonly _onContentsChanged = this._register(new Emitter()); + 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 { - this._markdownHoverParticipant?.updateFocusedMarkdownHoverPartVerbosityLevel(action); + public async updateMarkdownHoverVerbosityLevel(action: HoverVerbosityAction, index?: number, focus?: boolean): Promise { + 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 { diff --git a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts index 557f1fffa8579..5cf65a5b39924 100644 --- a/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts +++ b/src/vs/editor/contrib/hover/browser/hoverAccessibleViews.ts @@ -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.", 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.", 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 = this._register(new Emitter()); + public readonly onDidChangeContent: Event = 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() { } } diff --git a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts index 5cc42e1aa50b9..2ade6360ac1bf 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts @@ -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"); diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index 9654e7c3d0920..20a5148fd742b 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActions.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActions.ts @@ -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); } } diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index 629b189f2dbde..bf24cdc1c69b1 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -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()); diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts index 5d71cd64144d0..293902db46d36 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -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()); + 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 { diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index 3c11bee08498b..af02ec96e9358 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -191,8 +191,20 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant = 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 { + public async updateMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction, index: number = -1, focus: boolean = true): Promise { 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 { 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 { diff --git a/src/vs/platform/accessibility/browser/accessibleView.ts b/src/vs/platform/accessibility/browser/accessibleView.ts index d2fedcb8661d8..d71a58711953c 100644 --- a/src/vs/platform/accessibility/browser/accessibleView.ts +++ b/src/vs/platform/accessibility/browser/accessibleView.ts @@ -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, public onKeyDown?: (e: IKeyboardEvent) => void, public getSymbols?: () => IAccessibleViewSymbol[], public onDidRequestClearLastProvider?: Event, @@ -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, ) { } } @@ -168,7 +172,9 @@ export interface IBasicContentProvider { options: IAccessibleViewOptions; onClose(): void; provideContent(): string; + onOpen?(): void; actions?: IAction[]; previous?(): void; next?(): void; + onDidChangeContent?: Event; } diff --git a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts index 13679e64781a6..0a6369565bba7 100644 --- a/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts +++ b/src/vs/platform/accessibility/browser/accessibleViewRegistry.ts @@ -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(); } }; } diff --git a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts index 066afd7adda36..15a240f91502c 100644 --- a/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts +++ b/src/vs/workbench/browser/parts/notifications/notificationAccessibleView.ts @@ -92,6 +92,7 @@ export class NotificationAccessibleView implements IAccessibleViewImplentation { } return getProvider(); } + dispose() { } } diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts index d1a40cc944614..813cbd360d9dd 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibleView.ts @@ -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; } diff --git a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts index 46ea96cf1f034..f8381417bd864 100644 --- a/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts +++ b/src/vs/workbench/contrib/accessibility/browser/extensionAccesibilityHelp.contribution.ts @@ -56,7 +56,8 @@ function registerAccessibilityHelpAction(keybindingService: IKeybindingService, () => content, () => viewsService.openView(viewDescriptor.id, true), ); - } + }, + dispose: () => { }, })); disposableStore.add(keybindingService.onDidUpdateKeybindings(() => { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index d7ec863b04543..e7374ecb56578 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -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 { diff --git a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts index d97ecba4f428f..009d9f4c3b103 100644 --- a/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/chatResponseAccessibleView.ts @@ -95,4 +95,5 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplentation { }; } } + dispose() { } } diff --git a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts index 6ca47252ca5b4..ef4ce7693a180 100644 --- a/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/codeEditor/browser/diffEditorAccessibilityHelp.ts @@ -61,4 +61,5 @@ export class DiffEditorAccessibilityHelp implements IAccessibleViewImplentation options: { type: AccessibleViewType.Help } }; } + dispose() { } } diff --git a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts index 5ce9ac9a0ba5c..1c247dec3497a 100644 --- a/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts +++ b/src/vs/workbench/contrib/comments/browser/commentsAccessibility.ts @@ -48,4 +48,5 @@ export class CommentsAccessibilityHelp implements IAccessibleViewImplentation { getProvider(accessor: ServicesAccessor) { return accessor.get(IInstantiationService).createInstance(CommentsAccessibilityHelpProvider); } + dispose() { } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts index d168f54f836a5..a2348928fb6c7 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibilityHelp.ts @@ -23,4 +23,5 @@ export class InlineChatAccessibilityHelp implements IAccessibleViewImplentation } return getChatAccessibilityHelpProvider(accessor, codeEditor, 'inlineChat'); } + dispose() { } } diff --git a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts index 4cac306c23f1b..91a719e19996f 100644 --- a/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts +++ b/src/vs/workbench/contrib/inlineChat/browser/inlineChatAccessibleView.ts @@ -42,4 +42,5 @@ export class InlineChatAccessibleView implements IAccessibleViewImplentation { options: { type: AccessibleViewType.View } }; } + dispose() { } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts index c06912b28f935..b146d0f71d21c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibilityHelp.ts @@ -28,6 +28,7 @@ export class NotebookAccessibilityHelp implements IAccessibleViewImplentation { } return; } + dispose() { } } diff --git a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts index 3975c17bb9e5d..daa9bae804d5c 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookAccessibleView.ts @@ -20,6 +20,7 @@ export class NotebookAccessibleView implements IAccessibleViewImplentation { const editorService = accessor.get(IEditorService); return showAccessibleOutput(editorService); } + dispose() { } } diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts index f0bdac465ba84..35fcdd0410c24 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibilityHelp.ts @@ -35,6 +35,7 @@ export class TerminalChatAccessibilityHelp implements IAccessibleViewImplentatio options: { type: AccessibleViewType.Help } }; } + dispose() { } } export function getAccessibilityHelpText(accessor: ServicesAccessor): string { diff --git a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts index 215f1fe6f05d1..2898dfde41c69 100644 --- a/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts +++ b/src/vs/workbench/contrib/terminalContrib/chat/browser/terminalChatAccessibleView.ts @@ -33,4 +33,5 @@ export class TerminalInlineChatAccessibleView implements IAccessibleViewImplenta options: { type: AccessibleViewType.View } }; } + dispose() { } }