Merge pull request #124745 from microsoft/alex/ghost-text
More Ghost Text Improvements
This commit is contained in:
commit
b2da15ea2d
|
@ -458,6 +458,55 @@ export class CursorColumns {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array that maps one based columns to one based visible columns. The entry at position 0 is -1.
|
||||
*/
|
||||
public static visibleColumnsByColumns(lineContent: string, tabSize: number): number[] {
|
||||
const endOffset = lineContent.length;
|
||||
|
||||
let result = new Array<number>();
|
||||
result.push(-1);
|
||||
let pos = 0;
|
||||
let i = 0;
|
||||
while (i < endOffset) {
|
||||
const codePoint = strings.getNextCodePoint(lineContent, endOffset, i);
|
||||
i += (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
|
||||
|
||||
result.push(pos);
|
||||
if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) {
|
||||
result.push(pos);
|
||||
}
|
||||
|
||||
if (codePoint === CharCode.Tab) {
|
||||
pos = CursorColumns.nextRenderTabStop(pos, tabSize);
|
||||
} else {
|
||||
let graphemeBreakType = strings.getGraphemeBreakType(codePoint);
|
||||
while (i < endOffset) {
|
||||
const nextCodePoint = strings.getNextCodePoint(lineContent, endOffset, i);
|
||||
const nextGraphemeBreakType = strings.getGraphemeBreakType(nextCodePoint);
|
||||
if (strings.breakBetweenGraphemeBreakType(graphemeBreakType, nextGraphemeBreakType)) {
|
||||
break;
|
||||
}
|
||||
i += (nextCodePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN ? 2 : 1);
|
||||
|
||||
result.push(pos);
|
||||
if (codePoint >= Constants.UNICODE_SUPPLEMENTARY_PLANE_BEGIN) {
|
||||
result.push(pos);
|
||||
}
|
||||
|
||||
graphemeBreakType = nextGraphemeBreakType;
|
||||
}
|
||||
if (strings.isFullWidthCharacter(codePoint) || strings.isEmojiImprecise(codePoint)) {
|
||||
pos = pos + 2;
|
||||
} else {
|
||||
pos = pos + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
result.push(pos);
|
||||
return result;
|
||||
}
|
||||
|
||||
public static toStatusbarColumn(lineContent: string, column: number, tabSize: number): number {
|
||||
const lineContentLength = lineContent.length;
|
||||
const endOffset = column - 1 < lineContentLength ? column - 1 : lineContentLength;
|
||||
|
|
|
@ -15,11 +15,13 @@ import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inli
|
|||
import { SuggestWidgetAdapterModel } from 'vs/editor/contrib/inlineCompletions/suggestWidgetAdapterModel';
|
||||
import * as nls from 'vs/nls';
|
||||
import { ICommandService } from 'vs/platform/commands/common/commands';
|
||||
import { IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { ContextKeyExpr, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export class GhostTextController extends Disposable {
|
||||
public static readonly inlineCompletionsVisible = new RawContextKey<boolean>('inlineCompletionsVisible ', false, nls.localize('inlineCompletionsVisible', "Whether inline suggestions are visible"));
|
||||
public static readonly inlineCompletionSuggestsIndentation = new RawContextKey<boolean>('inlineCompletionSuggestsIndentation', false, nls.localize('inlineCompletionSuggestsIndentation', "Whether the inline suggestion suggests extending indentation"));
|
||||
|
||||
static ID = 'editor.contrib.ghostTextController';
|
||||
|
||||
public static get(editor: ICodeEditor): GhostTextController {
|
||||
|
@ -88,6 +90,7 @@ export class GhostTextController extends Disposable {
|
|||
|
||||
class GhostTextContextKeys {
|
||||
public readonly inlineCompletionVisible = GhostTextController.inlineCompletionsVisible.bindTo(this.contextKeyService);
|
||||
public readonly inlineCompletionSuggestsIndentation = GhostTextController.inlineCompletionSuggestsIndentation.bindTo(this.contextKeyService);
|
||||
|
||||
constructor(private readonly contextKeyService: IContextKeyService) {
|
||||
}
|
||||
|
@ -130,6 +133,21 @@ export class ActiveGhostTextController extends Disposable {
|
|||
this.widget.model === this.inlineCompletionsModel
|
||||
&& this.inlineCompletionsModel.ghostText !== undefined
|
||||
);
|
||||
|
||||
if (this.inlineCompletionsModel.ghostText) {
|
||||
const firstLine = this.inlineCompletionsModel.ghostText.lines[0] || '';
|
||||
const suggestionStartsWithWs = firstLine.startsWith(' ') || firstLine.startsWith('\t');
|
||||
const p = this.inlineCompletionsModel.ghostText.position;
|
||||
const indentationEndColumn = this.editor.getModel().getLineIndentColumn(p.lineNumber);
|
||||
const inIndentation = p.column <= indentationEndColumn;
|
||||
|
||||
this.contextKeys.inlineCompletionSuggestsIndentation.set(
|
||||
this.widget.model === this.inlineCompletionsModel
|
||||
&& suggestionStartsWithWs && inIndentation
|
||||
);
|
||||
} else {
|
||||
this.contextKeys.inlineCompletionSuggestsIndentation.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
public shouldShowHoverAt(hoverRange: Range): boolean {
|
||||
|
@ -182,7 +200,10 @@ const GhostTextCommand = EditorCommand.bindToContribution(GhostTextController.ge
|
|||
|
||||
registerEditorCommand(new GhostTextCommand({
|
||||
id: 'commitInlineCompletion',
|
||||
precondition: GhostTextController.inlineCompletionsVisible,
|
||||
precondition: ContextKeyExpr.and(
|
||||
GhostTextController.inlineCompletionsVisible,
|
||||
GhostTextController.inlineCompletionSuggestsIndentation.toNegated()
|
||||
),
|
||||
kbOpts: {
|
||||
weight: 100,
|
||||
primary: KeyCode.Tab,
|
||||
|
|
|
@ -21,6 +21,8 @@ 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 { RGBA, Color } from 'vs/base/common/color';
|
||||
import { MarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { CursorColumns } from 'vs/editor/common/controller/cursorCommon';
|
||||
|
||||
const ttPolicy = window.trustedTypes?.createPolicy('editorGhostText', { createHTML: value => value });
|
||||
|
||||
|
@ -149,8 +151,8 @@ export class GhostTextWidget extends Disposable {
|
|||
|
||||
if (renderData) {
|
||||
const suggestPreviewForeground = this._themeService.getColorTheme().getColor(editorSuggestPreviewOpacity);
|
||||
let opacity = '0.467';
|
||||
let color = 'white';
|
||||
let opacity: string | undefined = undefined;
|
||||
let color: string | undefined = undefined;
|
||||
if (suggestPreviewForeground) {
|
||||
function opaque(color: Color): Color {
|
||||
const { r, b, g } = color.rgba;
|
||||
|
@ -162,10 +164,18 @@ export class GhostTextWidget extends Disposable {
|
|||
}
|
||||
// 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');
|
||||
|
||||
this._codeEditorService.registerDecorationType('ghost-text', this.codeEditorDecorationTypeKey, {
|
||||
after: {
|
||||
// TODO: escape?
|
||||
contentText: renderData.lines[0],
|
||||
contentText,
|
||||
opacity,
|
||||
color,
|
||||
},
|
||||
|
@ -305,6 +315,41 @@ export class GhostTextWidget extends Disposable {
|
|||
}
|
||||
}
|
||||
|
||||
function 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) {
|
||||
if (c === '\t') {
|
||||
const width = visibleColumnsByColumns[curCol + 1] - visibleColumnsByColumns[curCol];
|
||||
if (renderWhitespace) {
|
||||
contentText += '→';
|
||||
for (let i = 1; i < width; i++) {
|
||||
contentText += '\xa0';
|
||||
}
|
||||
} else {
|
||||
for (let i = 0; i < width; i++) {
|
||||
contentText += '\xa0';
|
||||
}
|
||||
}
|
||||
} else if (c === ' ') {
|
||||
if (renderWhitespace) {
|
||||
contentText += '·';
|
||||
} else {
|
||||
contentText += '\xa0';
|
||||
}
|
||||
} else {
|
||||
contentText += c;
|
||||
}
|
||||
curCol += 1;
|
||||
}
|
||||
|
||||
return contentText;
|
||||
}
|
||||
|
||||
class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
|
||||
readonly allowEditorOverflow = false;
|
||||
readonly suppressMouseDown = false;
|
||||
|
@ -340,7 +385,6 @@ class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
|
|||
}
|
||||
|
||||
registerThemingParticipant((theme, collector) => {
|
||||
|
||||
const suggestPreviewForeground = theme.getColor(editorSuggestPreviewOpacity);
|
||||
|
||||
if (suggestPreviewForeground) {
|
||||
|
|
|
@ -37,6 +37,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
|
|||
this.hide();
|
||||
}
|
||||
setTimeout(() => {
|
||||
// Wait for the cursor update that happens in the same iteration loop iteration
|
||||
this.startSessionIfTriggered();
|
||||
}, 0);
|
||||
}));
|
||||
|
@ -71,7 +72,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
|
|||
public setActive(active: boolean) {
|
||||
this.active = active;
|
||||
if (active) {
|
||||
this.session?.scheduleUpdate();
|
||||
this.session?.scheduleAutomaticUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -101,44 +102,25 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
|
|||
}
|
||||
|
||||
public commitCurrentSuggestion(): void {
|
||||
if (this.session) {
|
||||
this.session.commitCurrentCompletion();
|
||||
}
|
||||
this.session?.commitCurrentCompletion();
|
||||
}
|
||||
|
||||
public showNextInlineCompletion(): void {
|
||||
if (this.session) {
|
||||
this.session.showNextInlineCompletion();
|
||||
}
|
||||
this.session?.showNextInlineCompletion();
|
||||
}
|
||||
|
||||
public showPreviousInlineCompletion(): void {
|
||||
if (this.session) {
|
||||
this.session.showPreviousInlineCompletion();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CachedInlineCompletion {
|
||||
public readonly semanticId: string = JSON.stringify(this.inlineCompletion);
|
||||
public lastRange: Range;
|
||||
|
||||
constructor(
|
||||
public readonly inlineCompletion: LiveInlineCompletion,
|
||||
public readonly decorationId: string,
|
||||
) {
|
||||
this.lastRange = inlineCompletion.range;
|
||||
this.session?.showPreviousInlineCompletion();
|
||||
}
|
||||
}
|
||||
|
||||
class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
||||
public readonly minReservedLineCount = 0;
|
||||
|
||||
private updatePromise: CancelablePromise<LiveInlineCompletions> | undefined = undefined;
|
||||
private cachedCompletions: CachedInlineCompletion[] | undefined = undefined;
|
||||
private cachedCompletionsSource: LiveInlineCompletions | undefined = undefined;
|
||||
private readonly updateOperation = this._register(new MutableDisposable<UpdateOperation>());
|
||||
private readonly cache = this._register(new MutableDisposable<SynchronizedInlineCompletionsCache>());
|
||||
|
||||
private updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50));
|
||||
private updateSoon = this._register(new RunOnceScheduler(() => this.update(InlineCompletionTriggerKind.Automatic), 50));
|
||||
private readonly textModel = this.editor.getModel();
|
||||
|
||||
constructor(
|
||||
|
@ -148,10 +130,6 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
private readonly commandService: ICommandService,
|
||||
) {
|
||||
super(editor);
|
||||
this._register(toDisposable(() => {
|
||||
this.clearGhostTextPromise();
|
||||
this.clearCache();
|
||||
}));
|
||||
|
||||
let lastCompletionItem: InlineCompletion | undefined = undefined;
|
||||
this._register(this.onDidChange(() => {
|
||||
|
@ -166,33 +144,29 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
}));
|
||||
|
||||
this._register(this.editor.onDidChangeModelDecorations(e => {
|
||||
if (!this.cachedCompletions) {
|
||||
return;
|
||||
}
|
||||
|
||||
let hasChanged = false;
|
||||
for (const c of this.cachedCompletions) {
|
||||
const newRange = this.textModel.getDecorationRange(c.decorationId);
|
||||
if (!newRange) {
|
||||
onUnexpectedError(new Error('Decoration has no range'));
|
||||
continue;
|
||||
}
|
||||
if (!c.lastRange.equalsRange(newRange)) {
|
||||
hasChanged = true;
|
||||
c.lastRange = newRange;
|
||||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(this.editor.onDidChangeModelContent((e) => {
|
||||
this.updateSoon.schedule();
|
||||
if (this.cache.value) {
|
||||
let hasChanged = false;
|
||||
for (const c of this.cache.value.completions) {
|
||||
const newRange = this.textModel.getDecorationRange(c.decorationId);
|
||||
if (!newRange) {
|
||||
onUnexpectedError(new Error('Decoration has no range'));
|
||||
continue;
|
||||
}
|
||||
if (!c.synchronizedRange.equalsRange(newRange)) {
|
||||
hasChanged = true;
|
||||
c.synchronizedRange = newRange;
|
||||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
}
|
||||
|
||||
this.scheduleAutomaticUpdate();
|
||||
}));
|
||||
|
||||
this.updateSoon.schedule();
|
||||
this.scheduleAutomaticUpdate();
|
||||
}
|
||||
|
||||
//#region Selection
|
||||
|
@ -200,41 +174,71 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
// We use a semantic id to track the selection even if the cache changes.
|
||||
private currentlySelectedCompletionId: string | undefined = undefined;
|
||||
|
||||
private getIndexOfCurrentSelection(): number {
|
||||
if (!this.currentlySelectedCompletionId || !this.cachedCompletions) {
|
||||
private fixAndGetIndexOfCurrentSelection(): number {
|
||||
if (!this.currentlySelectedCompletionId || !this.cache.value) {
|
||||
return 0;
|
||||
}
|
||||
if (this.cache.value.completions.length === 0) {
|
||||
// don't reset the selection in this case
|
||||
return 0;
|
||||
}
|
||||
|
||||
return this.cachedCompletions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId);
|
||||
const idx = this.cache.value.completions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId);
|
||||
if (idx === -1) {
|
||||
// Reset the selection so that the selection does not jump back when it appears again
|
||||
this.currentlySelectedCompletionId = undefined;
|
||||
return 0;
|
||||
}
|
||||
return idx;
|
||||
}
|
||||
|
||||
private get currentCachedCompletion(): CachedInlineCompletion | undefined {
|
||||
if (!this.cachedCompletions) {
|
||||
if (!this.cache.value) {
|
||||
return undefined;
|
||||
}
|
||||
return this.cachedCompletions[this.getIndexOfCurrentSelection()];
|
||||
return this.cache.value.completions[this.fixAndGetIndexOfCurrentSelection()];
|
||||
}
|
||||
|
||||
public showNextInlineCompletion(): void {
|
||||
if (this.cachedCompletions && this.cachedCompletions.length > 0) {
|
||||
const newIdx = (this.getIndexOfCurrentSelection() + 1) % this.cachedCompletions.length;
|
||||
this.currentlySelectedCompletionId = this.cachedCompletions[newIdx].semanticId;
|
||||
public async showNextInlineCompletion(): Promise<void> {
|
||||
await this.ensureUpdateWithExplicitContext();
|
||||
|
||||
const completions = this.cache.value?.completions || [];
|
||||
if (completions.length > 0) {
|
||||
const newIdx = (this.fixAndGetIndexOfCurrentSelection() + 1) % completions.length;
|
||||
this.currentlySelectedCompletionId = completions[newIdx].semanticId;
|
||||
} else {
|
||||
this.currentlySelectedCompletionId = undefined;
|
||||
}
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
|
||||
public showPreviousInlineCompletion(): void {
|
||||
if (this.cachedCompletions && this.cachedCompletions.length > 0) {
|
||||
const newIdx = (this.getIndexOfCurrentSelection() + this.cachedCompletions.length - 1) % this.cachedCompletions.length;
|
||||
this.currentlySelectedCompletionId = this.cachedCompletions[newIdx].semanticId;
|
||||
public async showPreviousInlineCompletion(): Promise<void> {
|
||||
await this.ensureUpdateWithExplicitContext();
|
||||
|
||||
const completions = this.cache.value?.completions || [];
|
||||
if (completions.length > 0) {
|
||||
const newIdx = (this.fixAndGetIndexOfCurrentSelection() + completions.length - 1) % completions.length;
|
||||
this.currentlySelectedCompletionId = completions[newIdx].semanticId;
|
||||
} else {
|
||||
this.currentlySelectedCompletionId = undefined;
|
||||
}
|
||||
this.onDidChangeEmitter.fire();
|
||||
}
|
||||
|
||||
private async ensureUpdateWithExplicitContext(): Promise<void> {
|
||||
if (this.updateOperation.value) {
|
||||
// Restart or wait for current update operation
|
||||
if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) {
|
||||
await this.updateOperation.value.promise;
|
||||
} else {
|
||||
await this.update(InlineCompletionTriggerKind.Explicit);
|
||||
}
|
||||
} else if (this.cache.value?.triggerKind !== InlineCompletionTriggerKind.Explicit) {
|
||||
// Refresh cache
|
||||
await this.update(InlineCompletionTriggerKind.Explicit);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
public get ghostText(): GhostText | undefined {
|
||||
|
@ -249,7 +253,7 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
return {
|
||||
text: completion.inlineCompletion.text,
|
||||
range: completion.lastRange,
|
||||
range: completion.synchronizedRange,
|
||||
command: completion.inlineCompletion.command,
|
||||
sourceProvider: completion.inlineCompletion.sourceProvider,
|
||||
sourceInlineCompletions: completion.inlineCompletion.sourceInlineCompletions,
|
||||
|
@ -261,61 +265,47 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
return this.editor.getPosition().lineNumber === this.triggerPosition.lineNumber;
|
||||
}
|
||||
|
||||
public scheduleUpdate(): void {
|
||||
public scheduleAutomaticUpdate(): void {
|
||||
this.updateSoon.schedule();
|
||||
}
|
||||
|
||||
private update(): void {
|
||||
private async update(triggerKind: InlineCompletionTriggerKind): Promise<void> {
|
||||
if (!this.shouldUpdate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const position = this.editor.getPosition();
|
||||
this.clearGhostTextPromise();
|
||||
this.updatePromise = createCancelablePromise(token =>
|
||||
provideInlineCompletions(position,
|
||||
this.editor.getModel(),
|
||||
{ triggerKind: InlineCompletionTriggerKind.Automatic },
|
||||
token
|
||||
)
|
||||
);
|
||||
this.updatePromise.then((result) => {
|
||||
this.cachedCompletions = [];
|
||||
const decorationIds = this.editor.deltaDecorations(
|
||||
(this.cachedCompletions || []).map(c => c.decorationId),
|
||||
(result.items).map(i => ({
|
||||
range: i.range,
|
||||
options: {
|
||||
description: 'inline-completion-tracking-range'
|
||||
},
|
||||
}))
|
||||
);
|
||||
|
||||
this.cachedCompletionsSource?.dispose();
|
||||
this.cachedCompletionsSource = result;
|
||||
this.cachedCompletions = result.items.map((item, idx) => new CachedInlineCompletion(item, decorationIds[idx]));
|
||||
this.onDidChangeEmitter.fire();
|
||||
}, onUnexpectedError);
|
||||
}
|
||||
|
||||
private clearCache(): void {
|
||||
const completions = this.cachedCompletions;
|
||||
if (completions) {
|
||||
this.cachedCompletions = undefined;
|
||||
this.editor.deltaDecorations(completions.map(c => c.decorationId), []);
|
||||
|
||||
if (!this.cachedCompletionsSource) {
|
||||
throw new Error('Unexpected state');
|
||||
const promise = createCancelablePromise(async token => {
|
||||
let result;
|
||||
try {
|
||||
result = await provideInlineCompletions(position,
|
||||
this.editor.getModel(),
|
||||
{ triggerKind },
|
||||
token
|
||||
);
|
||||
} catch (e) {
|
||||
onUnexpectedError(e);
|
||||
return;
|
||||
}
|
||||
this.cachedCompletionsSource.dispose();
|
||||
this.cachedCompletionsSource = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private clearGhostTextPromise(): void {
|
||||
if (this.updatePromise) {
|
||||
this.updatePromise.cancel();
|
||||
this.updatePromise = undefined;
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.cache.value = new SynchronizedInlineCompletionsCache(
|
||||
this.editor,
|
||||
result,
|
||||
() => this.onDidChangeEmitter.fire(),
|
||||
triggerKind
|
||||
);
|
||||
this.onDidChangeEmitter.fire();
|
||||
});
|
||||
const operation = new UpdateOperation(promise, triggerKind);
|
||||
this.updateOperation.value = operation;
|
||||
await promise;
|
||||
if (this.updateOperation.value === operation) {
|
||||
this.updateOperation.clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -331,7 +321,7 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
|
||||
public commit(completion: LiveInlineCompletion): void {
|
||||
this.clearCache();
|
||||
this.cache.clear();
|
||||
this.editor.executeEdits(
|
||||
'inlineCompletions.accept',
|
||||
[
|
||||
|
@ -346,6 +336,88 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
}
|
||||
|
||||
class UpdateOperation implements IDisposable {
|
||||
constructor(public readonly promise: CancelablePromise<void>, public readonly triggerKind: InlineCompletionTriggerKind) {
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.promise.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The cache keeps itself in sync with the editor.
|
||||
* It also owns the completions result and disposes it when the cache is diposed.
|
||||
*/
|
||||
class SynchronizedInlineCompletionsCache extends Disposable {
|
||||
public readonly completions: readonly CachedInlineCompletion[];
|
||||
|
||||
constructor(
|
||||
editor: IActiveCodeEditor,
|
||||
completionsSource: LiveInlineCompletions,
|
||||
onChange: () => void,
|
||||
public readonly triggerKind: InlineCompletionTriggerKind,
|
||||
) {
|
||||
super();
|
||||
|
||||
const decorationIds = editor.deltaDecorations(
|
||||
[],
|
||||
completionsSource.items.map(i => ({
|
||||
range: i.range,
|
||||
options: {
|
||||
description: 'inline-completion-tracking-range'
|
||||
},
|
||||
}))
|
||||
);
|
||||
this._register(toDisposable(() => {
|
||||
editor.deltaDecorations(decorationIds, []);
|
||||
}));
|
||||
|
||||
this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx]));
|
||||
|
||||
this._register(editor.onDidChangeModelContent(() => {
|
||||
let hasChanged = false;
|
||||
const model = editor.getModel();
|
||||
for (const c of this.completions) {
|
||||
const newRange = model.getDecorationRange(c.decorationId);
|
||||
if (!newRange) {
|
||||
onUnexpectedError(new Error('Decoration has no range'));
|
||||
continue;
|
||||
}
|
||||
if (!c.synchronizedRange.equalsRange(newRange)) {
|
||||
hasChanged = true;
|
||||
c.synchronizedRange = newRange;
|
||||
}
|
||||
}
|
||||
if (hasChanged) {
|
||||
onChange();
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(completionsSource);
|
||||
}
|
||||
}
|
||||
|
||||
class CachedInlineCompletion {
|
||||
public readonly semanticId: string = JSON.stringify({
|
||||
text: this.inlineCompletion.text,
|
||||
startLine: this.inlineCompletion.range.startLineNumber,
|
||||
startColumn: this.inlineCompletion.range.startColumn,
|
||||
command: this.inlineCompletion.command
|
||||
});
|
||||
/**
|
||||
* The range, synchronized with text model changes.
|
||||
*/
|
||||
public synchronizedRange: Range;
|
||||
|
||||
constructor(
|
||||
public readonly inlineCompletion: LiveInlineCompletion,
|
||||
public readonly decorationId: string,
|
||||
) {
|
||||
this.synchronizedRange = inlineCompletion.range;
|
||||
}
|
||||
}
|
||||
|
||||
export interface NormalizedInlineCompletion extends InlineCompletion {
|
||||
range: Range;
|
||||
}
|
||||
|
|
|
@ -83,7 +83,13 @@ export class SuggestWidgetAdapterModel extends BaseGhostTextWidgetModel {
|
|||
}
|
||||
|
||||
// TODO: item.isResolved
|
||||
this.setCurrentInlineCompletion(getInlineCompletion(suggestController, this.editor.getPosition(), focusedItem));
|
||||
this.setCurrentInlineCompletion(
|
||||
getInlineCompletion(
|
||||
suggestController,
|
||||
this.editor.getPosition(),
|
||||
focusedItem
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private setCurrentInlineCompletion(completion: NormalizedInlineCompletion | undefined): void {
|
||||
|
|
Loading…
Reference in a new issue