mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-09 08:15:05 +01:00
Adds support for snippets in inline completions (fixes #138260)
This commit is contained in:
@@ -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
|
||||
};
|
||||
}
|
||||
|
||||
+3
-1
@@ -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,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+1
-1
@@ -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(),
|
||||
|
||||
Vendored
+6
-1
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user