diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index c43adc0abc3..c838a2d6f0e 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -786,8 +786,11 @@ export interface InlineCompletion { * The text to insert. * If the text contains a line break, the range must end at the end of a line. * If existing text should be replaced, the existing text must be a prefix of the text to insert. + * + * The text can also be a snippet. In that case, a preview with default parameters is shown. + * When accepting the suggestion, the full snippet is inserted. */ - readonly text: string; + readonly text: string | { snippet: string }; /** * The range to replace. diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts index 56370689ed5..9e079b1b0d5 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostText.ts @@ -56,6 +56,10 @@ export class GhostText { return text.substring(this.parts[0].column - 1); } + + isEmpty(): boolean { + return this.parts.every(p => p.lines.length === 0); + } } class PositionOffsetTransformer { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextController.ts b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextController.ts index 564c37033d2..99e97646cf8 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/ghostTextController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/ghostTextController.ts @@ -156,6 +156,7 @@ export class ActiveGhostTextController extends Disposable { private updateContextKeys(): void { this.contextKeys.inlineCompletionVisible.set( this.model.activeInlineCompletionsModel?.ghostText !== undefined + && !this.model.activeInlineCompletionsModel.ghostText.isEmpty() ); let startsWithIndentation = false; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts index 2fab45c0ed6..7edf0d8480a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionToGhostText.ts @@ -15,7 +15,16 @@ import { GhostText, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/br * A normalized inline completion is an inline completion with a defined range. */ export interface NormalizedInlineCompletion extends InlineCompletion { - range: Range; + readonly range: Range; + readonly text: string; + + readonly snippetInfo: + | { + snippet: string; + /* Could be different than the main range */ + range: Range; + } + | undefined; } export function normalizedInlineCompletionsEquals(a: NormalizedInlineCompletion | undefined, b: NormalizedInlineCompletion | undefined): boolean { @@ -76,7 +85,8 @@ export function inlineCompletionToGhostText( inlineCompletion = { range: rangeThatDoesNotReplaceIndentation, text: suggestionWithoutIndentationChange, - command: inlineCompletion.command + command: inlineCompletion.command, + snippetInfo: undefined, }; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts index 9545d8a1a6e..6717d44d4e1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/inlineCompletionsModel.ts @@ -27,6 +27,9 @@ import { fixBracketsInLine } from 'vs/editor/common/model/bracketPairsTextModelP import { LanguageFeatureRegistry } from 'vs/editor/common/languageFeatureRegistry'; import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures'; import { IFeatureDebounceInformation, ILanguageFeatureDebounceService } from 'vs/editor/common/services/languageFeatureDebounce'; +import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; +import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; +import { assertNever } from 'vs/base/common/types'; export class InlineCompletionsModel extends Disposable @@ -416,7 +419,8 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { } public commitCurrentCompletion(): void { - if (!this.ghostText) { + const ghostText = this.ghostText; + if (!ghostText || ghostText.isEmpty()) { // No ghost text was shown for this completion. // Thus, we don't want to commit anything. return; @@ -432,12 +436,23 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel { // otherwise command args might get disposed. const cache = this.cache.clearAndLeak(); - this.editor.executeEdits( - 'inlineSuggestion.accept', - [ - EditOperation.replaceMove(completion.range, completion.text) - ] - ); + if (completion.snippetInfo) { + this.editor.executeEdits( + 'inlineSuggestion.accept', + [ + EditOperation.replaceMove(completion.range, '') + ] + ); + this.editor.setPosition(completion.snippetInfo.range.getStartPosition()); + SnippetController2.get(this.editor)?.insert(completion.snippetInfo.snippet); + } else { + this.editor.executeEdits( + 'inlineSuggestion.accept', + [ + EditOperation.replaceMove(completion.range, completion.text) + ] + ); + } if (completion.command) { this.commandService .executeCommand(completion.command.id, ...(completion.command.arguments || [])) @@ -543,6 +558,8 @@ class CachedInlineCompletion { sourceProvider: this.inlineCompletion.sourceProvider, sourceInlineCompletions: this.inlineCompletion.sourceInlineCompletions, sourceInlineCompletion: this.inlineCompletion.sourceInlineCompletion, + completeBracketPairs: this.inlineCompletion.completeBracketPairs, + snippetInfo: this.inlineCompletion.snippetInfo, }; } } @@ -623,8 +640,8 @@ export async function provideInlineCompletions( continue; } - const text = - languageConfigurationService && item.completeBracketPairs + const textOrSnippet = + languageConfigurationService && item.completeBracketPairs && typeof item.text === 'string' ? closeBrackets( item.text, range.getStartPosition(), @@ -633,8 +650,31 @@ export async function provideInlineCompletions( ) : item.text; + let text: string; + let snippetInfo: { + snippet: string; + /* Could be different than the main range */ + range: Range; + } + | undefined; + + if (typeof textOrSnippet === 'string') { + text = textOrSnippet; + snippetInfo = undefined; + } else if ('snippet' in textOrSnippet) { + const snippet = new SnippetParser().parse(textOrSnippet.snippet); + text = snippet.toString(); + snippetInfo = { + snippet: textOrSnippet.snippet, + range: range + }; + } else { + assertNever(textOrSnippet); + } + const trackedItem: TrackedInlineCompletion = ({ text, + snippetInfo, range, command: item.command, sourceProvider: result.provider, @@ -696,5 +736,6 @@ export function minimizeInlineCompletion(model: ITextModel, inlineCompletion: No return { range: Range.fromPositions(start, end), text: inlineCompletion.text.substr(commonPrefixLen, inlineCompletion.text.length - commonPrefixLen - commonSuffixLen), + snippetInfo: inlineCompletion.snippetInfo }; } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts index dffc56e313e..e23a79d0667 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/suggestWidgetInlineCompletionProvider.ts @@ -228,7 +228,8 @@ function suggestionToSuggestItemInfo(suggestController: SuggestController, posit normalizedInlineCompletion: { // Dummy element, so that space is reserved, but no text is shown range: Range.fromPositions(position, position), - text: '' + text: '', + snippetInfo: undefined, }, }; } @@ -260,6 +261,7 @@ function suggestionToSuggestItemInfo(suggestController: SuggestController, posit position.delta(0, -info.overwriteBefore), position.delta(0, Math.max(info.overwriteAfter, 0)) ), + snippetInfo: undefined, } }; } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 54337d83114..24f4d7a7a7b 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -35,7 +35,7 @@ suite('Inline Completions', () => { const options = ['prefix', 'subword'] as const; const result = {} as any; for (const option of options) { - result[option] = inlineCompletionToGhostText({ text: suggestion, range }, tempModel, option)?.render(cleanedText, true); + result[option] = inlineCompletionToGhostText({ text: suggestion, snippetInfo: undefined, range }, tempModel, option)?.render(cleanedText, true); } tempModel.dispose(); diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts index 7e03f58e981..0f18a2a3ae7 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/suggestWidgetModel.test.ts @@ -104,7 +104,7 @@ suite('Suggest Widget Model', () => { test('minimizeInlineCompletion', async () => { const model = createTextModel('fun'); - const result = minimizeInlineCompletion(model, { range: new Range(1, 1, 1, 4), text: 'function' })!; + const result = minimizeInlineCompletion(model, { range: new Range(1, 1, 1, 4), text: 'function', snippetInfo: undefined })!; assert.deepStrictEqual({ range: result.range.toString(), diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 6efc4c26a59..cadb1921827 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -6263,8 +6263,13 @@ declare namespace monaco.languages { * The text to insert. * If the text contains a line break, the range must end at the end of a line. * If existing text should be replaced, the existing text must be a prefix of the text to insert. + * + * The text can also be a snippet. In that case, a preview with default parameters is shown. + * When accepting the suggestion, the full snippet is inserted. */ - readonly text: string; + readonly text: string | { + snippet: string; + }; /** * The range to replace. * Must begin and end on the same line. diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 35dbba41237..59e2eba0ee7 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1083,7 +1083,7 @@ class InlineCompletionAdapter { throw new Error('text or insertText must be defined'); } return ({ - text: insertText, + text: typeof insertText === 'string' ? insertText : { snippet: insertText.value }, range: item.range ? typeConvert.Range.from(item.range) : undefined, command, idx: idx, diff --git a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts index 563cab4c4de..1073a3a3cc5 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletions.d.ts @@ -102,7 +102,7 @@ declare module 'vscode' { * Thus, ` B` can be replaced with ` ABC`, effectively removing a whitespace and inserting `A` and `C`. */ // TODO@API support vscode.SnippetString in addition to string, see CompletionItem#insertText - insertText?: string; + insertText?: string | SnippetString; /** * @deprecated Use `insertText` instead. Will be removed eventually.