Adds support for snippets in inline completions (fixes #138260)

This commit is contained in:
Henning Dieterichs
2022-03-02 16:02:51 +01:00
parent c468903ff7
commit 5f8dacf8a2
11 changed files with 84 additions and 18 deletions
+4 -1
View File
@@ -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.
@@ -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 {
@@ -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;
@@ -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,
};
}
@@ -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
};
}
@@ -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,
}
};
}
@@ -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();
@@ -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(),
+6 -1
View File
@@ -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.
@@ -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,
+1 -1
View File
@@ -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.