mirror of
https://github.com/Microsoft/vscode
synced 2024-09-13 21:55:38 +00:00
Merge pull request #124964 from microsoft/alex/ghost-text
ghost text improvements
This commit is contained in:
commit
4093effb9b
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}; }`);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
38
src/vs/monaco.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue