diff --git a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts index 6756b62b36f61..b523c21846671 100644 --- a/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts +++ b/extensions/typescript-language-features/src/languageFeatures/inlayHints.ts @@ -75,8 +75,8 @@ class TypeScriptInlayHintsProvider extends Disposable implements vscode.InlayHin return response.body.map(hint => { const result = new vscode.InlayHint( - hint.text, Position.fromLocation(hint.position), + hint.text, hint.kind && fromProtocolInlayHintKind(hint.kind) ); result.paddingLeft = hint.whitespaceBefore; diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 68fb919c5a5e3..400d415229cd9 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1802,6 +1802,7 @@ export interface InlayHintLabelPart { export interface InlayHint { label: string | InlayHintLabelPart[]; tooltip?: string | IMarkdownString; + command?: Command; position: IPosition; kind: InlayHintKind; paddingLeft?: boolean; diff --git a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts index 7ca1faaccb01f..00cf78e8743d1 100644 --- a/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts +++ b/src/vs/editor/contrib/inlayHints/browser/inlayHintsController.ts @@ -211,6 +211,7 @@ export class InlayHintsController implements IEditorContribution { // mouse gestures this._sessionDisposables.add(this._installLinkGesture()); + this._sessionDisposables.add(this._installDblClickGesture()); this._sessionDisposables.add(this._installContextMenu()); } @@ -264,21 +265,29 @@ export class InlayHintsController implements IEditorContribution { this._instaService.invokeFunction(goToDefinitionWithLocation, e, this._editor as IActiveCodeEditor, part.location); } else if (languages.Command.is(part.command)) { // command -> execute it - try { - await this._commandService.executeCommand(part.command.id, ...(part.command.arguments ?? [])); - } catch (err) { - this._notificationService.notify({ - severity: Severity.Error, - source: label.item.provider.displayName, - message: err - }); - } + await this._invokeCommand(part.command, label.item); } } }); return gesture; } + private _installDblClickGesture(): IDisposable { + return this._editor.onMouseUp(async e => { + if (e.event.detail !== 2) { + return; + } + const part = this._getInlayHintLabelPart(e); + if (!part) { + return; + } + await part.item.resolve(CancellationToken.None); + if (part.item.hint.command) { + await this._invokeCommand(part.item.hint.command, part.item); + } + }); + } + private _installContextMenu(): IDisposable { return this._editor.onContextMenu(async e => { if (!(e.event.target instanceof HTMLElement)) { @@ -302,6 +311,18 @@ export class InlayHintsController implements IEditorContribution { return undefined; } + private async _invokeCommand(command: languages.Command, item: InlayHintItem) { + try { + await this._commandService.executeCommand(command.id, ...(command.arguments ?? [])); + } catch (err) { + this._notificationService.notify({ + severity: Severity.Error, + source: item.provider.displayName, + message: err + }); + } + } + private _cacheHintsForFastRestore(model: ITextModel): void { const items = new Map(); for (const [id, obj] of this._decorationsMetadata) { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 94932425c4e37..ee2d925000c5b 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6910,6 +6910,7 @@ declare namespace monaco.languages { export interface InlayHint { label: string | InlayHintLabelPart[]; tooltip?: string | IMarkdownString; + command?: Command; position: IPosition; kind: InlayHintKind; paddingLeft?: boolean; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 8629a9048e9b2..4a9554ded8535 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1236,7 +1236,7 @@ class InlayHintsAdapter { } private _isValidInlayHint(hint: vscode.InlayHint, range?: vscode.Range): boolean { - if (hint.label.length === 0 || Array.isArray(hint.label) && hint.label.every(part => part.label.length === 0)) { + if (hint.label.length === 0 || Array.isArray(hint.label) && hint.label.every(part => part.value.length === 0)) { console.log('INVALID inlay hint, empty label', hint); return false; } @@ -1257,7 +1257,8 @@ class InlayHintsAdapter { const result: extHostProtocol.IInlayHintDto = { label: '', // fill-in below cacheId: id, - tooltip: hint.tooltip && typeConvert.MarkdownString.from(hint.tooltip), + tooltip: typeConvert.MarkdownString.fromStrict(hint.tooltip), + command: hint.command && this._commands.toInternal(hint.command, disposables), position: typeConvert.Position.from(hint.position), kind: typeConvert.InlayHintKind.from(hint.kind ?? InlayHintKind.Other), paddingLeft: hint.paddingLeft, @@ -1268,8 +1269,8 @@ class InlayHintsAdapter { result.label = hint.label; } else { result.label = hint.label.map(part => { - let result: languages.InlayHintLabelPart = { label: part.label }; - result.tooltip = part.tooltip && typeConvert.MarkdownString.from(part.tooltip); + let result: languages.InlayHintLabelPart = { label: part.value }; + result.tooltip = typeConvert.MarkdownString.fromStrict(part.tooltip); if (Location.isLocation(part.location)) { result.location = typeConvert.location.from(part.location); } diff --git a/src/vs/workbench/api/common/extHostStatusBar.ts b/src/vs/workbench/api/common/extHostStatusBar.ts index 75ba899e35cd8..a9076f1e20fbe 100644 --- a/src/vs/workbench/api/common/extHostStatusBar.ts +++ b/src/vs/workbench/api/common/extHostStatusBar.ts @@ -234,7 +234,7 @@ export class ExtHostStatusBarEntry implements vscode.StatusBarItem { color = ExtHostStatusBarEntry.ALLOWED_BACKGROUND_COLORS.get(this._backgroundColor.id); } - const tooltip = this._tooltip ? MarkdownString.fromStrict(this._tooltip) : undefined; + const tooltip = MarkdownString.fromStrict(this._tooltip); // Set to status bar this.#proxy.$setEntry(this._entryId, id, name, this._text, tooltip, this._command?.internal, color, diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 76c6a0738a9d6..4e31aef9e99e3 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -391,7 +391,7 @@ export namespace MarkdownString { return result; } - export function fromStrict(value: string | vscode.MarkdownString): undefined | string | htmlContent.IMarkdownString { + export function fromStrict(value: string | vscode.MarkdownString | undefined | null): undefined | string | htmlContent.IMarkdownString { if (!value) { return undefined; } @@ -1110,7 +1110,7 @@ export namespace ParameterInformation { export function from(info: types.ParameterInformation): languages.ParameterInformation { return { label: info.label, - documentation: info.documentation ? MarkdownString.fromStrict(info.documentation) : undefined + documentation: MarkdownString.fromStrict(info.documentation) }; } export function to(info: languages.ParameterInformation): types.ParameterInformation { @@ -1126,7 +1126,7 @@ export namespace SignatureInformation { export function from(info: types.SignatureInformation): languages.SignatureInformation { return { label: info.label, - documentation: info.documentation ? MarkdownString.fromStrict(info.documentation) : undefined, + documentation: MarkdownString.fromStrict(info.documentation), parameters: Array.isArray(info.parameters) ? info.parameters.map(ParameterInformation.from) : [], activeParameter: info.activeParameter, }; @@ -1165,11 +1165,12 @@ export namespace InlayHint { export function to(converter: Command.ICommandsConverter, hint: languages.InlayHint): vscode.InlayHint { const res = new types.InlayHint( - typeof hint.label === 'string' ? hint.label : hint.label.map(InlayHintLabelPart.to.bind(undefined, converter)), Position.to(hint.position), + typeof hint.label === 'string' ? hint.label : hint.label.map(InlayHintLabelPart.to.bind(undefined, converter)), InlayHintKind.to(hint.kind) ); res.tooltip = htmlContent.isMarkdownString(hint.tooltip) ? MarkdownString.to(hint.tooltip) : hint.tooltip; + res.command = hint.command && converter.fromInternal(hint.command); res.paddingLeft = hint.paddingLeft; res.paddingRight = hint.paddingRight; return res; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 5cada1f2c86c6..3a6db1043e55a 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1432,13 +1432,13 @@ export enum InlayHintKind { @es5ClassCompat export class InlayHintLabelPart { - label: string; + value: string; tooltip?: string | vscode.MarkdownString; location?: Location; command?: vscode.Command; - constructor(label: string) { - this.label = label; + constructor(value: string) { + this.value = value; } } @@ -1451,10 +1451,11 @@ export class InlayHint implements vscode.InlayHint { kind?: vscode.InlayHintKind; paddingLeft?: boolean; paddingRight?: boolean; + command?: vscode.Command; - constructor(label: string | InlayHintLabelPart[], position: Position, kind?: vscode.InlayHintKind) { - this.label = label; + constructor(position: Position, label: string | InlayHintLabelPart[], kind?: vscode.InlayHintKind) { this.position = position; + this.label = label; this.kind = kind; } } diff --git a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts index f9ec90035b279..bb60ad1fa8c93 100644 --- a/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts +++ b/src/vs/workbench/api/test/browser/extHostApiCommands.test.ts @@ -57,6 +57,7 @@ import 'vs/editor/contrib/rename/browser/rename'; import 'vs/editor/contrib/inlayHints/browser/inlayHintsController'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { LanguageFeaturesService } from 'vs/editor/common/services/languageFeaturesService'; +import { assertType } from 'vs/base/common/types'; function assertRejects(fn: () => Promise, message: string = 'Expected rejection') { return fn().then(() => assert.ok(false, message), _err => assert.ok(true)); @@ -1250,7 +1251,7 @@ suite('ExtHostLanguageFeatureCommands', function () { test('Inlay Hints, back and forth', async function () { disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { provideInlayHints() { - return [new types.InlayHint('Foo', new types.Position(0, 1))]; + return [new types.InlayHint(new types.Position(0, 1), 'Foo')]; } })); @@ -1268,13 +1269,21 @@ suite('ExtHostLanguageFeatureCommands', function () { test('Inline Hints, merge', async function () { disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { provideInlayHints() { - return [new types.InlayHint('Bar', new types.Position(10, 11))]; + const part = new types.InlayHintLabelPart('Bar'); + part.tooltip = 'part_tooltip'; + part.command = { command: 'cmd', title: 'part' }; + const hint = new types.InlayHint(new types.Position(10, 11), [part]); + hint.tooltip = 'hint_tooltip'; + hint.command = { command: 'cmd', title: 'hint' }; + hint.paddingLeft = true; + hint.paddingRight = false; + return [hint]; } })); disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { provideInlayHints() { - const hint = new types.InlayHint('Foo', new types.Position(0, 1), types.InlayHintKind.Parameter); + const hint = new types.InlayHint(new types.Position(0, 1), 'Foo', types.InlayHintKind.Parameter); return [hint]; } })); @@ -1289,15 +1298,26 @@ suite('ExtHostLanguageFeatureCommands', function () { assert.strictEqual(first.position.line, 0); assert.strictEqual(first.position.character, 1); - assert.strictEqual(second.label, 'Bar'); assert.strictEqual(second.position.line, 10); assert.strictEqual(second.position.character, 11); + assert.strictEqual(second.paddingLeft, true); + assert.strictEqual(second.paddingRight, false); + assert.strictEqual(second.tooltip, 'hint_tooltip'); + assert.strictEqual(second.command?.command, 'cmd'); + assert.strictEqual(second.command?.title, 'hint'); + + const label = (second.label)[0]; + assertType(label instanceof types.InlayHintLabelPart); + assert.strictEqual(label.value, 'Bar'); + assert.strictEqual(label.tooltip, 'part_tooltip'); + assert.strictEqual(label.command?.command, 'cmd'); + assert.strictEqual(label.command?.title, 'part'); }); test('Inline Hints, bad provider', async function () { disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { provideInlayHints() { - return [new types.InlayHint('Foo', new types.Position(0, 1))]; + return [new types.InlayHint(new types.Position(0, 1), 'Foo')]; } })); disposables.push(extHost.registerInlayHintsProvider(nullExtensionDescription, defaultSelector, { diff --git a/src/vscode-dts/vscode.proposed.inlayHints.d.ts b/src/vscode-dts/vscode.proposed.inlayHints.d.ts index fd5d6a4678ce5..653b69d1c74e1 100644 --- a/src/vscode-dts/vscode.proposed.inlayHints.d.ts +++ b/src/vscode-dts/vscode.proposed.inlayHints.d.ts @@ -7,11 +7,9 @@ declare module 'vscode' { // https://github.com/microsoft/vscode/issues/16221 - // todo@API Split between Inlay- and OverlayHints (InlayHint are for a position, OverlayHints for a non-empty range) - // (done) add "mini-markdown" for links and styles - // (done) remove description - // (done) rename to InlayHint - // (done) add InlayHintKind with type, argument, etc + // todo@API support over an optional overlay range + // todo@API all for more InlayHintLabelPart commands + // todo@API allow for InlayHintLabelPart#colors? export namespace languages { /** @@ -37,15 +35,21 @@ declare module 'vscode' { Parameter = 2, } + /** + * An inlay hint label part allows for interactive and composite labels of inlay hints. + */ export class InlayHintLabelPart { /** * The value of this label part. */ - label: string; + value: string; /** * The tooltip text when you hover over this label part. + * + * *Note* that this property can be set late during + * {@link InlayHintsProvider.resolveInlayHint resolving} of inlay hints. */ tooltip?: string | MarkdownString | undefined; @@ -74,29 +78,41 @@ declare module 'vscode' { */ command?: Command | undefined; - // todo@api - // context menu, contextMenuCommands - // secondaryCommands?: Command[]; - - constructor(label: string); + /** + * Creates a new inlay hint label part. + * + * @param value The value of the part. + */ + constructor(value: string); } /** * Inlay hint information. */ export class InlayHint { + /** * The position of this hint. */ position: Position; + /** + * The label of this hint. A human readable string or an array of {@link InlayHintLabelPart label parts}. * + * *Note* that neiter the string nor the label part can be empty. */ label: string | InlayHintLabelPart[]; + /** * The tooltip text when you hover over this item. */ tooltip?: string | MarkdownString | undefined; + + /** + * Optional command that will be the default gesture of this inlay hint. + */ + command?: Command; + /** * The kind of this hint. */ @@ -106,16 +122,20 @@ declare module 'vscode' { * Render padding before the hint. */ paddingLeft?: boolean; + /** * Render padding after the hint. */ paddingRight?: boolean; - // emphemeral overlay mode - // overlayRange?: Range; - - // todo@API make range first argument - constructor(label: string | InlayHintLabelPart[], position: Position, kind?: InlayHintKind); + /** + * Creates a new inlay hint. + * + * @param position The position of the hint. + * @param label The label of the hint. + * @param kind The {@link InlayHintKind kind} of the hint. + */ + constructor(position: Position, label: string | InlayHintLabelPart[], kind?: InlayHintKind); } /** @@ -132,7 +152,7 @@ declare module 'vscode' { /** * Provide inlay hints for the given range and document. * - * *Note* that inlay hints that are not {@link Range.contains contained} by the range are ignored. + * *Note* that inlay hints that are not {@link Range.contains contained} by the given range are ignored. * * @param document The document in which the command was invoked. * @param range The range for which inlay hints should be computed. @@ -142,9 +162,10 @@ declare module 'vscode' { provideInlayHints(document: TextDocument, range: Range, token: CancellationToken): ProviderResult; /** - * Given an inlay hint fill in {@link InlayHint.tooltip tooltip} or complete label {@link InlayHintLabelPart parts}. + * Given an inlay hint fill in {@link InlayHint.tooltip tooltip}, {@link InlayHint.command command}, or complete + * label {@link InlayHintLabelPart parts}. * - * The editor will at most resolve an inlay hint once. + * *Note* that the editor will resolve an inlay hint at most once. * * @param hint An inlay hint. * @param token A cancellation token.