diff --git a/src/vs/editor/browser/services/codeEditorServiceImpl.ts b/src/vs/editor/browser/services/codeEditorServiceImpl.ts index c961bfd795664..952b317c64016 100644 --- a/src/vs/editor/browser/services/codeEditorServiceImpl.ts +++ b/src/vs/editor/browser/services/codeEditorServiceImpl.ts @@ -347,6 +347,8 @@ const _CSS_MAP: { [prop: string]: string; } = { fontStyle: 'font-style:{0};', fontWeight: 'font-weight:{0};', + fontSize: 'font-size:{0};', + fontFamily: 'font-family:{0};', textDecoration: 'text-decoration:{0};', cursor: 'cursor:{0};', letterSpacing: 'letter-spacing:{0};', @@ -357,6 +359,7 @@ const _CSS_MAP: { [prop: string]: string; } = { contentText: 'content:\'{0}\';', contentIconPath: 'content:{0};', margin: 'margin:{0};', + padding: 'padding:{0};', width: 'width:{0};', height: 'height:{0};' }; @@ -529,7 +532,7 @@ class DecorationCSSRules { cssTextArr.push(strings.format(_CSS_MAP.contentText, escaped)); } - this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin'], cssTextArr); + this.collectCSSText(opts, ['fontStyle', 'fontWeight', 'fontSize', 'fontFamily', 'textDecoration', 'color', 'opacity', 'backgroundColor', 'margin', 'padding'], cssTextArr); if (this.collectCSSText(opts, ['width', 'height'], cssTextArr)) { cssTextArr.push('display:inline-block;'); } diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 40bf341ec262c..15cd2f345e63d 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -1835,6 +1835,7 @@ export class EditorModeContext extends Disposable { private readonly _hasMultipleDocumentFormattingProvider: IContextKey; private readonly _hasMultipleDocumentSelectionFormattingProvider: IContextKey; private readonly _hasSignatureHelpProvider: IContextKey; + private readonly _hasInlineHintsProvider: IContextKey; private readonly _isInWalkThrough: IContextKey; constructor( @@ -1857,6 +1858,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider = EditorContextKeys.hasReferenceProvider.bindTo(_contextKeyService); this._hasRenameProvider = EditorContextKeys.hasRenameProvider.bindTo(_contextKeyService); this._hasSignatureHelpProvider = EditorContextKeys.hasSignatureHelpProvider.bindTo(_contextKeyService); + this._hasInlineHintsProvider = EditorContextKeys.hasInlineHintsProvider.bindTo(_contextKeyService); this._hasDocumentFormattingProvider = EditorContextKeys.hasDocumentFormattingProvider.bindTo(_contextKeyService); this._hasDocumentSelectionFormattingProvider = EditorContextKeys.hasDocumentSelectionFormattingProvider.bindTo(_contextKeyService); this._hasMultipleDocumentFormattingProvider = EditorContextKeys.hasMultipleDocumentFormattingProvider.bindTo(_contextKeyService); @@ -1885,6 +1887,7 @@ export class EditorModeContext extends Disposable { this._register(modes.DocumentFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.DocumentRangeFormattingEditProviderRegistry.onDidChange(update)); this._register(modes.SignatureHelpProviderRegistry.onDidChange(update)); + this._register(modes.InlineHintsProviderRegistry.onDidChange(update)); update(); } @@ -1936,6 +1939,7 @@ export class EditorModeContext extends Disposable { this._hasReferenceProvider.set(modes.ReferenceProviderRegistry.has(model)); this._hasRenameProvider.set(modes.RenameProviderRegistry.has(model)); this._hasSignatureHelpProvider.set(modes.SignatureHelpProviderRegistry.has(model)); + this._hasInlineHintsProvider.set(modes.InlineHintsProviderRegistry.has(model)); this._hasDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.has(model) || modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasDocumentSelectionFormattingProvider.set(modes.DocumentRangeFormattingEditProviderRegistry.has(model)); this._hasMultipleDocumentFormattingProvider.set(modes.DocumentFormattingEditProviderRegistry.all(model).length + modes.DocumentRangeFormattingEditProviderRegistry.all(model).length > 1); diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index 79397349a6546..bce85536162d3 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -625,6 +625,10 @@ export interface IEditorOptions { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -2369,6 +2373,74 @@ class EditorLightbulb extends BaseEditorOption>; + +class EditorInlineHints extends BaseEditorOption { + + constructor() { + const defaults: EditorInlineHintsOptions = { enabled: true, fontSize: 0, fontFamily: EDITOR_FONT_DEFAULTS.fontFamily }; + super( + EditorOption.inlineHints, 'inlineHints', defaults, + { + 'editor.inlineHints.enabled': { + type: 'boolean', + default: defaults.enabled, + description: nls.localize('inlineHints.enable', "Enables the inline hints in the editor.") + }, + 'editor.inlineHints.fontSize': { + type: 'number', + default: defaults.fontSize, + description: nls.localize('inlineHints.fontSize', "Controls font size of inline hints in the editor. When set to `0`, the 90% of `#editor.fontSize#` is used.") + }, + 'editor.inlineHints.fontFamily': { + type: 'string', + default: defaults.fontFamily, + description: nls.localize('inlineHints.fontFamily', "Controls font family of inline hints in the editor.") + }, + } + ); + } + + public validate(_input: any): EditorInlineHintsOptions { + if (!_input || typeof _input !== 'object') { + return this.defaultValue; + } + const input = _input as IEditorInlineHintsOptions; + return { + enabled: boolean(input.enabled, this.defaultValue.enabled), + fontSize: EditorIntOption.clampedInt(input.fontSize, this.defaultValue.fontSize, 0, 100), + fontFamily: EditorStringOption.string(input.fontFamily, this.defaultValue.fontFamily) + }; + } +} + +//#endregion + //#region lineHeight class EditorLineHeight extends EditorIntOption { @@ -3759,7 +3831,7 @@ export const enum EditorOption { wrappingIndent, wrappingStrategy, showDeprecated, - + inlineHints, // Leave these at the end (because they have dependencies!) editorClassName, pixelRatio, @@ -4263,6 +4335,7 @@ export const EditorOptions = { EditorOption.showDeprecated, 'showDeprecated', true, { description: nls.localize('showDeprecated', "Controls strikethrough deprecated variables.") } )), + inlineHints: register(new EditorInlineHints()), snippetSuggestions: register(new EditorStringEnumOption( EditorOption.snippetSuggestions, 'snippetSuggestions', 'inline' as 'top' | 'bottom' | 'inline' | 'none', diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 52a770b6edda3..1f36f72930f88 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -606,6 +606,7 @@ export interface IThemeDecorationRenderOptions { fontStyle?: string; fontWeight?: string; + fontSize?: string; textDecoration?: string; cursor?: string; color?: string | ThemeColor; @@ -632,11 +633,14 @@ export interface IContentDecorationRenderOptions { borderColor?: string | ThemeColor; fontStyle?: string; fontWeight?: string; + fontSize?: string; + fontFamily?: string; textDecoration?: string; color?: string | ThemeColor; backgroundColor?: string | ThemeColor; margin?: string; + padding?: string; width?: string; height?: string; } diff --git a/src/vs/editor/common/editorContextKeys.ts b/src/vs/editor/common/editorContextKeys.ts index a3a3b3de1dc6a..2f24e31188725 100644 --- a/src/vs/editor/common/editorContextKeys.ts +++ b/src/vs/editor/common/editorContextKeys.ts @@ -61,6 +61,7 @@ export namespace EditorContextKeys { export const hasReferenceProvider = new RawContextKey('editorHasReferenceProvider', false); export const hasRenameProvider = new RawContextKey('editorHasRenameProvider', false); export const hasSignatureHelpProvider = new RawContextKey('editorHasSignatureHelpProvider', false); + export const hasInlineHintsProvider = new RawContextKey('editorHasInlineHintsProvider', false); // -- mode context keys: formatting export const hasDocumentFormattingProvider = new RawContextKey('editorHasDocumentFormattingProvider', false); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index b0ddffe0e42d8..28b30aadea39e 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1659,6 +1659,19 @@ export interface CodeLensProvider { resolveCodeLens?(model: model.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } +export interface InlineHint { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface InlineHintsProvider { + onDidChangeInlineHints?: Event | undefined; + provideInlineHints(model: model.ITextModel, range: Range, token: CancellationToken): ProviderResult; +} + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; @@ -1764,6 +1777,11 @@ export const TypeDefinitionProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const InlineHintsProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/common/standalone/standaloneEnums.ts b/src/vs/editor/common/standalone/standaloneEnums.ts index 8c772e3c39603..ad85fc86c5b25 100644 --- a/src/vs/editor/common/standalone/standaloneEnums.ts +++ b/src/vs/editor/common/standalone/standaloneEnums.ts @@ -287,11 +287,12 @@ export enum EditorOption { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } /** diff --git a/src/vs/editor/contrib/inlineHints/inlineHintsController.ts b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts new file mode 100644 index 0000000000000..0a5d3dd6622aa --- /dev/null +++ b/src/vs/editor/contrib/inlineHints/inlineHintsController.ts @@ -0,0 +1,222 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { RunOnceScheduler } from 'vs/base/common/async'; +import { onUnexpectedExternalError } from 'vs/base/common/errors'; +import { hash } from 'vs/base/common/hash'; +import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { registerEditorContribution } from 'vs/editor/browser/editorExtensions'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IContentDecorationRenderOptions, IEditorContribution } from 'vs/editor/common/editorCommon'; +import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model'; +import { InlineHintsProvider, InlineHintsProviderRegistry, InlineHint } from 'vs/editor/common/modes'; +import { EditorOption } from 'vs/editor/common/config/editorOptions'; +import { flatten } from 'vs/base/common/arrays'; +import { editorInlineHintForeground, editorInlineHintBackground } from 'vs/platform/theme/common/colorRegistry'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; +import { Range } from 'vs/editor/common/core/range'; +import { LanguageFeatureRequestDelays } from 'vs/editor/common/modes/languageFeatureRegistry'; +import { MarkdownString } from 'vs/base/common/htmlContent'; +import { CommandsRegistry } from 'vs/platform/commands/common/commands'; +import { URI } from 'vs/base/common/uri'; +import { IRange } from 'vs/base/common/range'; +import { assertType } from 'vs/base/common/types'; +import { ITextModelService } from 'vs/editor/common/services/resolverService'; + +const MAX_DECORATORS = 500; + +export interface InlineHintsData { + list: InlineHint[]; + provider: InlineHintsProvider; +} + +export async function getInlineHints(model: ITextModel, ranges: Range[], token: CancellationToken): Promise { + const datas: InlineHintsData[] = []; + const providers = InlineHintsProviderRegistry.ordered(model).reverse(); + const promises = flatten(providers.map(provider => ranges.map(range => Promise.resolve(provider.provideInlineHints(model, range, token)).then(result => { + if (result) { + datas.push({ list: result, provider }); + } + }, err => { + onUnexpectedExternalError(err); + })))); + + await Promise.all(promises); + + return datas; +} + +export class InlineHintsController implements IEditorContribution { + + static readonly ID: string = 'editor.contrib.InlineHints'; + + // static get(editor: ICodeEditor): InlineHintsController { + // return editor.getContribution(this.ID); + // } + + private readonly _disposables = new DisposableStore(); + private readonly _sessionDisposables = new DisposableStore(); + private readonly _getInlineHintsDelays = new LanguageFeatureRequestDelays(InlineHintsProviderRegistry, 250, 2500); + + private _decorationsTypeIds: string[] = []; + private _decorationIds: string[] = []; + + constructor(private readonly _editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IThemeService private readonly _themeService: IThemeService, + ) { + this._disposables.add(InlineHintsProviderRegistry.onDidChange(() => this._update())); + this._disposables.add(_editor.onDidChangeModel(() => this._update())); + this._disposables.add(_editor.onDidChangeModelLanguage(() => this._update())); + this._disposables.add(_editor.onDidChangeConfiguration(e => { + if (e.hasChanged(EditorOption.inlineHints)) { + this._update(); + } + })); + + this._update(); + } + + dispose(): void { + this._sessionDisposables.dispose(); + this._removeAllDecorations(); + this._disposables.dispose(); + } + + private _update(): void { + this._sessionDisposables.clear(); + + if (!this._editor.getOption(EditorOption.inlineHints).enabled) { + this._removeAllDecorations(); + return; + } + + const model = this._editor.getModel(); + if (!model || !InlineHintsProviderRegistry.has(model)) { + this._removeAllDecorations(); + return; + } + + const scheduler = new RunOnceScheduler(async () => { + const t1 = Date.now(); + + const cts = new CancellationTokenSource(); + this._sessionDisposables.add(toDisposable(() => cts.dispose(true))); + + const visibleRanges = this._editor.getVisibleRangesPlusViewportAboveBelow(); + const result = await getInlineHints(model, visibleRanges, cts.token); + + // update moving average + const newDelay = this._getInlineHintsDelays.update(model, Date.now() - t1); + scheduler.delay = newDelay; + + // render hints + this._updateHintsDecorators(result); + + }, this._getInlineHintsDelays.get(model)); + + this._sessionDisposables.add(scheduler); + + // update inline hints when content or scroll position changes + this._sessionDisposables.add(this._editor.onDidChangeModelContent(() => scheduler.schedule())); + this._disposables.add(this._editor.onDidScrollChange(() => scheduler.schedule())); + scheduler.schedule(); + + // update inline hints when any any provider fires an event + const providerListener = new DisposableStore(); + this._sessionDisposables.add(providerListener); + for (const provider of InlineHintsProviderRegistry.all(model)) { + if (typeof provider.onDidChangeInlineHints === 'function') { + providerListener.add(provider.onDidChangeInlineHints(() => scheduler.schedule())); + } + } + } + + private _updateHintsDecorators(hintsData: InlineHintsData[]): void { + const { fontSize, fontFamily } = this._getLayoutInfo(); + const backgroundColor = this._themeService.getColorTheme().getColor(editorInlineHintBackground); + const fontColor = this._themeService.getColorTheme().getColor(editorInlineHintForeground); + + const newDecorationsTypeIds: string[] = []; + const newDecorationsData: IModelDeltaDecoration[] = []; + + for (const { list: hints } of hintsData) { + + for (let j = 0; j < hints.length && newDecorationsData.length < MAX_DECORATORS; j++) { + const { text, range, hoverMessage, whitespaceBefore, whitespaceAfter } = hints[j]; + const marginBefore = whitespaceBefore ? (fontSize / 3) | 0 : 0; + const marginAfter = whitespaceAfter ? (fontSize / 3) | 0 : 0; + + const before: IContentDecorationRenderOptions = { + contentText: text, + backgroundColor: `${backgroundColor}`, + color: `${fontColor}`, + margin: `0px ${marginAfter}px 0px ${marginBefore}px`, + fontSize: `${fontSize}px`, + fontFamily: fontFamily, + padding: `0px ${(fontSize / 4) | 0}px` + }; + const key = 'inlineHints-' + hash(before).toString(16); + this._codeEditorService.registerDecorationType(key, { before }, undefined, this._editor); + + // decoration types are ref-counted which means we only need to + // call register und remove equally often + newDecorationsTypeIds.push(key); + + const options = this._codeEditorService.resolveDecorationOptions(key, true); + if (hoverMessage) { + options.hoverMessage = new MarkdownString().appendText(hoverMessage); + } + + newDecorationsData.push({ + range, + options + }); + } + } + + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = newDecorationsTypeIds; + + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, newDecorationsData); + } + + private _getLayoutInfo() { + const options = this._editor.getOption(EditorOption.inlineHints); + const editorFontSize = this._editor.getOption(EditorOption.fontSize); + let fontSize = options.fontSize; + if (!fontSize || fontSize < 5 || fontSize > editorFontSize) { + fontSize = (editorFontSize * .9) | 0; + } + const fontFamily = options.fontFamily; + return { fontSize, fontFamily }; + } + + private _removeAllDecorations(): void { + this._decorationIds = this._editor.deltaDecorations(this._decorationIds, []); + this._decorationsTypeIds.forEach(this._codeEditorService.removeDecorationType, this._codeEditorService); + this._decorationsTypeIds = []; + } +} + +registerEditorContribution(InlineHintsController.ID, InlineHintsController); + +CommandsRegistry.registerCommand('_executeInlineHintProvider', async (accessor, ...args: [URI, IRange]): Promise => { + + const [uri, range] = args; + assertType(URI.isUri(uri)); + assertType(Range.isIRange(range)); + + const ref = await accessor.get(ITextModelService).createModelReference(uri); + try { + const data = await getInlineHints(ref.object.textEditorModel, [Range.lift(range)], CancellationToken.None); + return flatten(data.map(item => item.list)).sort((a, b) => Range.compareRangesUsingStarts(a.range, b.range)); + + } finally { + ref.dispose(); + } +}); diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 04ff0686f6f64..9b4870b45b91f 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -29,6 +29,7 @@ import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; import 'vs/editor/contrib/hover/hover'; import 'vs/editor/contrib/indentation/indentation'; +import 'vs/editor/contrib/inlineHints/inlineHintsController'; import 'vs/editor/contrib/inPlaceReplace/inPlaceReplace'; import 'vs/editor/contrib/linesOperations/linesOperations'; import 'vs/editor/contrib/linkedEditing/linkedEditing'; diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index fa9796206b8a0..1c0703a1c3cd0 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -3181,6 +3181,10 @@ declare namespace monaco.editor { * Controls strikethrough deprecated variables. */ showDeprecated?: boolean; + /** + * Control the behavior and rendering of the inline hints. + */ + inlineHints?: IEditorInlineHintsOptions; } /** @@ -3532,6 +3536,29 @@ declare namespace monaco.editor { export type EditorLightbulbOptions = Readonly>; + /** + * Configuration options for editor inlineHints + */ + export interface IEditorInlineHintsOptions { + /** + * Enable the inline hints. + * Defaults to true. + */ + enabled?: boolean; + /** + * Font size of inline hints. + * Default to 90% of the editor font size. + */ + fontSize?: number; + /** + * Font family of inline hints. + * Defaults to editor font family. + */ + fontFamily?: string; + } + + export type EditorInlineHintsOptions = Readonly>; + /** * Configuration options for editor minimap */ @@ -4033,11 +4060,12 @@ declare namespace monaco.editor { wrappingIndent = 117, wrappingStrategy = 118, showDeprecated = 119, - editorClassName = 120, - pixelRatio = 121, - tabFocusMode = 122, - layoutInfo = 123, - wrappingInfo = 124 + inlineHints = 120, + editorClassName = 121, + pixelRatio = 122, + tabFocusMode = 123, + layoutInfo = 124, + wrappingInfo = 125 } export const EditorOptions: { acceptSuggestionOnCommitCharacter: IEditorOption; @@ -4138,6 +4166,7 @@ declare namespace monaco.editor { showFoldingControls: IEditorOption; showUnused: IEditorOption; showDeprecated: IEditorOption; + inlineHints: IEditorOption; snippetSuggestions: IEditorOption; smartSelect: IEditorOption; smoothScrolling: IEditorOption; @@ -6374,6 +6403,19 @@ declare namespace monaco.languages { resolveCodeLens?(model: editor.ITextModel, codeLens: CodeLens, token: CancellationToken): ProviderResult; } + export interface InlineHint { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + } + + export interface InlineHintsProvider { + onDidChangeInlineHints?: IEvent | undefined; + provideInlineHints(model: editor.ITextModel, range: Range, token: CancellationToken): ProviderResult; + } + export interface SemanticTokensLegend { readonly tokenTypes: string[]; readonly tokenModifiers: string[]; diff --git a/src/vs/platform/theme/common/colorRegistry.ts b/src/vs/platform/theme/common/colorRegistry.ts index b5239353a96b3..df2c3894198df 100644 --- a/src/vs/platform/theme/common/colorRegistry.ts +++ b/src/vs/platform/theme/common/colorRegistry.ts @@ -260,6 +260,8 @@ export const editorHintBorder = registerColor('editorHint.border', { dark: null, export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: null, light: null, hc: null }, nls.localize('sashActiveBorder', "Border color of active sashes.")); +export const editorInlineHintForeground = registerColor('editorInlineHint.foreground', { dark: '#A7A6A5', light: '#A7A6A5', hc: null }, nls.localize('editorInlineHintForeground', 'Foreground color of inline hints')); +export const editorInlineHintBackground = registerColor('editorInlineHint.background', { dark: '#3A3A3A', light: '#3A3A3A', hc: null }, nls.localize('editorInlineHintBackground', 'Background color of inline hints')); /** * Editor background color. * Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254 diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 0432e5972a20f..6b1d597fcfc5d 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1945,6 +1945,81 @@ declare module 'vscode' { //#endregion + //@region https://github.com/microsoft/vscode/issues/16221 + + export namespace languages { + /** + * Register a inline hints provider. + * + * Multiple providers can be registered for a language. In that case providers are sorted + * by their [score](#languages.match) and the best-matching provider is used. Failure + * of the selected provider will cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An on type inline hints provider. + * @return A [disposable](#Disposable) that unregisters this provider when being disposed. + */ + export function registerInlineHintsProvider(selector: DocumentSelector, provider: InlineHintsProvider): Disposable; + } + + /** + * Inline hint information. + */ + export class InlineHint { + /** + * The text of the hint. + */ + text: string; + /** + * The range of the hint. + */ + range: Range; + /** + * Tooltip when hover on the hint. + */ + hoverMessage?: string; + /** + * Whitespace before the hint. + */ + whitespaceBefore?: boolean; + /** + * Whitespace after the hint. + */ + whitespaceAfter?: boolean; + + /** + * Creates a new inline hint information object. + * + * @param text The text of the hint. + * @param range The range of the hint. + * @param hoverMessage Tooltip when hover on the hint. + * @param whitespaceBefore Whitespace before the hint. + * @param whitespaceAfter TWhitespace after the hint. + */ + constructor(text: string, range: Range, hoverMessage?: string, whitespaceBefore?: boolean, whitespaceAfter?: boolean); + } + + /** + * The document formatting provider interface defines the contract between extensions and + * the inline hints feature. + */ + export interface InlineHintsProvider { + + /** + * An optional event to signal that inline hints have changed. + * @see [EventEmitter](#EventEmitter) + */ + onDidChangeInlineHints?: Event; + /** + * @param model The document in which the command was invoked. + * @param token A cancellation token. + * + * @return A list of arguments labels or a thenable that resolves to such. + */ + provideInlineHints(model: TextDocument, range: Range, token: CancellationToken): ProviderResult; + } + //#endregion + //#region https://github.com/microsoft/vscode/issues/104436 export enum ExtensionRuntime { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 04fbbf44f9d57..c0f64b5e8d574 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -497,6 +497,32 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha })); } + // --- inline hints + + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void { + const provider = { + provideInlineHints: async (model: ITextModel, range: EditorRange, token: CancellationToken): Promise => { + const result = await this._proxy.$provideInlineHints(handle, model.uri, range, token); + return result?.hints; + } + }; + + if (typeof eventHandle === 'number') { + const emitter = new Emitter(); + this._registrations.set(eventHandle, emitter); + provider.onDidChangeInlineHints = emitter.event; + } + + this._registrations.set(handle, modes.InlineHintsProviderRegistry.register(selector, provider)); + } + + $emitInlineHintsEvent(eventHandle: number, event?: any): void { + const obj = this._registrations.get(eventHandle); + if (obj instanceof Emitter) { + obj.fire(event); + } + } + // --- links $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 8f271b937f961..5e90f0ae27ba0 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -461,6 +461,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I getTokenInformationAtPosition(doc: vscode.TextDocument, pos: vscode.Position) { checkProposedApiEnabled(extension); return extHostLanguages.tokenAtPosition(doc, pos); + }, + registerInlineHintsProvider(selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerInlineHintsProvider(extension, selector, provider); } }; @@ -1143,6 +1147,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FunctionBreakpoint: extHostTypes.FunctionBreakpoint, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, + InlineHint: extHostTypes.InlineHint, Location: extHostTypes.Location, MarkdownString: extHostTypes.MarkdownString, OverviewRulerLane: OverviewRulerLane, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 4320674124ce7..5da7b6f3761d3 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -402,6 +402,8 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; + $registerInlineHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; + $emitInlineHintsEvent(eventHandle: number, event?: any): void; $registerDocumentLinkProvider(handle: number, selector: IDocumentFilterDto[], supportsResolve: boolean): void; $registerDocumentColorProvider(handle: number, selector: IDocumentFilterDto[]): void; $registerFoldingRangeProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; @@ -1293,6 +1295,18 @@ export interface ISignatureHelpContextDto { readonly activeSignatureHelp?: ISignatureHelpDto; } +export interface IInlineHintDto { + text: string; + range: IRange; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; +} + +export interface IInlineHintsDto { + hints: IInlineHintDto[] +} + export interface ILocationDto { uri: UriComponents; range: IRange; @@ -1482,6 +1496,7 @@ export interface ExtHostLanguageFeaturesShape { $releaseCompletionItems(handle: number, id: number): void; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise $provideDocumentLinks(handle: number, resource: UriComponents, token: CancellationToken): Promise; $resolveDocumentLink(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseDocumentLinks(handle: number, id: number): void; diff --git a/src/vs/workbench/api/common/extHostApiCommands.ts b/src/vs/workbench/api/common/extHostApiCommands.ts index 54450f84b4c71..3808011474bf6 100644 --- a/src/vs/workbench/api/common/extHostApiCommands.ts +++ b/src/vs/workbench/api/common/extHostApiCommands.ts @@ -325,6 +325,14 @@ const newCommands: ApiCommand[] = [ return []; }) ), + // --- inline hints + new ApiCommand( + 'vscode.executeInlineHintProvider', '_executeInlineHintProvider', 'Execute inline hints provider', + [ApiCommandArgument.Uri, ApiCommandArgument.Range], + new ApiCommandResult('A promise that resolves to an array of InlineHint objects', result => { + return result.map(typeConverters.InlineHint.to); + }) + ), // --- notebooks new ApiCommand( 'vscode.resolveNotebookContentProviders', '_resolveNotebookContentProvider', 'Resolve Notebook Content Providers', diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 14a039a7dfc23..c09066055e64c 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1063,6 +1063,20 @@ class SignatureHelpAdapter { } } +class InlineHintsAdapter { + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.InlineHintsProvider, + ) { } + + provideInlineHints(resource: URI, range: IRange, token: CancellationToken): Promise { + const doc = this._documents.getDocument(resource); + return asPromise(() => this._provider.provideInlineHints(doc, typeConvert.Range.to(range), token)).then(value => { + return value ? { hints: value.map(typeConvert.InlineHint.from) } : undefined; + }); + } +} + class LinkProviderAdapter { private _cache = new Cache('DocumentLink'); @@ -1321,7 +1335,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | SuggestAdapter | SignatureHelpAdapter | LinkProviderAdapter | ImplementationAdapter | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter - | LinkedEditingRangeAdapter; + | LinkedEditingRangeAdapter | InlineHintsAdapter; class AdapterData { constructor( @@ -1774,6 +1788,27 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SignatureHelpAdapter, adapter => adapter.releaseSignatureHelp(id), undefined); } + // --- inline hints + + registerInlineHintsProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.InlineHintsProvider): vscode.Disposable { + + const eventHandle = typeof provider.onDidChangeInlineHints === 'function' ? this._nextHandle() : undefined; + const handle = this._addNewAdapter(new InlineHintsAdapter(this._documents, provider), extension); + + this._proxy.$registerInlineHintsProvider(handle, this._transformDocumentSelector(selector), eventHandle); + let result = this._createDisposable(handle); + + if (eventHandle !== undefined) { + const subscription = provider.onDidChangeInlineHints!(_ => this._proxy.$emitInlineHintsEvent(eventHandle)); + result = Disposable.from(result, subscription); + } + return result; + } + + $provideInlineHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineHintsAdapter, adapter => adapter.provideInlineHints(URI.revive(resource), range, token), undefined); + } + // --- links registerDocumentLinkProvider(extension: IExtensionDescription | undefined, selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index ee4a7197a3cb6..345f37fbba2ed 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -1015,6 +1015,29 @@ export namespace SignatureHelp { } } +export namespace InlineHint { + + export function from(hint: vscode.InlineHint): modes.InlineHint { + return { + text: hint.text, + range: Range.from(hint.range), + hoverMessage: hint.hoverMessage, + whitespaceBefore: hint.whitespaceBefore, + whitespaceAfter: hint.whitespaceAfter + }; + } + + export function to(hint: modes.InlineHint): vscode.InlineHint { + return { + text: hint.text, + range: Range.to(hint.range), + hoverMessage: hint.hoverMessage, + whitespaceBefore: hint.whitespaceBefore, + whitespaceAfter: hint.whitespaceAfter + }; + } +} + export namespace DocumentLink { export function from(link: vscode.DocumentLink): modes.ILink { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 9513d3047fffb..dbcc402fd6139 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1373,6 +1373,23 @@ export enum SignatureHelpTriggerKind { ContentChange = 3, } +@es5ClassCompat +export class InlineHint { + text: string; + range: Range; + hoverMessage?: string; + whitespaceBefore?: boolean; + whitespaceAfter?: boolean; + + constructor(text: string, range: Range, hoverMessage?: string, whitespaceBefore?: boolean, whitespaceAfter?: boolean) { + this.text = text; + this.range = range; + this.hoverMessage = hoverMessage; + this.whitespaceBefore = whitespaceBefore; + this.whitespaceAfter = whitespaceAfter; + } +} + export enum CompletionTriggerKind { Invoke = 0, TriggerCharacter = 1, diff --git a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts index 47db11d1a7b70..0cc30aa021ed9 100644 --- a/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts +++ b/src/vs/workbench/test/browser/api/extHostApiCommands.test.ts @@ -35,6 +35,7 @@ import { NullApiDeprecationService } from 'vs/workbench/api/common/extHostApiDep import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; +import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; import 'vs/editor/contrib/codeAction/codeAction'; import 'vs/editor/contrib/codelens/codelens'; @@ -48,7 +49,7 @@ import 'vs/editor/contrib/parameterHints/provideSignatureHelp'; import 'vs/editor/contrib/smartSelect/smartSelect'; import 'vs/editor/contrib/suggest/suggest'; import 'vs/editor/contrib/rename/rename'; -import { IResolvedTextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import 'vs/editor/contrib/inlineHints/inlineHintsController'; const defaultSelector = { scheme: 'far' }; const model: ITextModel = createTextModel( @@ -1141,6 +1142,85 @@ suite('ExtHostLanguageFeatureCommands', function () { }); }); + // --- inline hints + + test('Inline Hints, back and forth', async function () { + disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlineHints() { + return [new types.InlineHint('Foo', new types.Range(0, 1, 2, 3), undefined, true, false)]; + } + })); + + await rpcProtocol.sync(); + + const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + assert.strictEqual(value.length, 1); + + const [first] = value; + assert.strictEqual(first.text, 'Foo'); + assert.strictEqual(first.range.start.line, 0); + assert.strictEqual(first.range.start.character, 1); + assert.strictEqual(first.range.end.line, 2); + assert.strictEqual(first.range.end.character, 3); + }); + + test('Inline Hints, merge', async function () { + disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlineHints() { + return [new types.InlineHint('Bar', new types.Range(10, 11, 12, 13), undefined, true, false)]; + } + })); + + disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlineHints() { + return [new types.InlineHint('Foo', new types.Range(0, 1, 2, 3), undefined, true, false)]; + } + })); + + await rpcProtocol.sync(); + + const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + assert.strictEqual(value.length, 2); + + const [first, second] = value; + assert.strictEqual(first.text, 'Foo'); + assert.strictEqual(first.range.start.line, 0); + assert.strictEqual(first.range.start.character, 1); + assert.strictEqual(first.range.end.line, 2); + assert.strictEqual(first.range.end.character, 3); + + assert.strictEqual(second.text, 'Bar'); + assert.strictEqual(second.range.start.line, 10); + assert.strictEqual(second.range.start.character, 11); + assert.strictEqual(second.range.end.line, 12); + assert.strictEqual(second.range.end.character, 13); + }); + + test('Inline Hints, bad provider', async function () { + disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlineHints() { + return [new types.InlineHint('Foo', new types.Range(0, 1, 2, 3), undefined, true, false)]; + } + })); + disposables.push(extHost.registerInlineHintsProvider(nullExtensionDescription, defaultSelector, { + provideInlineHints() { + throw new Error(); + } + })); + + await rpcProtocol.sync(); + + const value = await commands.executeCommand('vscode.executeInlineHintProvider', model.uri, new types.Range(0, 0, 20, 20)); + assert.strictEqual(value.length, 1); + + const [first] = value; + assert.strictEqual(first.text, 'Foo'); + assert.strictEqual(first.range.start.line, 0); + assert.strictEqual(first.range.start.character, 1); + assert.strictEqual(first.range.end.line, 2); + assert.strictEqual(first.range.end.character, 3); + }); + // --- selection ranges test('Selection Range, back and forth', async function () {