Merge pull request #127295 from microsoft/hediet/multi-line-ghost-text
Implements support for multiline suggestions in the middle of an existing line
This commit is contained in:
commit
e1a8566a29
8 changed files with 184 additions and 103 deletions
|
@ -18,3 +18,7 @@
|
|||
text-decoration: underline;
|
||||
text-underline-position: under;
|
||||
}
|
||||
|
||||
.monaco-editor .ghost-text-hidden {
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -12,21 +12,13 @@ export class GhostText {
|
|||
constructor(
|
||||
public readonly lineNumber: number,
|
||||
public readonly parts: GhostTextPart[],
|
||||
public readonly additionalLines: string[],
|
||||
public readonly additionalReservedLineCount: number = 0
|
||||
) {
|
||||
}
|
||||
|
||||
public get isMultiLine(): boolean {
|
||||
return this.additionalLines.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export interface GhostTextPart {
|
||||
/**
|
||||
* Single line text.
|
||||
*/
|
||||
readonly text: string;
|
||||
readonly lines: string[];
|
||||
readonly column: number;
|
||||
}
|
||||
|
||||
|
|
|
@ -148,8 +148,8 @@ export class ActiveGhostTextController extends Disposable {
|
|||
|
||||
const ghostText = this.model.inlineCompletionsModel.ghostText;
|
||||
if (ghostText && ghostText.parts.length > 0) {
|
||||
const { column, text } = ghostText.parts[0];
|
||||
const suggestionStartsWithWs = text.startsWith(' ') || text.startsWith('\t');
|
||||
const { column, lines } = ghostText.parts[0];
|
||||
const suggestionStartsWithWs = lines[0].startsWith(' ') || lines[0].startsWith('\t');
|
||||
|
||||
const indentationEndColumn = this.editor.getModel().getLineIndentColumn(ghostText.lineNumber);
|
||||
const inIndentation = column <= indentationEndColumn;
|
||||
|
|
|
@ -21,13 +21,16 @@ import { ghostTextBorder, ghostTextForeground } from 'vs/editor/common/view/edit
|
|||
import { RGBA, Color } from 'vs/base/common/color';
|
||||
import { CursorColumns } from 'vs/editor/common/controller/cursorCommon';
|
||||
import { IDecorationRenderOptions } from 'vs/editor/common/editorCommon';
|
||||
import { GhostTextWidgetModel, GhostTextPart } from 'vs/editor/contrib/inlineCompletions/ghostText';
|
||||
import { GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostText';
|
||||
import { IModelDeltaDecoration } from 'vs/editor/common/model';
|
||||
import { LineDecoration } from 'vs/editor/common/viewLayout/lineDecorations';
|
||||
import { InlineDecorationType } from 'vs/editor/common/viewModel/viewModel';
|
||||
|
||||
const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value });
|
||||
|
||||
export class GhostTextWidget extends Disposable {
|
||||
private disposed = false;
|
||||
private readonly partsWidget = this._register(new PartsWidget(this.editor, this.codeEditorService, this.themeService));
|
||||
private readonly partsWidget = this._register(new DecorationsWidget(this.editor, this.codeEditorService, this.themeService));
|
||||
private readonly additionalLinesWidget = this._register(new AdditionalLinesWidget(this.editor));
|
||||
private viewMoreContentWidget: ViewMoreLinesContentWidget | undefined = undefined;
|
||||
|
||||
|
@ -79,14 +82,67 @@ export class GhostTextWidget extends Disposable {
|
|||
}
|
||||
|
||||
const ghostText = this.model.ghostText;
|
||||
this.partsWidget.setParts(ghostText.lineNumber, ghostText.parts);
|
||||
this.additionalLinesWidget.updateLines(ghostText.lineNumber, ghostText.additionalLines, ghostText.additionalReservedLineCount);
|
||||
|
||||
if (ghostText.additionalLines.length < 0) {
|
||||
// not supported at the moment
|
||||
const inlineTexts = new Array<InsertedInlineText>();
|
||||
const additionalLines = new Array<LineData>();
|
||||
|
||||
function addToAdditionalLines(lines: string[], className: string | undefined) {
|
||||
if (additionalLines.length > 0) {
|
||||
const lastLine = additionalLines[additionalLines.length - 1];
|
||||
if (className) {
|
||||
lastLine.decorations.push(new LineDecoration(lastLine.content.length + 1, lastLine.content.length + 1 + lines[0].length, className, InlineDecorationType.Regular));
|
||||
}
|
||||
lastLine.content += lines[0];
|
||||
|
||||
lines.splice(0, 1);
|
||||
}
|
||||
for (const line of lines) {
|
||||
additionalLines.push({
|
||||
content: line,
|
||||
decorations: className ? [new LineDecoration(1, line.length + 1, className, InlineDecorationType.Regular)] : []
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const textBufferLine = this.editor.getModel().getLineContent(ghostText.lineNumber);
|
||||
this.editor.getModel().getLineTokens(ghostText.lineNumber);
|
||||
|
||||
let hiddenTextStartColumn: number | undefined = undefined;
|
||||
let lastIdx = 0;
|
||||
for (const part of ghostText.parts) {
|
||||
let lines = part.lines;
|
||||
if (hiddenTextStartColumn === undefined) {
|
||||
inlineTexts.push({
|
||||
column: part.column,
|
||||
text: lines[0],
|
||||
});
|
||||
lines = lines.slice(1);
|
||||
} else {
|
||||
addToAdditionalLines([textBufferLine.substring(lastIdx, part.column - 1)], undefined);
|
||||
}
|
||||
|
||||
if (lines.length > 0) {
|
||||
addToAdditionalLines(lines, 'ghost-text');
|
||||
if (hiddenTextStartColumn === undefined && part.column <= textBufferLine.length) {
|
||||
hiddenTextStartColumn = part.column;
|
||||
}
|
||||
}
|
||||
|
||||
lastIdx = part.column - 1;
|
||||
}
|
||||
if (hiddenTextStartColumn !== undefined) {
|
||||
addToAdditionalLines([textBufferLine.substring(lastIdx)], undefined);
|
||||
}
|
||||
|
||||
this.partsWidget.setParts(ghostText.lineNumber, inlineTexts,
|
||||
hiddenTextStartColumn !== undefined ? { column: hiddenTextStartColumn, length: textBufferLine.length + 1 - hiddenTextStartColumn } : undefined);
|
||||
this.additionalLinesWidget.updateLines(ghostText.lineNumber, additionalLines, ghostText.additionalReservedLineCount);
|
||||
|
||||
if (ghostText.parts.some(p => p.lines.length < 0)) {
|
||||
// Not supported at the moment, condition is always false.
|
||||
this.viewMoreContentWidget = this.renderViewMoreLines(
|
||||
new Position(ghostText.lineNumber, this.editor.getModel()!.getLineMaxColumn(ghostText.lineNumber)),
|
||||
'', ghostText.additionalLines.length
|
||||
'', 0
|
||||
);
|
||||
} else {
|
||||
this.viewMoreContentWidget?.dispose();
|
||||
|
@ -127,7 +183,17 @@ export class GhostTextWidget extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
class PartsWidget implements IDisposable {
|
||||
interface HiddenText {
|
||||
column: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
interface InsertedInlineText {
|
||||
column: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
class DecorationsWidget implements IDisposable {
|
||||
private decorationIds: string[] = [];
|
||||
private disposableStore: DisposableStore = new DisposableStore();
|
||||
|
||||
|
@ -148,7 +214,7 @@ class PartsWidget implements IDisposable {
|
|||
this.disposableStore.clear();
|
||||
}
|
||||
|
||||
public setParts(lineNumber: number, parts: GhostTextPart[]): void {
|
||||
public setParts(lineNumber: number, parts: InsertedInlineText[], hiddenText?: HiddenText): void {
|
||||
this.disposableStore.clear();
|
||||
|
||||
const colorTheme = this.themeService.getColorTheme();
|
||||
|
@ -177,15 +243,25 @@ class PartsWidget implements IDisposable {
|
|||
let lastIndex = 0;
|
||||
let currentLinePrefix = '';
|
||||
|
||||
this.decorationIds = this.editor.deltaDecorations(this.decorationIds, parts.map(p => {
|
||||
const hiddenTextDecorations = new Array<IModelDeltaDecoration>();
|
||||
if (hiddenText) {
|
||||
hiddenTextDecorations.push({
|
||||
range: Range.fromPositions(new Position(lineNumber, hiddenText.column), new Position(lineNumber, hiddenText.column + hiddenText.length)),
|
||||
options: {
|
||||
inlineClassName: 'ghost-text-hidden',
|
||||
description: 'ghost-text-hidden'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.decorationIds = this.editor.deltaDecorations(this.decorationIds, parts.map<IModelDeltaDecoration>(p => {
|
||||
currentLinePrefix += line.substring(lastIndex, p.column - 1);
|
||||
lastIndex = p.column - 1;
|
||||
|
||||
// To avoid visual confusion, we don't want to render visible whitespace
|
||||
const contentText = this.renderSingleLineText(p.text, currentLinePrefix, tabSize, false);
|
||||
|
||||
const decorationType = registerDecorationType(this.codeEditorService, 'ghost-text', '0-ghost-text-', {
|
||||
const decorationType = this.disposableStore.add(registerDecorationType(this.codeEditorService, 'ghost-text', '0-ghost-text-', {
|
||||
after: {
|
||||
// TODO: escape?
|
||||
contentText,
|
||||
|
@ -193,22 +269,21 @@ class PartsWidget implements IDisposable {
|
|||
color,
|
||||
border,
|
||||
},
|
||||
});
|
||||
this.disposableStore.add(decorationType);
|
||||
}));
|
||||
|
||||
return ({
|
||||
range: Range.fromPositions(new Position(lineNumber, p.column)),
|
||||
options: {
|
||||
...decorationType.resolve()
|
||||
}
|
||||
});
|
||||
}));
|
||||
}).concat(hiddenTextDecorations));
|
||||
}
|
||||
|
||||
private renderSingleLineText(text: string, lineStart: string, tabSize: number, renderWhitespace: boolean): string {
|
||||
const newLine = lineStart + text;
|
||||
const visibleColumnsByColumns = CursorColumns.visibleColumnsByColumns(newLine, tabSize);
|
||||
|
||||
|
||||
let contentText = '';
|
||||
let curCol = lineStart.length + 1;
|
||||
for (const c of text) {
|
||||
|
@ -264,7 +339,7 @@ class AdditionalLinesWidget implements IDisposable {
|
|||
});
|
||||
}
|
||||
|
||||
public updateLines(lineNumber: number, additionalLines: string[], minReservedLineCount: number): void {
|
||||
public updateLines(lineNumber: number, additionalLines: LineData[], minReservedLineCount: number): void {
|
||||
const textModel = this.editor.getModel();
|
||||
if (!textModel) {
|
||||
return;
|
||||
|
@ -281,7 +356,7 @@ class AdditionalLinesWidget implements IDisposable {
|
|||
const heightInLines = Math.max(additionalLines.length, minReservedLineCount);
|
||||
if (heightInLines > 0) {
|
||||
const domNode = document.createElement('div');
|
||||
this.renderLines(domNode, tabSize, additionalLines, this.editor.getOptions());
|
||||
renderLines(domNode, tabSize, additionalLines, this.editor.getOptions());
|
||||
|
||||
this._viewZoneId = changeAccessor.addZone({
|
||||
afterLineNumber: lineNumber,
|
||||
|
@ -291,62 +366,68 @@ class AdditionalLinesWidget implements IDisposable {
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private renderLines(domNode: HTMLElement, tabSize: number, lines: string[], opts: IComputedEditorOptions): void {
|
||||
const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations);
|
||||
const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter);
|
||||
// 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);
|
||||
const lineHeight = opts.get(EditorOption.lineHeight);
|
||||
interface LineData {
|
||||
content: string;
|
||||
decorations: LineDecoration[];
|
||||
}
|
||||
|
||||
const sb = createStringBuilder(10000);
|
||||
sb.appendASCIIString('<div class="suggest-preview-text">');
|
||||
function renderLines(domNode: HTMLElement, tabSize: number, lines: LineData[], opts: IComputedEditorOptions): void {
|
||||
const disableMonospaceOptimizations = opts.get(EditorOption.disableMonospaceOptimizations);
|
||||
const stopRenderingLineAfter = opts.get(EditorOption.stopRenderingLineAfter);
|
||||
// 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);
|
||||
const lineHeight = opts.get(EditorOption.lineHeight);
|
||||
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
const line = lines[i];
|
||||
sb.appendASCIIString('<div class="view-line');
|
||||
sb.appendASCIIString('" style="top:');
|
||||
sb.appendASCIIString(String(i * lineHeight));
|
||||
sb.appendASCIIString('px;width:1000000px;">');
|
||||
const sb = createStringBuilder(10000);
|
||||
sb.appendASCIIString('<div class="suggest-preview-text">');
|
||||
|
||||
const isBasicASCII = strings.isBasicASCII(line);
|
||||
const containsRTL = strings.containsRTL(line);
|
||||
const lineTokens = LineTokens.createEmpty(line);
|
||||
for (let i = 0, len = lines.length; i < len; i++) {
|
||||
const lineData = lines[i];
|
||||
const line = lineData.content;
|
||||
sb.appendASCIIString('<div class="view-line');
|
||||
sb.appendASCIIString('" style="top:');
|
||||
sb.appendASCIIString(String(i * lineHeight));
|
||||
sb.appendASCIIString('px;width:1000000px;">');
|
||||
|
||||
renderViewLine(new RenderLineInput(
|
||||
(fontInfo.isMonospace && !disableMonospaceOptimizations),
|
||||
fontInfo.canUseHalfwidthRightwardsArrow,
|
||||
line,
|
||||
false,
|
||||
isBasicASCII,
|
||||
containsRTL,
|
||||
0,
|
||||
lineTokens,
|
||||
[],
|
||||
tabSize,
|
||||
0,
|
||||
fontInfo.spaceWidth,
|
||||
fontInfo.middotWidth,
|
||||
fontInfo.wsmiddotWidth,
|
||||
stopRenderingLineAfter,
|
||||
renderWhitespace,
|
||||
renderControlCharacters,
|
||||
fontLigatures !== EditorFontLigatures.OFF,
|
||||
null
|
||||
), sb);
|
||||
const isBasicASCII = strings.isBasicASCII(line);
|
||||
const containsRTL = strings.containsRTL(line);
|
||||
const lineTokens = LineTokens.createEmpty(line);
|
||||
|
||||
renderViewLine(new RenderLineInput(
|
||||
(fontInfo.isMonospace && !disableMonospaceOptimizations),
|
||||
fontInfo.canUseHalfwidthRightwardsArrow,
|
||||
line,
|
||||
false,
|
||||
isBasicASCII,
|
||||
containsRTL,
|
||||
0,
|
||||
lineTokens,
|
||||
lineData.decorations,
|
||||
tabSize,
|
||||
0,
|
||||
fontInfo.spaceWidth,
|
||||
fontInfo.middotWidth,
|
||||
fontInfo.wsmiddotWidth,
|
||||
stopRenderingLineAfter,
|
||||
renderWhitespace,
|
||||
renderControlCharacters,
|
||||
fontLigatures !== EditorFontLigatures.OFF,
|
||||
null
|
||||
), sb);
|
||||
|
||||
sb.appendASCIIString('</div>');
|
||||
}
|
||||
sb.appendASCIIString('</div>');
|
||||
|
||||
Configuration.applyFontInfoSlow(domNode, fontInfo);
|
||||
const html = sb.build();
|
||||
const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html;
|
||||
domNode.innerHTML = trustedhtml as string;
|
||||
}
|
||||
sb.appendASCIIString('</div>');
|
||||
|
||||
Configuration.applyFontInfoSlow(domNode, fontInfo);
|
||||
const html = sb.build();
|
||||
const trustedhtml = ttPolicy ? ttPolicy.createHTML(html) : html;
|
||||
domNode.innerHTML = trustedhtml as string;
|
||||
}
|
||||
|
||||
let keyCounter = 0;
|
||||
|
@ -406,11 +487,11 @@ registerThemingParticipant((theme, collector) => {
|
|||
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}; }`);
|
||||
collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { opacity: ${opacity}; color: ${color}; }`);
|
||||
}
|
||||
|
||||
const border = theme.getColor(ghostTextBorder);
|
||||
if (border) {
|
||||
collector.addRule(`.monaco-editor .suggest-preview-text .mtk1 { border: 2px dashed ${border}; }`);
|
||||
collector.addRule(`.monaco-editor .suggest-preview-text .ghost-text { border: 2px dashed ${border}; }`);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -178,6 +178,12 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
}));
|
||||
|
||||
this._register(this.editor.onDidChangeCursorPosition((e) => {
|
||||
if (this.cache.value) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.editor.onDidChangeModelContent((e) => {
|
||||
if (this.cache.value) {
|
||||
let hasChanged = false;
|
||||
|
@ -287,7 +293,7 @@ export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
public get ghostText(): GhostText | undefined {
|
||||
const currentCompletion = this.currentCompletion;
|
||||
const mode = this.editor.getOptions().get(EditorOption.inlineSuggest).mode;
|
||||
return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode) : undefined;
|
||||
return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, this.editor.getSelection().getEndPosition()) : undefined;
|
||||
}
|
||||
|
||||
get currentCompletion(): LiveInlineCompletion | undefined {
|
||||
|
@ -484,7 +490,7 @@ export interface NormalizedInlineCompletion extends InlineCompletion {
|
|||
range: Range;
|
||||
}
|
||||
|
||||
export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel, mode: 'prefix' | 'subwordDiff'): GhostText | undefined {
|
||||
export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel, mode: 'prefix' | 'subwordDiff', cursorPosition?: Position): GhostText | undefined {
|
||||
if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
|
||||
// Only single line replacements are supported.
|
||||
return undefined;
|
||||
|
@ -498,7 +504,6 @@ export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCo
|
|||
const lineNumber = inlineCompletion.range.startLineNumber;
|
||||
|
||||
const parts = new Array<GhostTextPart>();
|
||||
let additionalLines = new Array<string>();
|
||||
|
||||
if (mode === 'prefix') {
|
||||
const filteredChanges = changes.filter(c => c.originalLength === 0);
|
||||
|
@ -510,6 +515,11 @@ export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCo
|
|||
for (const c of changes) {
|
||||
const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
|
||||
|
||||
if (cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
|
||||
// No ghost text before cursor
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (c.originalLength > 0) {
|
||||
const originalText = valueToBeReplaced.substr(c.originalStart, c.originalLength);
|
||||
const firstNonWsCol = textModel.getLineFirstNonWhitespaceColumn(lineNumber);
|
||||
|
@ -523,21 +533,11 @@ export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCo
|
|||
}
|
||||
|
||||
const text = inlineCompletion.text.substr(c.modifiedStart, c.modifiedLength);
|
||||
const isEndOfLine = insertColumn === textModel.getLineMaxColumn(lineNumber);
|
||||
if (!isEndOfLine) {
|
||||
if (text.indexOf('\n') !== -1) {
|
||||
// no line breaks inside the text
|
||||
return undefined;
|
||||
}
|
||||
parts.push({ column: insertColumn, text });
|
||||
} else {
|
||||
const lines = strings.splitLines(text);
|
||||
additionalLines = lines.slice(1);
|
||||
parts.push({ column: insertColumn, text: lines[0] });
|
||||
}
|
||||
const lines = strings.splitLines(text);
|
||||
parts.push({ column: insertColumn, lines });
|
||||
}
|
||||
|
||||
return new GhostText(lineNumber, parts, additionalLines, 0);
|
||||
return new GhostText(lineNumber, parts, 0);
|
||||
}
|
||||
|
||||
export interface LiveInlineCompletion extends NormalizedInlineCompletion {
|
||||
|
|
|
@ -142,11 +142,11 @@ export class SuggestWidgetAdapterModel extends BaseGhostTextWidgetModel {
|
|||
? (
|
||||
inlineCompletionToGhostText(completion, this.editor.getModel(), mode) ||
|
||||
// Show an invisible ghost text to reserve space
|
||||
new GhostText(completion.range.endLineNumber, [], [], this.minReservedLineCount)
|
||||
new GhostText(completion.range.endLineNumber, [], this.minReservedLineCount)
|
||||
) : undefined;
|
||||
|
||||
if (this.currentGhostText && this.expanded) {
|
||||
this.minReservedLineCount = Math.max(this.minReservedLineCount, this.currentGhostText.additionalLines.length);
|
||||
this.minReservedLineCount = Math.max(this.minReservedLineCount, ...this.currentGhostText.parts.map(p => p.lines.length - 1));
|
||||
}
|
||||
|
||||
const suggestController = SuggestController.get(this.editor);
|
||||
|
|
|
@ -42,7 +42,7 @@ suite('inlineCompletionToGhostText', () => {
|
|||
assert.deepStrictEqual(getOutput('[aaa]aaa', 'aaaaaa'), 'aaa[aaa]aaa');
|
||||
assert.deepStrictEqual(getOutput('[foo]baz', 'boobar'), undefined);
|
||||
assert.deepStrictEqual(getOutput('[foo]foo', 'foofoo'), 'foo[foo]foo');
|
||||
assert.deepStrictEqual(getOutput('foo[]', 'bar\nhello'), 'foo[bar]{\nhello}');
|
||||
assert.deepStrictEqual(getOutput('foo[]', 'bar\nhello'), 'foo[bar\nhello]');
|
||||
});
|
||||
|
||||
test('Empty ghost text', () => {
|
||||
|
@ -437,7 +437,15 @@ test('Support backward instability', async function () {
|
|||
assert.deepStrictEqual(provider.getAndClearCallHistory(), [
|
||||
{ position: '(1,5)', text: 'foob', triggerKind: 0, }
|
||||
]);
|
||||
assert.deepStrictEqual(context.getAndClearViewStates(), ['foob[ar]', 'foob[az]']);
|
||||
assert.deepStrictEqual(context.getAndClearViewStates(), [
|
||||
/*
|
||||
TODO: Remove this flickering. Fortunately, it is not visible.
|
||||
It is caused by the text model updating before the cursor position.
|
||||
*/
|
||||
'foob',
|
||||
'foob[ar]',
|
||||
'foob[az]'
|
||||
]);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -24,11 +24,7 @@ export function renderGhostTextToText(ghostText: GhostText | undefined, text: st
|
|||
const tempModel = createTextModel(text);
|
||||
tempModel.applyEdits(
|
||||
[
|
||||
...ghostText.parts.map(p => ({ range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column }, text: `[${p.text}]` })),
|
||||
...(ghostText.additionalLines.length > 0 ? [{
|
||||
range: { startLineNumber: l, endLineNumber: l, startColumn: tempModel.getLineMaxColumn(l), endColumn: tempModel.getLineMaxColumn(l) },
|
||||
text: `{${ghostText.additionalLines.map(s => '\n' + s).join('')}}`
|
||||
}] : [])
|
||||
...ghostText.parts.map(p => ({ range: { startLineNumber: l, endLineNumber: l, startColumn: p.column, endColumn: p.column }, text: `[${p.lines.join('\n')}]` })),
|
||||
]
|
||||
);
|
||||
const value = tempModel.getValue();
|
||||
|
|
Loading…
Reference in a new issue