Merge pull request #124964 from microsoft/alex/ghost-text

ghost text improvements
This commit is contained in:
Henning Dieterichs 2021-05-31 19:29:13 +02:00 committed by GitHub
commit 4093effb9b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 134 additions and 44 deletions

View file

@ -686,7 +686,7 @@ export interface CompletionItemProvider {
}
/**
* How an {@link InlineCompletionItemProvider inline completion provider} was triggered.
* How an {@link InlineCompletionsProvider inline completion provider} was triggered.
*/
export enum InlineCompletionTriggerKind {
/**
@ -709,9 +709,6 @@ export interface InlineCompletionContext {
readonly triggerKind: InlineCompletionTriggerKind;
}
/**
* @internal
*/
export interface InlineCompletion {
/**
* The text to insert.
@ -729,20 +726,21 @@ export interface InlineCompletion {
readonly command?: Command;
}
/**
* @internal
*/
export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> {
readonly items: readonly TItem[];
}
/**
* @internal
*/
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
/**
* Will be called when an item is shown.
*/
handleItemDidShow?(completions: T, item: T['items'][number]): void;
/**
* Will be called when a completions list is no longer in use and can be garbage-collected.
*/
freeInlineCompletions(completions: T): void;
}

View file

@ -361,7 +361,7 @@ export enum InlayHintKind {
}
/**
* How an {@link InlineCompletionItemProvider inline completion provider} was triggered.
* How an {@link InlineCompletionsProvider inline completion provider} was triggered.
*/
export enum InlineCompletionTriggerKind {
/**

View file

@ -43,8 +43,8 @@ export const editorGutter = registerColor('editorGutter.background', { dark: edi
export const editorUnnecessaryCodeBorder = registerColor('editorUnnecessaryCode.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('unnecessaryCodeBorder', 'Border color of unnecessary (unused) source code in the editor.'));
export const editorUnnecessaryCodeOpacity = registerColor('editorUnnecessaryCode.opacity', { dark: Color.fromHex('#000a'), light: Color.fromHex('#0007'), hc: null }, nls.localize('unnecessaryCodeOpacity', 'Opacity of unnecessary (unused) source code in the editor. For example, "#000000c0" will render the code with 75% opacity. For high contrast themes, use the \'editorUnnecessaryCode.border\' theme color to underline unnecessary code instead of fading it out.'));
export const editorSuggestPreviewBorder = registerColor('editorSuggestPreview.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('editorSuggestPreviewBorder', 'Border color of inline suggestion preview in the editor.'));
export const editorSuggestPreviewOpacity = registerColor('editorSuggestPreview.opacity', { dark: Color.fromHex('#FFFa'), light: Color.fromHex('#0007'), hc: null }, nls.localize('editorSuggestPreviewOpacity', 'Opacity of inline suggestion preview in the editor. For example, "#000000c0" will render the code with 75% opacity. For high contrast themes, use the \'editorSuggestPreview.border\' theme color instead of fading it out.'));
export const ghostTextBorder = registerColor('editorGhostText.border', { dark: null, light: null, hc: Color.fromHex('#fff').transparent(0.8) }, nls.localize('editorGhostTextBorder', 'Border color of ghost text in the editor.'));
export const ghostTextForeground = registerColor('editorGhostText.foreground', { dark: Color.fromHex('#FFFa'), light: Color.fromHex('#0007'), hc: null }, nls.localize('editorGhostTextForeground', 'Foreground color of the ghost text in the editor.'));
const rulerRangeDefault = new Color(new RGBA(0, 122, 204, 0.6));
export const overviewRulerRangeHighlight = registerColor('editorOverviewRuler.rangeHighlightForeground', { dark: rulerRangeDefault, light: rulerRangeDefault, hc: rulerRangeDefault }, nls.localize('overviewRulerRangeHighlight', 'Overview ruler marker color for range highlights. The color must not be opaque so as not to hide underlying decorations.'), true);

View file

@ -19,7 +19,7 @@ import { Position } from 'vs/editor/common/core/position';
import { Emitter, Event } from 'vs/base/common/event';
import { IModelDeltaDecoration } from 'vs/editor/common/model';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { editorSuggestPreviewBorder, editorSuggestPreviewOpacity } from 'vs/editor/common/view/editorColorRegistry';
import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/editorColorRegistry';
import { RGBA, Color } from 'vs/base/common/color';
import { CursorColumns } from 'vs/editor/common/controller/cursorCommon';
@ -152,28 +152,34 @@ export class GhostTextWidget extends Disposable {
this.codeEditorDecorationTypeKey = null;
}
if (renderData) {
const suggestPreviewForeground = this._themeService.getColorTheme().getColor(editorSuggestPreviewOpacity);
if (renderData && renderData.lines.length > 0) {
const foreground = this._themeService.getColorTheme().getColor(ghostTextForeground);
let opacity: string | undefined = undefined;
let color: string | undefined = undefined;
if (suggestPreviewForeground) {
if (foreground) {
function opaque(color: Color): Color {
const { r, b, g } = color.rgba;
return new Color(new RGBA(r, g, b, 255));
}
opacity = String(suggestPreviewForeground.rgba.a);
color = Color.Format.CSS.format(opaque(suggestPreviewForeground))!;
opacity = String(foreground.rgba.a);
color = Color.Format.CSS.format(opaque(foreground))!;
}
const borderColor = this._themeService.getColorTheme().getColor(ghostTextBorder);
let border: string | undefined = undefined;
if (borderColor) {
border = `2px dashed ${borderColor}`;
}
// We add 0 to bring it before any other decoration.
this.codeEditorDecorationTypeKey = `0-ghost-text-${++GhostTextWidget.decorationTypeCount}`;
const line = this.editor.getModel()?.getLineContent(renderData.position.lineNumber) || '';
const linePrefix = line.substr(0, renderData.position.column - 1);
const opts = this.editor.getOptions();
const renderWhitespace = opts.get(EditorOption.renderWhitespace);
const contentText = renderSingleLineText(renderData.lines[0] || '', linePrefix, renderData.tabSize, renderWhitespace === 'all');
// To avoid visual confusion, we don't want to render visible whitespace
const contentText = renderSingleLineText(renderData.lines[0] || '', linePrefix, renderData.tabSize, false);
this._codeEditorService.registerDecorationType('ghost-text', this.codeEditorDecorationTypeKey, {
after: {
@ -181,6 +187,7 @@ export class GhostTextWidget extends Disposable {
contentText,
opacity,
color,
border,
},
});
}
@ -265,7 +272,8 @@ export class GhostTextWidget extends Disposable {
const opts = this.editor.getOptions();
const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations);
const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter);
const renderWhitespace = opts.get(EditorOption.renderWhitespace);
// To avoid visual confusion, we don't want to render visible whitespace
const renderWhitespace = 'none';
const renderControlCharacters = opts.get(EditorOption.renderControlCharacters);
const fontLigatures = opts.get(EditorOption.fontLigatures);
const fontInfo = opts.get(EditorOption.fontInfo);
@ -388,24 +396,24 @@ class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
}
registerThemingParticipant((theme, collector) => {
const suggestPreviewForeground = theme.getColor(editorSuggestPreviewOpacity);
const foreground = theme.getColor(ghostTextForeground);
if (suggestPreviewForeground) {
if (foreground) {
function opaque(color: Color): Color {
const { r, b, g } = color.rgba;
return new Color(new RGBA(r, g, b, 255));
}
const opacity = String(suggestPreviewForeground.rgba.a);
const color = Color.Format.CSS.format(opaque(suggestPreviewForeground))!;
const opacity = String(foreground.rgba.a);
const color = Color.Format.CSS.format(opaque(foreground))!;
// We need to override the only used token type .mtk1
collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { opacity: ${opacity}; color: ${color}; }`);
}
const suggestPreviewBorder = theme.getColor(editorSuggestPreviewBorder);
if (suggestPreviewBorder) {
collector.addRule(`.monaco-editor .suggest-preview-text { border-bottom: 2px dashed ${suggestPreviewBorder}; }`);
const border = theme.getColor(ghostTextBorder);
if (border) {
collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { border: 2px dashed ${border}; }`);
}
});

View file

@ -81,16 +81,16 @@ export class InlineCompletionsHoverParticipant implements IEditorHoverParticipan
this._contextKeyService
);
statusBar.addAction({
label: nls.localize('showPreviousInlineCompletion', "Previous"),
commandId: ShowPreviousInlineCompletionAction.ID,
run: () => this._commandService.executeCommand(ShowPreviousInlineCompletionAction.ID)
});
statusBar.addAction({
label: nls.localize('showNextInlineCompletion', "Next"),
commandId: ShowNextInlineCompletionAction.ID,
run: () => this._commandService.executeCommand(ShowNextInlineCompletionAction.ID)
});
statusBar.addAction({
label: nls.localize('showPreviousInlineCompletion', "Previous"),
commandId: ShowPreviousInlineCompletionAction.ID,
run: () => this._commandService.executeCommand(ShowPreviousInlineCompletionAction.ID)
});
for (const [_, group] of menu.getActions()) {
for (const action of group) {

View file

@ -33,7 +33,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
) {
super();
this._register(this.editor.onDidChangeModelContent((e) => {
this._register(this.editor.onDidType((e) => {
if (this.session && !this.session.isValid) {
this.hide();
}
@ -428,22 +428,50 @@ export interface NormalizedInlineCompletion extends InlineCompletion {
range: Range;
}
function leftTrim(str: string): string {
return str.replace(/^\s+/, '');
}
export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel): GhostText | undefined {
// This is a single line string
const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
if (!inlineCompletion.text.startsWith(valueToBeReplaced)) {
let remainingInsertText: string;
// Consider these cases
// valueToBeReplaced -> inlineCompletion.text
// "\t\tfoo" -> "\t\tfoobar" (+"bar")
// "\t" -> "\t\tfoobar" (+"\tfoobar")
// "\t\tfoo" -> "\t\t\tfoobar" (+"\t", +"bar")
// "\t\tfoo" -> "\tfoobar" (-"\t", +"\bar")
if (inlineCompletion.text.startsWith(valueToBeReplaced)) {
remainingInsertText = inlineCompletion.text.substr(valueToBeReplaced.length);
} else {
const valueToBeReplacedTrimmed = leftTrim(valueToBeReplaced);
const insertTextTrimmed = leftTrim(inlineCompletion.text);
if (!insertTextTrimmed.startsWith(valueToBeReplacedTrimmed)) {
return undefined;
}
remainingInsertText = insertTextTrimmed.substr(valueToBeReplacedTrimmed.length);
}
const position = inlineCompletion.range.getEndPosition();
const lines = strings.splitLines(remainingInsertText);
if (lines.length > 1 && textModel.getLineMaxColumn(position.lineNumber) !== position.column) {
// Such ghost text is not supported.
return undefined;
}
const lines = strings.splitLines(inlineCompletion.text.substr(valueToBeReplaced.length));
return {
lines,
position: inlineCompletion.range.getEndPosition()
position
};
}
export interface LiveInlineCompletion extends InlineCompletion {
range: Range;
export interface LiveInlineCompletion extends NormalizedInlineCompletion {
sourceProvider: InlineCompletionsProvider;
sourceInlineCompletion: InlineCompletion;
sourceInlineCompletions: InlineCompletions;
@ -504,6 +532,10 @@ async function provideInlineCompletions(
sourceInlineCompletions: completions,
sourceInlineCompletion: item
}))) {
if (item.range.startLineNumber !== item.range.endLineNumber) {
// Ignore invalid ranges.
continue;
}
itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item);
}
}

View file

@ -13,6 +13,7 @@ import { CompletionItemInsertTextRule } from 'vs/editor/common/modes';
import { BaseGhostTextWidgetModel, GhostText } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget';
import { inlineCompletionToGhostText, NormalizedInlineCompletion } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
import { SnippetParser } from 'vs/editor/contrib/snippet/snippetParser';
import { SnippetSession } from 'vs/editor/contrib/snippet/snippetSession';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { ISelectedSuggestion } from 'vs/editor/contrib/suggest/suggestWidget';
@ -145,7 +146,14 @@ function getInlineCompletion(suggestController: SuggestController, position: Pos
let { insertText } = item.completion;
if (item.completion.insertTextRules! & CompletionItemInsertTextRule.InsertAsSnippet) {
insertText = new SnippetParser().text(insertText);
const snippet = new SnippetParser().parse(insertText);
const model = suggestController.editor.getModel()!;
SnippetSession.adjustWhitespace(
model, position, snippet,
true,
true
);
insertText = snippet.toString();
}
const info = suggestController.getOverwriteInfo(item, false);

View file

@ -547,6 +547,13 @@ export function registerDocumentRangeSemanticTokensProvider(languageId: string,
return modes.DocumentRangeSemanticTokensProviderRegistry.register(languageId, provider);
}
/**
* Register an inline completions provider.
*/
export function registerInlineCompletionsProvider(languageId: string, provider: modes.InlineCompletionsProvider): IDisposable {
return modes.InlineCompletionsProviderRegistry.register(languageId, provider);
}
/**
* Contains additional diagnostic information about the context in which
* a [code action](#CodeActionProvider.provideCodeActions) is run.
@ -613,6 +620,7 @@ export function createMonacoLanguagesAPI(): typeof monaco.languages {
registerSelectionRangeProvider: <any>registerSelectionRangeProvider,
registerDocumentSemanticTokensProvider: <any>registerDocumentSemanticTokensProvider,
registerDocumentRangeSemanticTokensProvider: <any>registerDocumentRangeSemanticTokensProvider,
registerInlineCompletionsProvider: <any>registerInlineCompletionsProvider,
// enums
DocumentHighlightKind: standaloneEnums.DocumentHighlightKind,

38
src/vs/monaco.d.ts vendored
View file

@ -5330,6 +5330,11 @@ declare namespace monaco.languages {
*/
export function registerDocumentRangeSemanticTokensProvider(languageId: string, provider: DocumentRangeSemanticTokensProvider): IDisposable;
/**
* Register an inline completions provider.
*/
export function registerInlineCompletionsProvider(languageId: string, provider: InlineCompletionsProvider): IDisposable;
/**
* Contains additional diagnostic information about the context in which
* a [code action](#CodeActionProvider.provideCodeActions) is run.
@ -5837,7 +5842,7 @@ declare namespace monaco.languages {
}
/**
* How an {@link InlineCompletionItemProvider inline completion provider} was triggered.
* How an {@link InlineCompletionsProvider inline completion provider} was triggered.
*/
export enum InlineCompletionTriggerKind {
/**
@ -5859,6 +5864,37 @@ declare namespace monaco.languages {
readonly triggerKind: InlineCompletionTriggerKind;
}
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.
*/
readonly text: string;
/**
* The range to replace.
* Must begin and end on the same line.
*/
readonly range?: IRange;
readonly command?: Command;
}
export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> {
readonly items: readonly TItem[];
}
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(model: editor.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;
/**
* Will be called when an item is shown.
*/
handleItemDidShow?(completions: T, item: T['items'][number]): void;
/**
* Will be called when a completions list is no longer in use and can be garbage-collected.
*/
freeInlineCompletions(completions: T): void;
}
export interface CodeAction {
title: string;
command?: Command;