From b8eba5da208f0d467bf00121ba331ae2319c0d1e Mon Sep 17 00:00:00 2001 From: Aiday Marlen Kyzy Date: Fri, 19 Apr 2024 19:17:21 +0200 Subject: [PATCH] Adding the possibility to enrich hovers with additional data (#210472) Adding the possibility to enrich hovers with additional data --- src/vs/base/browser/ui/hover/hoverWidget.ts | 42 ++- src/vs/editor/common/languages.ts | 38 +- .../common/standalone/standaloneEnums.ts | 11 + .../hover/browser/contentHoverController.ts | 15 +- .../hover/browser/contentHoverStatusBar.ts | 1 + .../editor/contrib/hover/browser/getHover.ts | 27 +- src/vs/editor/contrib/hover/browser/hover.css | 22 ++ .../contrib/hover/browser/hoverActionIds.ts | 2 + .../contrib/hover/browser/hoverActions.ts | 51 ++- .../hover/browser/hoverContribution.ts | 4 +- .../contrib/hover/browser/hoverController.ts | 5 + .../hover/browser/markdownHoverParticipant.ts | 340 ++++++++++++++++-- .../hover/browser/markerHoverParticipant.ts | 1 + .../inlayHints/browser/inlayHintsHover.ts | 10 +- .../browser/unicodeHighlighter.ts | 2 +- .../standalone/browser/standaloneLanguages.ts | 5 +- src/vs/monaco.d.ts | 36 +- .../api/browser/mainThreadLanguageFeatures.ts | 13 +- .../workbench/api/common/extHost.api.impl.ts | 2 + .../workbench/api/common/extHost.protocol.ts | 10 +- .../api/common/extHostLanguageFeatures.ts | 47 ++- .../api/common/extHostTypeConverters.ts | 17 +- src/vs/workbench/api/common/extHostTypes.ts | 23 ++ .../browser/extHostLanguageFeatures.test.ts | 12 +- .../common/extensionsApiProposals.ts | 1 + ...de.proposed.editorHoverVerbosityLevel.d.ts | 76 ++++ 26 files changed, 717 insertions(+), 96 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts diff --git a/src/vs/base/browser/ui/hover/hoverWidget.ts b/src/vs/base/browser/ui/hover/hoverWidget.ts index bff397303beae..2e9ecbdd1fe88 100644 --- a/src/vs/base/browser/ui/hover/hoverWidget.ts +++ b/src/vs/base/browser/ui/hover/hoverWidget.ts @@ -67,21 +67,8 @@ export class HoverAction extends Disposable { const label = dom.append(this.action, $('span')); label.textContent = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label; - this._register(dom.addDisposableListener(this.actionContainer, dom.EventType.CLICK, e => { - e.stopPropagation(); - e.preventDefault(); - actionOptions.run(this.actionContainer); - })); - - this._register(dom.addDisposableListener(this.actionContainer, dom.EventType.KEY_DOWN, e => { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - e.stopPropagation(); - e.preventDefault(); - actionOptions.run(this.actionContainer); - } - })); - + this._store.add(new ClickAction(this.actionContainer, actionOptions.run)); + this._store.add(new KeyDownAction(this.actionContainer, actionOptions.run, [KeyCode.Enter, KeyCode.Space])); this.setEnabled(true); } @@ -99,3 +86,28 @@ export class HoverAction extends Disposable { export function getHoverAccessibleViewHint(shouldHaveHint?: boolean, keybinding?: string | null): string | undefined { return shouldHaveHint && keybinding ? localize('acessibleViewHint', "Inspect this in the accessible view with {0}.", keybinding) : shouldHaveHint ? localize('acessibleViewHintNoKbOpen', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.") : ''; } + +export class ClickAction extends Disposable { + constructor(container: HTMLElement, run: (container: HTMLElement) => void) { + super(); + this._register(dom.addDisposableListener(container, dom.EventType.CLICK, e => { + e.stopPropagation(); + e.preventDefault(); + run(container); + })); + } +} + +export class KeyDownAction extends Disposable { + constructor(container: HTMLElement, run: (container: HTMLElement) => void, keyCodes: KeyCode[]) { + super(); + this._register(dom.addDisposableListener(container, dom.EventType.KEY_DOWN, e => { + const event = new StandardKeyboardEvent(e); + if (keyCodes.some(keyCode => event.equals(keyCode))) { + e.stopPropagation(); + e.preventDefault(); + run(container); + } + })); + } +} diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 6a86d6c91dab2..312f42938294c 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -168,19 +168,51 @@ export interface Hover { * current position itself. */ range?: IRange; + + /** + * Can increase the verbosity of the hover + */ + canIncreaseVerbosity?: boolean; + + /** + * Can decrease the verbosity of the hover + */ + canDecreaseVerbosity?: boolean; } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ -export interface HoverProvider { +export interface HoverProvider { /** - * Provide a hover for the given position and document. Multiple hovers at the same + * Provide a hover for the given position, context and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideHover(model: model.ITextModel, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; +} + +export interface HoverContext { + /** + * Whether to increase or decrease the hover's verbosity + */ + action?: HoverVerbosityAction; + /** + * The previous hover for the same position + */ + previousHover?: THover; +} + +export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase, + /** + * Decrease the verbosity of the hover + */ + Decrease } /** diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index d01db6500b346..b89e4d695007e 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -366,6 +366,17 @@ export enum GlyphMarginLane { Right = 3 } +export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase = 0, + /** + * Decrease the verbosity of the hover + */ + Decrease = 1 +} + /** * Describes what to do with the indentation when pressing Enter. */ diff --git a/src/vs/editor/contrib/hover/browser/contentHoverController.ts b/src/vs/editor/contrib/hover/browser/contentHoverController.ts index 34bcdb0e4b223..20c441162b5cf 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverController.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverController.ts @@ -16,6 +16,9 @@ import { HoverOperation, HoverStartMode, HoverStartSource } from 'vs/editor/cont import { HoverAnchor, HoverParticipantRegistry, HoverRangeAnchor, IEditorHoverColorPickerWidget, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { MarkdownHoverParticipant } from 'vs/editor/contrib/hover/browser/markdownHoverParticipant'; +import { InlayHintsHover } from 'vs/editor/contrib/inlayHints/browser/inlayHintsHover'; +import { HoverVerbosityAction } from 'vs/editor/common/standalone/standaloneEnums'; import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; import { ContentHoverComputer } from 'vs/editor/contrib/hover/browser/contentHoverComputer'; import { ContentHoverVisibleData, HoverResult } from 'vs/editor/contrib/hover/browser/contentHoverTypes'; @@ -28,6 +31,8 @@ export class ContentHoverController extends Disposable implements IHoverWidget { private readonly _computer: ContentHoverComputer; private readonly _widget: ContentHoverWidget; private readonly _participants: IEditorHoverParticipant[]; + // TODO@aiday-mar make array of participants, dispatch between them + private readonly _markdownHoverParticipant: MarkdownHoverParticipant | undefined; private readonly _hoverOperation: HoverOperation; constructor( @@ -42,7 +47,11 @@ export class ContentHoverController extends Disposable implements IHoverWidget { // Instantiate participants and sort them by `hoverOrdinal` which is relevant for rendering order. this._participants = []; for (const participant of HoverParticipantRegistry.getAll()) { - this._participants.push(this._instantiationService.createInstance(participant, this._editor)); + const participantInstance = this._instantiationService.createInstance(participant, this._editor); + if (participantInstance instanceof MarkdownHoverParticipant && !(participantInstance instanceof InlayHintsHover)) { + this._markdownHoverParticipant = participantInstance; + } + this._participants.push(participantInstance); } this._participants.sort((p1, p2) => p1.hoverOrdinal - p2.hoverOrdinal); @@ -342,6 +351,10 @@ 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 getWidgetContent(): string | undefined { const node = this._widget.getDomNode(); if (!node.textContent) { diff --git a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts index 594e86084edac..04d84593d06ee 100644 --- a/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts +++ b/src/vs/editor/contrib/hover/browser/contentHoverStatusBar.ts @@ -25,6 +25,7 @@ export class EditorHoverStatusBar extends Disposable implements IEditorHoverStat ) { super(); this.hoverElement = $('div.hover-row.status-bar'); + this.hoverElement.tabIndex = 0; this.actionsElement = dom.append(this.hoverElement, $('div.actions')); } diff --git a/src/vs/editor/contrib/hover/browser/getHover.ts b/src/vs/editor/contrib/hover/browser/getHover.ts index a89e872eaba32..608f8a1dc9651 100644 --- a/src/vs/editor/contrib/hover/browser/getHover.ts +++ b/src/vs/editor/contrib/hover/browser/getHover.ts @@ -21,31 +21,32 @@ export class HoverProviderResult { ) { } } +/** + * Does not throw or return a rejected promise (returns undefined instead). + */ async function executeProvider(provider: HoverProvider, ordinal: number, model: ITextModel, position: Position, token: CancellationToken): Promise { - try { - const result = await Promise.resolve(provider.provideHover(model, position, token)); - if (result && isValid(result)) { - return new HoverProviderResult(provider, result, ordinal); - } - } catch (err) { - onUnexpectedExternalError(err); + const result = await Promise + .resolve(provider.provideHover(model, position, token)) + .catch(onUnexpectedExternalError); + if (!result || !isValid(result)) { + return undefined; } - return undefined; + return new HoverProviderResult(provider, result, ordinal); } -export function getHover(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): AsyncIterableObject { +export function getHoverProviderResultsAsAsyncIterable(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): AsyncIterableObject { const providers = registry.ordered(model); const promises = providers.map((provider, index) => executeProvider(provider, index, model, position, token)); return AsyncIterableObject.fromPromises(promises).coalesce(); } -export function getHoverPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { - return getHover(registry, model, position, token).map(item => item.hover).toPromise(); +export function getHoversPromise(registry: LanguageFeatureRegistry, model: ITextModel, position: Position, token: CancellationToken): Promise { + return getHoverProviderResultsAsAsyncIterable(registry, model, position, token).map(item => item.hover).toPromise(); } -registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, position) => { +registerModelAndPositionCommand('_executeHoverProvider', (accessor, model, position): Promise => { const languageFeaturesService = accessor.get(ILanguageFeaturesService); - return getHoverPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None); + return getHoversPromise(languageFeaturesService.hoverProvider, model, position, CancellationToken.None); }); function isValid(result: Hover) { diff --git a/src/vs/editor/contrib/hover/browser/hover.css b/src/vs/editor/contrib/hover/browser/hover.css index 6e0324a2d42c6..3c21b4edf84b3 100644 --- a/src/vs/editor/contrib/hover/browser/hover.css +++ b/src/vs/editor/contrib/hover/browser/hover.css @@ -32,6 +32,28 @@ flex-direction: column; } +.monaco-editor .monaco-hover .hover-row .verbosity-actions { + display: flex; + flex-direction: column; + padding-left: 5px; + padding-right: 5px; + justify-content: end; + border-right: 1px solid var(--vscode-editorHoverWidget-border); +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon { + cursor: pointer; + font-size: 11px; +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.enabled { + color: var(--vscode-textLink-foreground); +} + +.monaco-editor .monaco-hover .hover-row .verbosity-actions .codicon.disabled { + opacity: 0.6; +} + .monaco-editor .monaco-hover .hover-row .actions { background-color: var(--vscode-editorHoverWidget-statusBarBackground); } diff --git a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts index 81ce81ae276a6..5cc42e1aa50b9 100644 --- a/src/vs/editor/contrib/hover/browser/hoverActionIds.ts +++ b/src/vs/editor/contrib/hover/browser/hoverActionIds.ts @@ -13,3 +13,5 @@ export const PAGE_UP_HOVER_ACTION_ID = 'editor.action.pageUpHover'; 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 DECREASE_HOVER_VERBOSITY_ACTION_ID = 'editor.action.decreaseHoverVerbosityLevel'; diff --git a/src/vs/editor/contrib/hover/browser/hoverActions.ts b/src/vs/editor/contrib/hover/browser/hoverActions.ts index f167ca8aa24ec..623733a5478ba 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 { GO_TO_BOTTOM_HOVER_ACTION_ID, GO_TO_TOP_HOVER_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, 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 { KeyChord, KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; @@ -15,6 +15,7 @@ import { HoverStartMode, HoverStartSource } from 'vs/editor/contrib/hover/browse import { AccessibilitySupport } from 'vs/platform/accessibility/common/accessibility'; import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { HoverController } from 'vs/editor/contrib/hover/browser/hoverController'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; import * as nls from 'vs/nls'; import 'vs/css!./hover'; @@ -418,3 +419,51 @@ export class GoToBottomHoverAction extends EditorAction { controller.goToBottom(); } } + +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"), + alias: 'Increase Hover Verbosity Level', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyP), + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor): void { + HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Increase); + } +} + +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"), + alias: 'Decrease Hover Verbosity Level', + precondition: EditorContextKeys.hoverFocused, + kbOpts: { + kbExpr: EditorContextKeys.hoverFocused, + primary: KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyMod.CtrlCmd | KeyCode.KeyM), + weight: KeybindingWeight.EditorContrib + } + }); + } + + public run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void { + HoverController.get(editor)?.updateFocusedMarkdownHoverVerbosityLevel(HoverVerbosityAction.Decrease); + } +} diff --git a/src/vs/editor/contrib/hover/browser/hoverContribution.ts b/src/vs/editor/contrib/hover/browser/hoverContribution.ts index 7bae393a8e010..33f5cd8f31657 100644 --- a/src/vs/editor/contrib/hover/browser/hoverContribution.ts +++ b/src/vs/editor/contrib/hover/browser/hoverContribution.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { GoToBottomHoverAction, GoToTopHoverAction, PageDownHoverAction, PageUpHoverAction, ScrollDownHoverAction, ScrollLeftHoverAction, ScrollRightHoverAction, ScrollUpHoverAction, ShowDefinitionPreviewHoverAction, ShowOrFocusHoverAction } from 'vs/editor/contrib/hover/browser/hoverActions'; +import { DecreaseHoverVerbosityLevel, GoToBottomHoverAction, GoToTopHoverAction, IncreaseHoverVerbosityLevel, PageDownHoverAction, PageUpHoverAction, ScrollDownHoverAction, ScrollLeftHoverAction, ScrollRightHoverAction, ScrollUpHoverAction, ShowDefinitionPreviewHoverAction, ShowOrFocusHoverAction } from 'vs/editor/contrib/hover/browser/hoverActions'; import { EditorContributionInstantiation, registerEditorAction, registerEditorContribution } from 'vs/editor/browser/editorExtensions'; import { editorHoverBorder } from 'vs/platform/theme/common/colorRegistry'; import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; @@ -24,6 +24,8 @@ registerEditorAction(PageUpHoverAction); registerEditorAction(PageDownHoverAction); registerEditorAction(GoToTopHoverAction); registerEditorAction(GoToBottomHoverAction); +registerEditorAction(IncreaseHoverVerbosityLevel); +registerEditorAction(DecreaseHoverVerbosityLevel); HoverParticipantRegistry.register(MarkdownHoverParticipant); HoverParticipantRegistry.register(MarkerHoverParticipant); diff --git a/src/vs/editor/contrib/hover/browser/hoverController.ts b/src/vs/editor/contrib/hover/browser/hoverController.ts index 7013b9b1b8e83..c4b56e02c7fe4 100644 --- a/src/vs/editor/contrib/hover/browser/hoverController.ts +++ b/src/vs/editor/contrib/hover/browser/hoverController.ts @@ -17,6 +17,7 @@ import { IHoverWidget } from 'vs/editor/contrib/hover/browser/hoverTypes'; import { InlineSuggestionHintsContentWidget } from 'vs/editor/contrib/inlineCompletions/browser/inlineCompletionsHintsWidget'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { ResultKind } from 'vs/platform/keybinding/common/keybindingResolver'; +import { HoverVerbosityAction } from 'vs/editor/common/languages'; import { RunOnceScheduler } from 'vs/base/common/async'; import { ContentHoverWidget } from 'vs/editor/contrib/hover/browser/contentHoverWidget'; import { ContentHoverController } from 'vs/editor/contrib/hover/browser/contentHoverController'; @@ -401,6 +402,10 @@ export class HoverController extends Disposable implements IEditorContribution { return this._contentWidget?.widget.isResizing || false; } + public updateFocusedMarkdownHoverVerbosityLevel(action: HoverVerbosityAction): void { + this._getOrCreateContentWidget().updateFocusedMarkdownHoverVerbosityLevel(action); + } + public focus(): void { this._contentWidget?.focus(); } diff --git a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts index c548b89e21dce..7c3030a93626c 100644 --- a/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markdownHoverParticipant.ts @@ -4,26 +4,40 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from 'vs/base/browser/dom'; -import { asArray } from 'vs/base/common/arrays'; -import { AsyncIterableObject } from 'vs/base/common/async'; +import { asArray, compareBy, numberComparator } from 'vs/base/common/arrays'; import { CancellationToken } from 'vs/base/common/cancellation'; import { IMarkdownString, isEmptyMarkdownString, MarkdownString } from 'vs/base/common/htmlContent'; -import { DisposableStore, IDisposable } from 'vs/base/common/lifecycle'; +import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { MarkdownRenderer } from 'vs/editor/browser/widget/markdownRenderer/browser/markdownRenderer'; +import { DECREASE_HOVER_VERBOSITY_ACTION_ID, INCREASE_HOVER_VERBOSITY_ACTION_ID } from 'vs/editor/contrib/hover/browser/hoverActionIds'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IModelDecoration } from 'vs/editor/common/model'; +import { IModelDecoration, ITextModel } from 'vs/editor/common/model'; import { ILanguageService } from 'vs/editor/common/languages/language'; -import { getHover } from 'vs/editor/contrib/hover/browser/getHover'; -import { HoverAnchor, HoverAnchorType, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; +import { HoverAnchor, HoverAnchorType, HoverRangeAnchor, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart } from 'vs/editor/contrib/hover/browser/hoverTypes'; import * as nls from 'vs/nls'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { IOpenerService } from 'vs/platform/opener/common/opener'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { Hover, HoverContext, HoverProvider, HoverVerbosityAction } from 'vs/editor/common/languages'; +import { registerIcon } from 'vs/platform/theme/common/iconRegistry'; +import { Codicon } from 'vs/base/common/codicons'; +import { ThemeIcon } from 'vs/base/common/themables'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { getDefaultHoverDelegate } from 'vs/base/browser/ui/hover/hoverDelegateFactory'; +import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; +import { ClickAction, KeyDownAction } from 'vs/base/browser/ui/hover/hoverWidget'; +import { KeyCode } from 'vs/base/common/keyCodes'; +import { IHoverService } from 'vs/platform/hover/browser/hover'; +import { AsyncIterableObject } from 'vs/base/common/async'; +import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; +import { getHoverProviderResultsAsAsyncIterable } from 'vs/editor/contrib/hover/browser/getHover'; const $ = dom.$; +const increaseHoverVerbosityIcon = registerIcon('hover-increase-verbosity', Codicon.add, nls.localize('increaseHoverVerbosity', 'Icon for increaseing hover verbosity.')); +const decreaseHoverVerbosityIcon = registerIcon('hover-decrease-verbosity', Codicon.remove, nls.localize('decreaseHoverVerbosity', 'Icon for decreasing hover verbosity.')); export class MarkdownHover implements IHoverPart { @@ -32,7 +46,8 @@ export class MarkdownHover implements IHoverPart { public readonly range: Range, public readonly contents: IMarkdownString[], public readonly isBeforeContent: boolean, - public readonly ordinal: number + public readonly ordinal: number, + public readonly source: HoverSource | undefined = undefined, ) { } public isValidForHoverAnchor(anchor: HoverAnchor): boolean { @@ -44,16 +59,38 @@ export class MarkdownHover implements IHoverPart { } } +class HoverSource { + + constructor( + readonly hover: Hover, + readonly hoverProvider: HoverProvider, + readonly hoverPosition: Position, + ) { } + + public supportsVerbosityAction(hoverVerbosityAction: HoverVerbosityAction): boolean { + switch (hoverVerbosityAction) { + case HoverVerbosityAction.Increase: + return this.hover.canIncreaseVerbosity ?? false; + case HoverVerbosityAction.Decrease: + return this.hover.canDecreaseVerbosity ?? false; + } + } +} + export class MarkdownHoverParticipant implements IEditorHoverParticipant { public readonly hoverOrdinal: number = 3; + private _renderedHoverParts: MarkdownRenderedHoverParts | undefined; + constructor( protected readonly _editor: ICodeEditor, @ILanguageService private readonly _languageService: ILanguageService, @IOpenerService private readonly _openerService: IOpenerService, @IConfigurationService private readonly _configurationService: IConfigurationService, @ILanguageFeaturesService protected readonly _languageFeaturesService: ILanguageFeaturesService, + @IKeybindingService private readonly _keybindingService: IKeybindingService, + @IHoverService private readonly _hoverService: IHoverService, ) { } public createLoadingMessage(anchor: HoverAnchor): MarkdownHover | null { @@ -120,57 +157,294 @@ export class MarkdownHoverParticipant implements IEditorHoverParticipant !isEmptyMarkdownString(item.hover.contents)) + private _getMarkdownHovers(hoverProviderRegistry: LanguageFeatureRegistry, model: ITextModel, anchor: HoverRangeAnchor, token: CancellationToken): AsyncIterableObject { + const position = anchor.range.getStartPosition(); + const hoverProviderResults = getHoverProviderResultsAsAsyncIterable(hoverProviderRegistry, model, position, token); + const markdownHovers = hoverProviderResults.filter(item => !isEmptyMarkdownString(item.hover.contents)) .map(item => { - const rng = item.hover.range ? Range.lift(item.hover.range) : anchor.range; - return new MarkdownHover(this, rng, item.hover.contents, false, item.ordinal); + const range = item.hover.range ? Range.lift(item.hover.range) : anchor.range; + const hoverSource = new HoverSource(item.hover, item.provider, position); + return new MarkdownHover(this, range, item.hover.contents, false, item.ordinal, hoverSource); }); + return markdownHovers; } public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { + this._renderedHoverParts = new MarkdownRenderedHoverParts( + hoverParts, + context.fragment, + this._editor, + this._languageService, + this._openerService, + this._keybindingService, + this._hoverService, + context.onContentsChanged + ); + return this._renderedHoverParts; + } + + public updateFocusedMarkdownHoverPartVerbosityLevel(action: HoverVerbosityAction) { + this._renderedHoverParts?.updateFocusedHoverPartVerbosityLevel(action); + } +} + +interface RenderedHoverPart { + renderedMarkdown: HTMLElement; + disposables: DisposableStore; + 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 }; + + constructor( + hoverParts: MarkdownHover[], // we own! + hoverPartsContainer: DocumentFragment, + private readonly _editor: ICodeEditor, + private readonly _languageService: ILanguageService, + private readonly _openerService: IOpenerService, + private readonly _keybindingService: IKeybindingService, + private readonly _hoverService: IHoverService, + private readonly _onFinishedRendering: () => void, + ) { + super(); + this._renderedHoverParts = this._renderHoverParts(hoverParts, hoverPartsContainer, this._onFinishedRendering); + this._register(toDisposable(() => { + this._renderedHoverParts.forEach(renderedHoverPart => { + renderedHoverPart.disposables.dispose(); + }); + })); + } + + private _renderHoverParts( + hoverParts: MarkdownHover[], + hoverPartsContainer: DocumentFragment, + onFinishedRendering: () => void, + ): RenderedHoverPart[] { + hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); + return hoverParts.map((hoverPart, hoverIndex) => { + const renderedHoverPart = this._renderHoverPart( + hoverIndex, + hoverPart.contents, + hoverPart.source, + onFinishedRendering + ); + hoverPartsContainer.appendChild(renderedHoverPart.renderedMarkdown); + return renderedHoverPart; + }); + } + + private _renderHoverPart( + hoverPartIndex: number, + hoverContents: IMarkdownString[], + hoverSource: HoverSource | undefined, + onFinishedRendering: () => void + ): RenderedHoverPart { + + const { renderedMarkdown, disposables } = this._renderMarkdownContent(hoverContents, onFinishedRendering); + + if (!hoverSource) { + return { renderedMarkdown, disposables }; + } + + const canIncreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Increase); + const canDecreaseVerbosity = hoverSource.supportsVerbosityAction(HoverVerbosityAction.Decrease); + + if (!canIncreaseVerbosity && !canDecreaseVerbosity) { + return { renderedMarkdown, disposables, hoverSource }; + } + + const actionsContainer = $('div.verbosity-actions'); + renderedMarkdown.prepend(actionsContainer); + + 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 + }; + })); + disposables.add(focusTracker.onDidBlur(() => { + if (this._hoverFocusInfo?.focusRemains) { + this._hoverFocusInfo.focusRemains = false; + return; + } + })); + return { renderedMarkdown, disposables, hoverSource }; + } + + private _renderMarkdownContent( + markdownContent: IMarkdownString[], + onFinishedRendering: () => void + ): RenderedHoverPart { const renderedMarkdown = $('div.hover-row'); - context.fragment.appendChild(renderedMarkdown); + renderedMarkdown.tabIndex = 0; const renderedMarkdownContents = $('div.hover-row-contents'); renderedMarkdown.appendChild(renderedMarkdownContents); - return renderMarkdownHovers(renderedMarkdownContents, hoverParts, this._editor, this._languageService, this._openerService, context.onContentsChanged); + const disposables = new DisposableStore(); + disposables.add(renderMarkdownInContainer( + this._editor, + renderedMarkdownContents, + markdownContent, + this._languageService, + this._openerService, + onFinishedRendering, + )); + return { renderedMarkdown, disposables }; + } + + private _renderHoverExpansionAction(container: HTMLElement, action: HoverVerbosityAction, actionEnabled: boolean): DisposableStore { + const store = new DisposableStore(); + const isActionIncrease = action === HoverVerbosityAction.Increase; + const actionElement = dom.append(container, $(ThemeIcon.asCSSSelector(isActionIncrease ? increaseHoverVerbosityIcon : decreaseHoverVerbosityIcon))); + actionElement.tabIndex = 0; + if (isActionIncrease) { + const kb = this._keybindingService.lookupKeybinding(INCREASE_HOVER_VERBOSITY_ACTION_ID); + store.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), actionElement, kb ? + nls.localize('increaseVerbosityWithKb', "Increase Verbosity ({0})", kb.getLabel()) : + nls.localize('increaseVerbosity', "Increase Verbosity"))); + } else { + const kb = this._keybindingService.lookupKeybinding(DECREASE_HOVER_VERBOSITY_ACTION_ID); + store.add(this._hoverService.setupUpdatableHover(getDefaultHoverDelegate('mouse'), actionElement, kb ? + nls.localize('decreaseVerbosityWithKb', "Decrease Verbosity ({0})", kb.getLabel()) : + nls.localize('decreaseVerbosity', "Decrease Verbosity"))); + } + if (!actionEnabled) { + actionElement.classList.add('disabled'); + return store; + } + actionElement.classList.add('enabled'); + const actionFunction = () => this.updateFocusedHoverPartVerbosityLevel(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 { + const model = this._editor.getModel(); + if (!model) { + return; + } + const hoverFocusedPartIndex = this._hoverFocusInfo.hoverPartIndex; + const hoverRenderedPart = this._getRenderedHoverPartAtIndex(hoverFocusedPartIndex); + if (!hoverRenderedPart || !hoverRenderedPart.hoverSource?.supportsVerbosityAction(action)) { + return; + } + const hoverPosition = hoverRenderedPart.hoverSource.hoverPosition; + const hoverProvider = hoverRenderedPart.hoverSource.hoverProvider; + const hover = hoverRenderedPart.hoverSource.hover; + const hoverContext: HoverContext = { action, previousHover: hover }; + + let newHover: Hover | null | undefined; + try { + newHover = await Promise.resolve(hoverProvider.provideHover(model, hoverPosition, CancellationToken.None, hoverContext)); + } catch (e) { + onUnexpectedExternalError(e); + } + if (!newHover) { + return; + } + + const hoverSource = new HoverSource(newHover, hoverProvider, hoverPosition); + const renderedHoverPart = this._renderHoverPart( + hoverFocusedPartIndex, + newHover.contents, + hoverSource, + this._onFinishedRendering + ); + this._replaceRenderedHoverPartAtIndex(hoverFocusedPartIndex, renderedHoverPart); + this._focusOnHoverPartWithIndex(hoverFocusedPartIndex); + this._onFinishedRendering(); + } + + private _replaceRenderedHoverPartAtIndex(index: number, renderedHoverPart: RenderedHoverPart): void { + if (index >= this._renderHoverParts.length || index < 0) { + return; + } + const currentRenderedHoverPart = this._renderedHoverParts[index]; + const currentRenderedMarkdown = currentRenderedHoverPart.renderedMarkdown; + currentRenderedMarkdown.replaceWith(renderedHoverPart.renderedMarkdown); + currentRenderedHoverPart.disposables.dispose(); + this._renderedHoverParts[index] = renderedHoverPart; + } + + private _focusOnHoverPartWithIndex(index: number): void { + this._renderedHoverParts[index].renderedMarkdown.focus(); + this._hoverFocusInfo.focusRemains = true; + } + + private _getRenderedHoverPartAtIndex(index: number): RenderedHoverPart | undefined { + return this._renderedHoverParts[index]; } } export function renderMarkdownHovers( - container: DocumentFragment | HTMLElement, + context: IEditorHoverRenderContext, hoverParts: MarkdownHover[], editor: ICodeEditor, languageService: ILanguageService, openerService: IOpenerService, - onFinishedRendering: () => void ): IDisposable { // Sort hover parts to keep them stable since they might come in async, out-of-order - hoverParts.sort((a, b) => a.ordinal - b.ordinal); + hoverParts.sort(compareBy(hover => hover.ordinal, numberComparator)); const disposables = new DisposableStore(); for (const hoverPart of hoverParts) { - for (const contents of hoverPart.contents) { - if (isEmptyMarkdownString(contents)) { - continue; - } - const markdownHoverElement = $('div.markdown-hover'); - const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); - const renderer = disposables.add(new MarkdownRenderer({ editor }, languageService, openerService)); - disposables.add(renderer.onDidRenderAsync(() => { - hoverContentsElement.className = 'hover-contents code-hover-contents'; - onFinishedRendering(); - })); - const renderedContents = disposables.add(renderer.render(contents)); - hoverContentsElement.appendChild(renderedContents.element); - container.appendChild(markdownHoverElement); - } + disposables.add(renderMarkdownInContainer( + editor, + context.fragment, + hoverPart.contents, + languageService, + openerService, + context.onContentsChanged, + )); } return disposables; } + +function renderMarkdownInContainer( + editor: ICodeEditor, + container: DocumentFragment | HTMLElement, + markdownStrings: IMarkdownString[], + languageService: ILanguageService, + openerService: IOpenerService, + onFinishedRendering: () => void, +): IDisposable { + const store = new DisposableStore(); + for (const contents of markdownStrings) { + if (isEmptyMarkdownString(contents)) { + continue; + } + const markdownHoverElement = $('div.markdown-hover'); + const hoverContentsElement = dom.append(markdownHoverElement, $('div.hover-contents')); + const renderer = store.add(new MarkdownRenderer({ editor }, languageService, openerService)); + store.add(renderer.onDidRenderAsync(() => { + hoverContentsElement.className = 'hover-contents code-hover-contents'; + onFinishedRendering(); + })); + const renderedContents = store.add(renderer.render(contents)); + hoverContentsElement.appendChild(renderedContents.element); + container.appendChild(markdownHoverElement); + } + return store; +} diff --git a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts index ffdc5ccf50fb0..3ed0b3fab14fd 100644 --- a/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts +++ b/src/vs/editor/contrib/hover/browser/markerHoverParticipant.ts @@ -103,6 +103,7 @@ export class MarkerHoverParticipant implements IEditorHoverParticipant !isEmptyMarkdownString(item.hover.contents)) .map(item => new MarkdownHover(this, part.item.anchor.range, item.hover.contents, false, 2 + item.ordinal)); } finally { diff --git a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts index 8d25bcccde90b..f44fc76e0bdf8 100644 --- a/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts +++ b/src/vs/editor/contrib/unicodeHighlighter/browser/unicodeHighlighter.ts @@ -507,7 +507,7 @@ export class UnicodeHighlighterHoverParticipant implements IEditorHoverParticipa } public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: MarkdownHover[]): IDisposable { - return renderMarkdownHovers(context.fragment, hoverParts, this._editor, this._languageService, this._openerService, context.onContentsChanged); + return renderMarkdownHovers(context, hoverParts, this._editor, this._languageService, this._openerService); } } diff --git a/src/vs/editor/standalone/browser/standaloneLanguages.ts b/src/vs/editor/standalone/browser/standaloneLanguages.ts index 5ade938e7c29a..a7013f7d691c2 100644 --- a/src/vs/editor/standalone/browser/standaloneLanguages.ts +++ b/src/vs/editor/standalone/browser/standaloneLanguages.ts @@ -481,10 +481,10 @@ export function registerSignatureHelpProvider(languageSelector: LanguageSelector export function registerHoverProvider(languageSelector: LanguageSelector, provider: languages.HoverProvider): IDisposable { const languageFeaturesService = StandaloneServices.get(ILanguageFeaturesService); return languageFeaturesService.hoverProvider.register(languageSelector, { - provideHover: (model: model.ITextModel, position: Position, token: CancellationToken): Promise => { + provideHover: async (model: model.ITextModel, position: Position, token: CancellationToken, context?: languages.HoverContext): Promise => { const word = model.getWordAtPosition(position); - return Promise.resolve(provider.provideHover(model, position, token)).then((value): languages.Hover | undefined => { + return Promise.resolve(provider.provideHover(model, position, token, context)).then((value): languages.Hover | undefined => { if (!value) { return undefined; } @@ -810,6 +810,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages { CodeActionTriggerType: standaloneEnums.CodeActionTriggerType, NewSymbolNameTag: standaloneEnums.NewSymbolNameTag, PartialAcceptTriggerKind: standaloneEnums.PartialAcceptTriggerKind, + HoverVerbosityAction: standaloneEnums.HoverVerbosityAction, // classes FoldingRangeKind: languages.FoldingRangeKind, diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index ae56ff86935a1..99a91de53887d 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6832,19 +6832,49 @@ declare namespace monaco.languages { * current position itself. */ range?: IRange; + /** + * Can increase the verbosity of the hover + */ + canIncreaseVerbosity?: boolean; + /** + * Can decrease the verbosity of the hover + */ + canDecreaseVerbosity?: boolean; } /** * The hover provider interface defines the contract between extensions and * the [hover](https://code.visualstudio.com/docs/editor/intellisense)-feature. */ - export interface HoverProvider { + export interface HoverProvider { /** - * Provide a hover for the given position and document. Multiple hovers at the same + * Provide a hover for the given position, context and document. Multiple hovers at the same * position will be merged by the editor. A hover can have a range which defaults * to the word range at the position when omitted. */ - provideHover(model: editor.ITextModel, position: Position, token: CancellationToken): ProviderResult; + provideHover(model: editor.ITextModel, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; + } + + export interface HoverContext { + /** + * Whether to increase or decrease the hover's verbosity + */ + action?: HoverVerbosityAction; + /** + * The previous hover for the same position + */ + previousHover?: THover; + } + + export enum HoverVerbosityAction { + /** + * Increase the verbosity of the hover + */ + Increase = 0, + /** + * Decrease the verbosity of the hover + */ + Decrease = 1 } export enum CompletionItemKind { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 9c13506c29ab4..316816a6f9c91 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -32,7 +32,7 @@ import * as callh from 'vs/workbench/contrib/callHierarchy/common/callHierarchy' import * as search from 'vs/workbench/contrib/search/common/search'; import * as typeh from 'vs/workbench/contrib/typeHierarchy/common/typeHierarchy'; import { extHostNamedCustomer, IExtHostContext } from 'vs/workbench/services/extensions/common/extHostCustomers'; -import { ExtHostContext, ExtHostLanguageFeaturesShape, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; +import { ExtHostContext, ExtHostLanguageFeaturesShape, HoverWithId, ICallHierarchyItemDto, ICodeActionDto, ICodeActionProviderMetadataDto, IdentifiableInlineCompletion, IdentifiableInlineCompletions, IdentifiableInlineEdit, IDocumentDropEditDto, IDocumentDropEditProviderMetadata, IDocumentFilterDto, IIndentationRuleDto, IInlayHintDto, ILanguageConfigurationDto, ILanguageWordDefinitionDto, ILinkDto, ILocationDto, ILocationLinkDto, IOnEnterRuleDto, IPasteEditDto, IPasteEditProviderMetadataDto, IRegExpDto, ISignatureHelpProviderMetadataDto, ISuggestDataDto, ISuggestDataDtoField, ISuggestResultDtoField, ITypeHierarchyItemDto, IWorkspaceSymbolDto, MainContext, MainThreadLanguageFeaturesShape } from '../common/extHost.protocol'; import { ResourceMap } from 'vs/base/common/map'; import { isFalsyOrEmpty } from 'vs/base/common/arrays'; import { HierarchicalKind } from 'vs/base/common/hierarchicalKind'; @@ -251,9 +251,16 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread // --- extra info $registerHoverProvider(handle: number, selector: IDocumentFilterDto[]): void { + /* + const hoverFinalizationRegistry = new FinalizationRegistry((hoverId: number) => { + this._proxy.$releaseHover(handle, hoverId); + }); + */ this._registrations.set(handle, this._languageFeaturesService.hoverProvider.register(selector, { - provideHover: (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { - return this._proxy.$provideHover(handle, model.uri, position, token); + provideHover: async (model: ITextModel, position: EditorPosition, token: CancellationToken, context?: languages.HoverContext<{ id: number }>): Promise => { + const hover = await this._proxy.$provideHover(handle, model.uri, position, context, token); + // hoverFinalizationRegistry.register(hover, hover.id); + return hover; } })); } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 4494eeb26d8aa..9578d2f3d778d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1563,6 +1563,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I InlineCompletionItem: extHostTypes.InlineSuggestion, InlineCompletionList: extHostTypes.InlineSuggestionList, Hover: extHostTypes.Hover, + VerboseHover: extHostTypes.VerboseHover, + HoverVerbosityAction: extHostTypes.HoverVerbosityAction, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 39e85e70c23ea..1327e8184cc90 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -1619,6 +1619,13 @@ export interface MainThreadTimelineShape extends IDisposable { $emitTimelineChangeEvent(e: TimelineChangeEvent | undefined): void; } +export interface HoverWithId extends languages.Hover { + /** + * Id of the hover + */ + id: number; +} + // -- extension host export interface ICommandMetadataDto { @@ -2110,7 +2117,8 @@ export interface ExtHostLanguageFeaturesShape { $provideDeclaration(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideImplementation(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideTypeDefinition(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; - $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; + $provideHover(handle: number, resource: UriComponents, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken): Promise; + $releaseHover(handle: number, id: number): void; $provideEvaluatableExpression(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideInlineValues(handle: number, resource: UriComponents, range: IRange, context: languages.InlineValueContext, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 3b05c263ca52f..516129675098e 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -255,17 +255,33 @@ class TypeDefinitionAdapter { class HoverAdapter { + private _hoverCounter: number = 0; + private _hoverMap: Map = new Map(); + + private static HOVER_MAP_MAX_SIZE = 10; + constructor( private readonly _documents: ExtHostDocuments, private readonly _provider: vscode.HoverProvider, ) { } - async provideHover(resource: URI, position: IPosition, token: CancellationToken): Promise { + async provideHover(resource: URI, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken): Promise { const doc = this._documents.getDocument(resource); const pos = typeConvert.Position.to(position); - const value = await this._provider.provideHover(doc, pos, token); + let value: vscode.Hover | null | undefined; + if (context && context.previousHover !== undefined && context.action !== undefined) { + const previousHoverId = context.previousHover.id; + const previousHover = this._hoverMap.get(previousHoverId); + if (!previousHover) { + throw new Error(`Hover with id ${previousHoverId} not found`); + } + const hoverContext: vscode.HoverContext = { action: context.action, previousHover }; + value = await this._provider.provideHover(doc, pos, token, hoverContext); + } else { + value = await this._provider.provideHover(doc, pos, token); + } if (!value || isFalsyOrEmpty(value.contents)) { return undefined; } @@ -275,7 +291,24 @@ class HoverAdapter { if (!value.range) { value.range = new Range(pos, pos); } - return typeConvert.Hover.from(value); + const convertedHover: languages.Hover = typeConvert.Hover.from(value); + const id = this._hoverCounter; + // Check if hover map has more than 10 elements and if yes, remove oldest from the map + if (this._hoverMap.size === HoverAdapter.HOVER_MAP_MAX_SIZE) { + const minimumId = Math.min(...this._hoverMap.keys()); + this._hoverMap.delete(minimumId); + } + this._hoverMap.set(id, value); + this._hoverCounter += 1; + const hover: extHostProtocol.HoverWithId = { + ...convertedHover, + id + }; + return hover; + } + + releaseHover(id: number): void { + this._hoverMap.delete(id); } } @@ -2246,8 +2279,12 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._createDisposable(handle); } - $provideHover(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { - return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, token), undefined, token); + $provideHover(handle: number, resource: UriComponents, position: IPosition, context: languages.HoverContext<{ id: number }> | undefined, token: CancellationToken,): Promise { + return this._withAdapter(handle, HoverAdapter, adapter => adapter.provideHover(URI.revive(resource), position, context, token), undefined, token); + } + + $releaseHover(handle: number, id: number): void { + this._withAdapter(handle, HoverAdapter, adapter => Promise.resolve(adapter.releaseHover(id)), undefined, undefined); } // --- debug hover diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 26dc10bea0206..dd224c56a7d30 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -945,15 +945,22 @@ export namespace DefinitionLink { } export namespace Hover { - export function from(hover: vscode.Hover): languages.Hover { - return { + export function from(hover: vscode.VerboseHover): languages.Hover { + const convertedHover: languages.Hover = { range: Range.from(hover.range), - contents: MarkdownString.fromMany(hover.contents) + contents: MarkdownString.fromMany(hover.contents), + canIncreaseVerbosity: hover.canIncreaseVerbosity, + canDecreaseVerbosity: hover.canDecreaseVerbosity, }; + return convertedHover; } - export function to(info: languages.Hover): types.Hover { - return new types.Hover(info.contents.map(MarkdownString.to), Range.to(info.range)); + export function to(info: languages.Hover): types.VerboseHover { + const contents = info.contents.map(MarkdownString.to); + const range = Range.to(info.range); + const canIncreaseVerbosity = info.canIncreaseVerbosity; + const canDecreaseVerbosity = info.canDecreaseVerbosity; + return new types.VerboseHover(contents, range, canIncreaseVerbosity, canDecreaseVerbosity); } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 1c0fdba7e8527..32a0e30022cbc 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1202,6 +1202,29 @@ export class Hover { } } +@es5ClassCompat +export class VerboseHover extends Hover { + + public canIncreaseHover: boolean | undefined; + public canDecreaseHover: boolean | undefined; + + constructor( + contents: vscode.MarkdownString | vscode.MarkedString | (vscode.MarkdownString | vscode.MarkedString)[], + range?: Range, + canIncreaseHover?: boolean, + canDecreaseHover?: boolean, + ) { + super(contents, range); + this.canIncreaseHover = canIncreaseHover; + this.canDecreaseHover = canDecreaseHover; + } +} + +export enum HoverVerbosityAction { + Increase = 0, + Decrease = 1 +} + export enum DocumentHighlightKind { Text = 0, Read = 1, diff --git a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts index 3909249dc09f7..585e3745c107c 100644 --- a/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/api/test/browser/extHostLanguageFeatures.test.ts @@ -23,7 +23,7 @@ import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocum import * as languages from 'vs/editor/common/languages'; import { getCodeLensModel } from 'vs/editor/contrib/codelens/browser/codelens'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition, getDeclarationsAtPosition, getReferencesAtPosition } from 'vs/editor/contrib/gotoSymbol/browser/goToSymbol'; -import { getHoverPromise } from 'vs/editor/contrib/hover/browser/getHover'; +import { getHoversPromise } from 'vs/editor/contrib/hover/browser/getHover'; import { getOccurrencesAtPosition } from 'vs/editor/contrib/wordHighlighter/browser/wordHighlighter'; import { getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction'; import { getWorkspaceSymbols } from 'vs/workbench/contrib/search/common/search'; @@ -431,7 +431,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); const [entry] = hovers; assert.deepStrictEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 5 }); @@ -447,7 +447,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); const [entry] = hovers; assert.deepStrictEqual(entry.range, { startLineNumber: 4, startColumn: 1, endLineNumber: 9, endColumn: 8 }); @@ -469,9 +469,9 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const value = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const value = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(value.length, 2); - const [first, second] = (value as languages.Hover[]); + const [first, second] = value; assert.strictEqual(first.contents[0].value, 'registered second'); assert.strictEqual(second.contents[0].value, 'registered first'); }); @@ -491,7 +491,7 @@ suite('ExtHostLanguageFeatures', function () { })); await rpcProtocol.sync(); - const hovers = await getHoverPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); + const hovers = await getHoversPromise(languageFeaturesService.hoverProvider, model, new EditorPosition(1, 1), CancellationToken.None); assert.strictEqual(hovers.length, 1); }); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index e81b97f58cb4a..93fffb8efbc97 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -58,6 +58,7 @@ export const allApiProposals = Object.freeze({ documentFiltersExclusive: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentFiltersExclusive.d.ts', documentPaste: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.documentPaste.d.ts', editSessionIdentityProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editSessionIdentityProvider.d.ts', + editorHoverVerbosityLevel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts', editorInsets: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.editorInsets.d.ts', extensionRuntime: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionRuntime.d.ts', extensionsAny: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.extensionsAny.d.ts', diff --git a/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts new file mode 100644 index 0000000000000..5c0d97905e866 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.editorHoverVerbosityLevel.d.ts @@ -0,0 +1,76 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + /** + * A hover represents additional information for a symbol or word. Hovers are + * rendered in a tooltip-like widget. + */ + export class VerboseHover extends Hover { + + /** + * Can increase the verbosity of the hover + */ + canIncreaseVerbosity?: boolean; + + /** + * Can decrease the verbosity of the hover + */ + canDecreaseVerbosity?: boolean; + + /** + * Creates a new hover object. + * + * @param contents The contents of the hover. + * @param range The range to which the hover applies. + */ + constructor(contents: MarkdownString | MarkedString | Array, range?: Range, canIncreaseVerbosity?: boolean, canDecreaseVerbosity?: boolean); + } + + export interface HoverContext { + + /** + * Whether to increase or decrease the hover's verbosity + */ + action?: HoverVerbosityAction; + + /** + * The previous hover sent for the same position + */ + previousHover?: Hover; + } + + export enum HoverVerbosityAction { + /** + * Increase the hover verbosity + */ + Increase = 0, + /** + * Decrease the hover verbosity + */ + Decrease = 1 + } + + /** + * The hover provider class + */ + export interface HoverProvider { + + /** + * Provide a hover for the given position and document. Multiple hovers at the same + * position will be merged by the editor. A hover can have a range which defaults + * to the word range at the position when omitted. + * + * @param document The document in which the command was invoked. + * @param position The position at which the command was invoked. + * @param token A cancellation token. + * @oaram context A hover context. + * @returns A hover or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideHover(document: TextDocument, position: Position, token: CancellationToken, context?: HoverContext): ProviderResult; + } +}