diff --git a/src/tsec.exemptions.json b/src/tsec.exemptions.json index 2f123f56f0e..4da4fe78745 100644 --- a/src/tsec.exemptions.json +++ b/src/tsec.exemptions.json @@ -20,6 +20,7 @@ "vs/editor/browser/view/domLineBreaksComputer.ts", "vs/editor/browser/view/viewLayer.ts", "vs/editor/browser/widget/diffEditorWidget.ts", + "vs/editor/contrib/ghostText/ghostTextWidget.ts", "vs/editor/browser/widget/diffReview.ts", "vs/editor/standalone/browser/colorizer.ts", "vs/workbench/api/worker/extHostExtensionService.ts", diff --git a/src/vs/code/electron-browser/workbench/workbench.html b/src/vs/code/electron-browser/workbench/workbench.html index 2933be27364..bd37436f1fd 100644 --- a/src/vs/code/electron-browser/workbench/workbench.html +++ b/src/vs/code/electron-browser/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/src/vs/code/electron-sandbox/workbench/workbench.html b/src/vs/code/electron-sandbox/workbench/workbench.html index 2933be27364..bd37436f1fd 100644 --- a/src/vs/code/electron-sandbox/workbench/workbench.html +++ b/src/vs/code/electron-sandbox/workbench/workbench.html @@ -4,7 +4,7 @@ - + diff --git a/src/vs/editor/browser/widget/diffReview.ts b/src/vs/editor/browser/widget/diffReview.ts index 8a568c82136..3d27ade5911 100644 --- a/src/vs/editor/browser/widget/diffReview.ts +++ b/src/vs/editor/browser/widget/diffReview.ts @@ -22,7 +22,6 @@ import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { Position } from 'vs/editor/common/core/position'; import { ILineChange, ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel, TextModelResolvedOptions } from 'vs/editor/common/model'; -import { ColorId, FontStyle, MetadataConsts } from 'vs/editor/common/modes'; import { editorLineNumbers } from 'vs/editor/common/view/editorColorRegistry'; import { RenderLineInput, renderViewLine2 as renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; import { ViewLineRenderingData } from 'vs/editor/common/viewModel/viewModel'; @@ -777,19 +776,7 @@ export class DiffReview extends Disposable { private static _renderLine(model: ITextModel, options: IComputedEditorOptions, tabSize: number, lineNumber: number): string { const lineContent = model.getLineContent(lineNumber); const fontInfo = options.get(EditorOption.fontInfo); - - const defaultMetadata = ( - (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) - | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) - | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) - ) >>> 0; - - const tokens = new Uint32Array(2); - tokens[0] = lineContent.length; - tokens[1] = defaultMetadata; - - const lineTokens = new LineTokens(tokens, lineContent); - + const lineTokens = LineTokens.createEmpty(lineContent); const isBasicASCII = ViewLineRenderingData.isBasicASCII(lineContent, model.mightContainNonBasicASCII()); const containsRTL = ViewLineRenderingData.containsRTL(lineContent, isBasicASCII, model.mightContainRTL()); const r = renderViewLine(new RenderLineInput( diff --git a/src/vs/editor/common/core/lineTokens.ts b/src/vs/editor/common/core/lineTokens.ts index 51aa6a93ca8..66d9e367817 100644 --- a/src/vs/editor/common/core/lineTokens.ts +++ b/src/vs/editor/common/core/lineTokens.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ColorId, LanguageId, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; +import { ColorId, FontStyle, LanguageId, MetadataConsts, StandardTokenType, TokenMetadata } from 'vs/editor/common/modes'; export interface IViewLineTokens { equals(other: IViewLineTokens): boolean; @@ -22,6 +22,20 @@ export class LineTokens implements IViewLineTokens { private readonly _tokensCount: number; private readonly _text: string; + public static createEmpty(lineContent: string): LineTokens { + const defaultMetadata = ( + (FontStyle.None << MetadataConsts.FONT_STYLE_OFFSET) + | (ColorId.DefaultForeground << MetadataConsts.FOREGROUND_OFFSET) + | (ColorId.DefaultBackground << MetadataConsts.BACKGROUND_OFFSET) + ) >>> 0; + + const tokens = new Uint32Array(2); + tokens[0] = lineContent.length; + tokens[1] = defaultMetadata; + + return new LineTokens(tokens, lineContent); + } + constructor(tokens: Uint32Array, text: string) { this._tokens = tokens; this._tokensCount = (this._tokens.length >>> 1); diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 9167945d9e9..e331f82272a 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -685,6 +685,21 @@ export interface CompletionItemProvider { resolveCompletionItem?(item: CompletionItem, token: CancellationToken): ProviderResult; } +/** + * @internal + */ +export interface GhostText { + text: string; + replaceRange?: IRange; +} + +/** + * @internal + */ +export interface GhostTextProvider { + provideGhostText(model: model.ITextModel, position: Position, token: CancellationToken): ProviderResult; +} + export interface CodeAction { title: string; command?: Command; @@ -1770,6 +1785,11 @@ export const RenameProviderRegistry = new LanguageFeatureRegistry(); +/** + * @internal + */ +export const GhostTextProviderRegistry = new LanguageFeatureRegistry(); + /** * @internal */ diff --git a/src/vs/editor/contrib/ghostText/ghostText.ts b/src/vs/editor/contrib/ghostText/ghostText.ts new file mode 100644 index 00000000000..94ee9aa94fb --- /dev/null +++ b/src/vs/editor/contrib/ghostText/ghostText.ts @@ -0,0 +1,155 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nls from 'vs/nls'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { Disposable, DisposableStore, toDisposable } from 'vs/base/common/lifecycle'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { EditorAction, registerEditorAction, registerEditorContribution, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; +import { ITextModel } from 'vs/editor/common/model'; +import { GhostTextProviderRegistry } from 'vs/editor/common/modes'; +import { Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async'; +import * as errors from 'vs/base/common/errors'; +import { GhostTextWidget, ValidGhostText } from 'vs/editor/contrib/ghostText/ghostTextWidget'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; + +class GhostTextController extends Disposable { + static ID = 'editor.contrib.ghostTextController'; + + public static get(editor: ICodeEditor): GhostTextController { + return editor.getContribution(GhostTextController.ID); + } + + private readonly _editor: ICodeEditor; + private readonly _widget: GhostTextWidget; + private _modelDisposable: DisposableStore; + private _ghostTextPromise: CancelablePromise | null; + + constructor( + editor: ICodeEditor, + @IInstantiationService instantiationService: IInstantiationService, + ) { + super(); + this._editor = editor; + this._widget = this._register(instantiationService.createInstance(GhostTextWidget, this._editor)); + this._modelDisposable = this._register(new DisposableStore()); + this._ghostTextPromise = null; + this._register(toDisposable(() => { + if (this._ghostTextPromise) { + this._ghostTextPromise.cancel(); + this._ghostTextPromise = null; + } + })); + + this._editor.onDidChangeModel(() => { + this._reinit(); + }); + GhostTextProviderRegistry.onDidChange(() => { + this._reinit(); + }); + this._reinit(); + + } + + private _reinit(): void { + this._modelDisposable.clear(); + if (!this._editor.hasModel()) { + return; + } + const model = this._editor.getModel(); + + this._modelDisposable.add(model.onDidChangeContent((e) => { + if (!GhostTextProviderRegistry.has(model)) { + return; + } + + this._trigger(); + })); + this._modelDisposable.add(toDisposable(() => { + this._widget.hide(); + })); + } + + private _trigger(): void { + if (!this._editor.hasModel()) { + return; + } + + const model = this._editor.getModel(); + const position = this._editor.getPosition(); + + if (this._ghostTextPromise) { + this._ghostTextPromise.cancel(); + this._ghostTextPromise = null; + } + + this._ghostTextPromise = createCancelablePromise(token => provideGhostText(model, position, token)); + + this._ghostTextPromise.then((result) => this._renderResult(result), errors.onUnexpectedError); + } + + private _renderResult(result: ValidGhostText | undefined): void { + if (!result) { + this._widget.hide(); + return; + } + this._widget.show(result); + } + + public trigger(): void { + this._trigger(); + } +} + +export class TriggerGhostTextAction extends EditorAction { + + constructor() { + super({ + id: 'editor.action.triggerGhostText', + label: nls.localize('triggerGhostTextAction', "Trigger Ghost Text"), + alias: 'Trigger Ghost Text', + precondition: EditorContextKeys.writable + }); + } + + public async run(accessor: ServicesAccessor | null, editor: ICodeEditor): Promise { + const controller = GhostTextController.get(editor); + if (controller) { + controller.trigger(); + } + } +} + +async function provideGhostText( + model: ITextModel, + position: Position, + token: CancellationToken = CancellationToken.None +): Promise { + + const word = model.getWordAtPosition(position); + const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position); + + const providers = GhostTextProviderRegistry.all(model); + const results = await Promise.all( + providers.map(provider => provider.provideGhostText(model, position, token)) + ); + + for (const result of results) { + if (result) { + return { + text: result.text, + replaceRange: result.replaceRange || defaultReplaceRange + }; + } + } + + return undefined; +} + +registerEditorContribution(GhostTextController.ID, GhostTextController); +registerEditorAction(TriggerGhostTextAction); diff --git a/src/vs/editor/contrib/ghostText/ghostTextWidget.ts b/src/vs/editor/contrib/ghostText/ghostTextWidget.ts new file mode 100644 index 00000000000..21888a424c5 --- /dev/null +++ b/src/vs/editor/contrib/ghostText/ghostTextWidget.ts @@ -0,0 +1,188 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, toDisposable } from 'vs/base/common/lifecycle'; +import { IRange, Range } from 'vs/editor/common/core/range'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { GhostText } from 'vs/editor/common/modes'; +import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import * as strings from 'vs/base/common/strings'; +import { RenderLineInput, renderViewLine } from 'vs/editor/common/viewLayout/viewLineRenderer'; +import { EditorFontLigatures, EditorOption } from 'vs/editor/common/config/editorOptions'; +import { createStringBuilder } from 'vs/editor/common/core/stringBuilder'; +import { Configuration } from 'vs/editor/browser/config/configuration'; +import { LineTokens } from 'vs/editor/common/core/lineTokens'; + +export interface ValidGhostText extends GhostText { + replaceRange: IRange; +} + +const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value }); + +// TODO: +// - interaction with other text decorations +export class GhostTextWidget extends Disposable { + + private static instanceCount = 0; + + private readonly _editor: ICodeEditor; + private readonly _codeEditorDecorationTypeKey: string; + + private _currentGhostText: ValidGhostText | null; + private _hasDecoration: boolean; + private _decorationIds: string[]; + private _viewZoneId: string | null; + + constructor( + editor: ICodeEditor, + @ICodeEditorService private readonly _codeEditorService: ICodeEditorService + ) { + super(); + this._editor = editor; + this._codeEditorDecorationTypeKey = `ghost-text-${++GhostTextWidget.instanceCount}`; + this._currentGhostText = null; + this._hasDecoration = false; + this._decorationIds = []; + this._viewZoneId = null; + + this._register(this._editor.onDidChangeConfiguration((e) => { + if ( + e.hasChanged(EditorOption.disableMonospaceOptimizations) + || e.hasChanged(EditorOption.stopRenderingLineAfter) + || e.hasChanged(EditorOption.renderWhitespace) + || e.hasChanged(EditorOption.renderControlCharacters) + || e.hasChanged(EditorOption.fontLigatures) + || e.hasChanged(EditorOption.fontInfo) + || e.hasChanged(EditorOption.lineHeight) + ) { + this._render(); + } + })); + this._register(toDisposable(() => this._removeInlineText())); + } + + private _removeInlineText(): void { + if (this._hasDecoration) { + this._hasDecoration = false; + this._codeEditorService.removeDecorationType(this._codeEditorDecorationTypeKey); + } + } + + public hide(): void { + this._removeInlineText(); + this._editor.changeViewZones((changeAccessor) => { + if (this._viewZoneId) { + changeAccessor.removeZone(this._viewZoneId); + this._viewZoneId = null; + } + }); + this._currentGhostText = null; + } + + public show(ghostText: ValidGhostText): void { + if (!this._editor.hasModel()) { + return; + } + this._currentGhostText = ghostText; + this._render(); + } + + private _render(): void { + if (!this._editor.hasModel() || !this._currentGhostText) { + return; + } + + const model = this._editor.getModel(); + const { tabSize } = model.getOptions(); + const ghostLines = strings.splitLines(this._currentGhostText.text); + + this._removeInlineText(); + + this._codeEditorService.registerDecorationType(this._codeEditorDecorationTypeKey, { + after: { + contentText: ghostLines[0] + } + }); + this._hasDecoration = true; + const insertPosition = Range.lift(this._currentGhostText.replaceRange).getEndPosition(); + this._decorationIds = model.deltaDecorations(this._decorationIds, [{ + range: Range.fromPositions(insertPosition, insertPosition), + options: this._codeEditorService.resolveDecorationOptions(this._codeEditorDecorationTypeKey, true) + }]); + + this._editor.changeViewZones((changeAccessor) => { + if (this._viewZoneId) { + changeAccessor.removeZone(this._viewZoneId); + this._viewZoneId = null; + } + const remainingLines = ghostLines.slice(1); + if (remainingLines.length > 0) { + const domNode = document.createElement('div'); + this._renderLines(domNode, tabSize, remainingLines); + + this._viewZoneId = changeAccessor.addZone({ + afterLineNumber: insertPosition.lineNumber, + afterColumn: insertPosition.column, + heightInLines: ghostLines.length - 1, + domNode, + }); + } + }); + } + + private _renderLines(domNode: HTMLElement, tabSize: number, lines: string[]): void { + const opts = this._editor.getOptions(); + const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations); + const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter); + const renderWhitespace = opts.get(EditorOption.renderWhitespace); + const renderControlCharacters = opts.get(EditorOption.renderControlCharacters); + const fontLigatures = opts.get(EditorOption.fontLigatures); + const fontInfo = opts.get(EditorOption.fontInfo); + const lineHeight = opts.get(EditorOption.lineHeight); + + const sb = createStringBuilder(10000); + + for (let i = 0, len = lines.length; i < len; i++) { + const line = lines[i]; + sb.appendASCIIString('
'); + + const isBasicASCII = strings.isBasicASCII(line); + const containsRTL = strings.containsRTL(line); + const lineTokens = LineTokens.createEmpty(line); + + renderViewLine(new RenderLineInput( + (fontInfo.isMonospace && !disableMonospaceOptimizations), + fontInfo.canUseHalfwidthRightwardsArrow, + line, + false, + isBasicASCII, + containsRTL, + 0, + lineTokens, + [], + tabSize, + 0, + fontInfo.spaceWidth, + fontInfo.middotWidth, + fontInfo.wsmiddotWidth, + stopRenderingLineAfter, + renderWhitespace, + renderControlCharacters, + fontLigatures !== EditorFontLigatures.OFF, + null + ), sb); + + sb.appendASCIIString('
'); + } + + Configuration.applyFontInfoSlow(domNode, fontInfo); + const html = sb.build(); + const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html; + domNode.innerHTML = trustedhtml as string; + } +} diff --git a/src/vs/editor/editor.all.ts b/src/vs/editor/editor.all.ts index 16bb5b3fe7e..e0d961e6778 100644 --- a/src/vs/editor/editor.all.ts +++ b/src/vs/editor/editor.all.ts @@ -24,6 +24,7 @@ import 'vs/editor/contrib/folding/folding'; import 'vs/editor/contrib/fontZoom/fontZoom'; import 'vs/editor/contrib/format/formatActions'; import 'vs/editor/contrib/documentSymbols/documentSymbols'; +import 'vs/editor/contrib/ghostText/ghostText'; import 'vs/editor/contrib/gotoSymbol/goToCommands'; import 'vs/editor/contrib/gotoSymbol/link/goToDefinitionAtPosition'; import 'vs/editor/contrib/gotoError/gotoError'; diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 64675d0840f..b369c180da1 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -3281,6 +3281,27 @@ declare module 'vscode' { //#endregion + //#region https://github.com/microsoft/vscode/issues/124024 @hediet @alexdima + + export class GhostText { + + text: string; + + replaceRange?: Range; + + constructor(text: string); + } + + export interface GhostTextProvider { + provideGhostTextItems(document: TextDocument, position: Position, token: CancellationToken): ProviderResult; + } + + export namespace languages { + export function registerGhostTextProvider(selector: DocumentSelector, provider: GhostTextProvider): Disposable; + } + + //#endregion + //#region FileSystemProvider stat readonly - https://github.com/microsoft/vscode/issues/73122 export enum FilePermission { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index f65d89673ce..3c3c8704112 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -499,6 +499,15 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha this._registrations.set(handle, modes.CompletionProviderRegistry.register(selector, provider)); } + $registerGhostTextSupport(handle: number, selector: IDocumentFilterDto[]): void { + const provider: modes.GhostTextProvider = { + provideGhostText: async (model: ITextModel, position: EditorPosition, token: CancellationToken): Promise => { + return this._proxy.$provideGhostText(handle, model.uri, position, token); + } + }; + this._registrations.set(handle, modes.GhostTextProviderRegistry.register(selector, provider)); + } + // --- parameter hints $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e0f69afce4c..68a9476384c 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -480,6 +480,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerCompletionItemProvider(selector: vscode.DocumentSelector, provider: vscode.CompletionItemProvider, ...triggerCharacters: string[]): vscode.Disposable { return extHostLanguageFeatures.registerCompletionItemProvider(extension, checkSelector(selector), provider, triggerCharacters); }, + registerGhostTextProvider(selector: vscode.DocumentSelector, provider: vscode.GhostTextProvider): vscode.Disposable { + checkProposedApiEnabled(extension); + return extHostLanguageFeatures.registerGhostTextProvider(extension, checkSelector(selector), provider); + }, registerDocumentLinkProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentLinkProvider): vscode.Disposable { return extHostLanguageFeatures.registerDocumentLinkProvider(extension, checkSelector(selector), provider); }, @@ -1177,6 +1181,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I FoldingRange: extHostTypes.FoldingRange, FoldingRangeKind: extHostTypes.FoldingRangeKind, FunctionBreakpoint: extHostTypes.FunctionBreakpoint, + GhostText: extHostTypes.GhostText, Hover: extHostTypes.Hover, IndentAction: languageConfiguration.IndentAction, Location: extHostTypes.Location, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 42b5c7f9e95..ce25add0fec 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -395,6 +395,7 @@ export interface MainThreadLanguageFeaturesShape extends IDisposable { $emitDocumentSemanticTokensEvent(eventHandle: number): void; $registerDocumentRangeSemanticTokensProvider(handle: number, selector: IDocumentFilterDto[], legend: modes.SemanticTokensLegend): void; $registerSuggestSupport(handle: number, selector: IDocumentFilterDto[], triggerCharacters: string[], supportsResolveDetails: boolean, displayName: string): void; + $registerGhostTextSupport(handle: number, selector: IDocumentFilterDto[]): void; $registerSignatureHelpProvider(handle: number, selector: IDocumentFilterDto[], metadata: ISignatureHelpProviderMetadataDto): void; $registerInlayHintsProvider(handle: number, selector: IDocumentFilterDto[], eventHandle: number | undefined): void; $emitInlayHintsEvent(eventHandle: number, event?: any): void; @@ -1634,6 +1635,7 @@ export interface ExtHostLanguageFeaturesShape { $provideCompletionItems(handle: number, resource: UriComponents, position: IPosition, context: modes.CompletionContext, token: CancellationToken): Promise; $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; + $provideGhostText(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise; $provideSignatureHelp(handle: number, resource: UriComponents, position: IPosition, context: modes.SignatureHelpContext, token: CancellationToken): Promise; $releaseSignatureHelp(handle: number, id: number): void; $provideInlayHints(handle: number, resource: UriComponents, range: IRange, token: CancellationToken): Promise diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index eb5d247de8b..34bea6d81de 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1038,6 +1038,41 @@ class SuggestAdapter { } } +class GhostTextAdapter { + + constructor( + private readonly _documents: ExtHostDocuments, + private readonly _provider: vscode.GhostTextProvider, + ) { } + + async provideGhostText(resource: URI, position: IPosition, token: CancellationToken): Promise { + + const doc = this._documents.getDocument(resource); + const pos = typeConvert.Position.to(position); + + // The default insert/replace ranges. + const defaultReplaceRange = doc.getWordRangeAtPosition(pos) || new Range(pos, pos); + + const result = await asPromise(() => this._provider.provideGhostTextItems(doc, pos, token)); + + if (!result) { + // undefined and null are valid results + return undefined; + } + + if (token.isCancellationRequested) { + // cancelled -> return without further ado, esp no caching + // of results as they will leak + return undefined; + } + + return { + text: result.text, + replaceRange: typeConvert.Range.from(result.replaceRange || defaultReplaceRange) + }; + } +} + class SignatureHelpAdapter { private readonly _cache = new Cache('SignatureHelp'); @@ -1355,7 +1390,7 @@ type Adapter = DocumentSymbolAdapter | CodeLensAdapter | DefinitionAdapter | Hov | TypeDefinitionAdapter | ColorProviderAdapter | FoldingProviderAdapter | DeclarationAdapter | SelectionRangeAdapter | CallHierarchyAdapter | DocumentSemanticTokensAdapter | DocumentRangeSemanticTokensAdapter | EvaluatableExpressionAdapter | InlineValuesAdapter - | LinkedEditingRangeAdapter | InlayHintsAdapter; + | LinkedEditingRangeAdapter | InlayHintsAdapter | GhostTextAdapter; class AdapterData { constructor( @@ -1809,6 +1844,19 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF this._withAdapter(handle, SuggestAdapter, adapter => adapter.releaseCompletionItems(id), undefined); } + // --- ghost test + + registerGhostTextProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.GhostTextProvider): vscode.Disposable { + const handle = this._addNewAdapter(new GhostTextAdapter(this._documents, provider), extension); + this._proxy.$registerGhostTextSupport(handle, this._transformDocumentSelector(selector)); + return this._createDisposable(handle); + } + + $provideGhostText(handle: number, resource: UriComponents, position: IPosition, token: CancellationToken): Promise { + return this._withAdapter(handle, GhostTextAdapter, adapter => adapter.provideGhostText(URI.revive(resource), position, token), undefined); + } + + // --- parameter hints registerSignatureHelpProvider(extension: IExtensionDescription, selector: vscode.DocumentSelector, provider: vscode.SignatureHelpProvider, metadataOrTriggerChars: string[] | vscode.SignatureHelpProviderMetadata): vscode.Disposable { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 0cd309dbf10..352b41a4efa 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -1547,6 +1547,17 @@ export class CompletionList { } } +@es5ClassCompat +export class GhostText implements vscode.GhostText { + + text: string; + replaceRange?: Range; + + constructor(text: string) { + this.text = text; + } +} + export enum ViewColumn { Active = -1, Beside = -2,