Skip to content

Commit

Permalink
Merge pull request #124707 from microsoft/alex/ghost-text
Browse files Browse the repository at this point in the history
ghost text updates
  • Loading branch information
alexdima committed May 26, 2021
2 parents e0a52df + b6acb19 commit 641c2b1
Show file tree
Hide file tree
Showing 11 changed files with 289 additions and 84 deletions.
2 changes: 1 addition & 1 deletion src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3315,7 +3315,7 @@ class EditorSuggest extends BaseEditorOption<EditorOption.suggest, InternalSugge
showIcons: true,
showStatusBar: false,
showSuggestionPreview: false,
suggestionPreviewExpanded: false,
suggestionPreviewExpanded: true,
showInlineDetails: true,
showMethods: true,
showFunctions: true,
Expand Down
15 changes: 10 additions & 5 deletions src/vs/editor/common/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -718,27 +718,32 @@ export interface InlineCompletion {
* 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.
*/
text: string;
readonly text: string;

/**
* The range to replace.
* Must begin and end on the same line.
*/
range?: IRange;
readonly range?: IRange;

readonly command?: Command;
}

/**
* @internal
*/
export interface InlineCompletions<TItem extends InlineCompletion = InlineCompletion> {
items: TItem[];
readonly items: readonly TItem[];
}

/**
* @internal
*/
export interface InlineCompletionsProvider {
provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<InlineCompletions>;
export interface InlineCompletionsProvider<T extends InlineCompletions = InlineCompletions> {
provideInlineCompletions(model: model.ITextModel, position: Position, context: InlineCompletionContext, token: CancellationToken): ProviderResult<T>;

handleItemDidShow?(completions: T, item: T['items'][number]): void;
freeInlineCompletions(completions: T): void;
}

export interface CodeAction {
Expand Down
10 changes: 6 additions & 4 deletions src/vs/editor/contrib/inlineCompletions/ghostTextController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { GhostTextWidget } from 'vs/editor/contrib/inlineCompletions/ghostTextWi
import { InlineCompletionsModel } from 'vs/editor/contrib/inlineCompletions/inlineCompletionsModel';
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 { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';

Expand All @@ -31,7 +32,7 @@ class GhostTextController extends Disposable {

constructor(
private readonly editor: ICodeEditor,
@IInstantiationService instantiationService: IInstantiationService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
super();
Expand All @@ -55,7 +56,7 @@ class GhostTextController extends Disposable {

this.activeController.value = undefined;
this.activeController.value = this.editor.hasModel() && suggestOptions.showSuggestionPreview
? new ActiveGhostTextController(this.editor, this.widget, this.contextKeys)
? this.instantiationService.createInstance(ActiveGhostTextController, this.editor, this.widget, this.contextKeys)
: undefined;
}

Expand Down Expand Up @@ -92,12 +93,13 @@ class GhostTextContextKeys {
*/
export class ActiveGhostTextController extends Disposable {
private readonly suggestWidgetAdapterModel = new SuggestWidgetAdapterModel(this.editor);
private readonly inlineCompletionsModel = new InlineCompletionsModel(this.editor);
private readonly inlineCompletionsModel = new InlineCompletionsModel(this.editor, this.commandService);

constructor(
private readonly editor: IActiveCodeEditor,
private readonly widget: GhostTextWidget,
private readonly contextKeys: GhostTextContextKeys,
@ICommandService private readonly commandService: ICommandService,
) {
super();

Expand Down Expand Up @@ -226,7 +228,7 @@ export class ShowPreviousInlineCompletionAction extends EditorAction {
public async run(accessor: ServicesAccessor | undefined, editor: ICodeEditor): Promise<void> {
const controller = GhostTextController.get(editor);
if (controller) {
controller.showNextInlineCompletion();
controller.showPreviousInlineCompletion();
}
}
}
Expand Down
19 changes: 12 additions & 7 deletions src/vs/editor/contrib/inlineCompletions/ghostTextWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export class GhostTextWidget extends Disposable {
private decorationIds: string[] = [];
private viewZoneId: string | null = null;
private viewMoreContentWidget: ViewMoreLinesContentWidget | null = null;
private viewMoreContentWidget2: ViewMoreLinesContentWidget | null = null;

constructor(
private readonly editor: ICodeEditor,
Expand Down Expand Up @@ -191,11 +190,6 @@ export class GhostTextWidget extends Disposable {
this.viewMoreContentWidget = null;
}

if (this.viewMoreContentWidget2) {
this.viewMoreContentWidget2.dispose();
this.viewMoreContentWidget2 = null;
}

this.editor.changeViewZones((changeAccessor) => {
if (this.viewZoneId) {
changeAccessor.removeZone(this.viewZoneId);
Expand Down Expand Up @@ -348,9 +342,20 @@ class ViewMoreLinesContentWidget extends Disposable implements IContentWidget {
}

registerThemingParticipant((theme, collector) => {

const suggestPreviewForeground = theme.getColor(editorSuggestPreviewOpacity);

if (suggestPreviewForeground) {
collector.addRule(`.monaco-editor .suggest-preview-text { opacity: ${suggestPreviewForeground.rgba.a}; }`);
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))!;

// 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);
Expand Down
123 changes: 98 additions & 25 deletions src/vs/editor/contrib/inlineCompletions/inlineCompletionsModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@

import { CancelablePromise, createCancelablePromise, RunOnceScheduler } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { onUnexpectedError } from 'vs/base/common/errors';
import { onUnexpectedError, onUnexpectedExternalError } from 'vs/base/common/errors';
import { Emitter } from 'vs/base/common/event';
import { Disposable, IDisposable, MutableDisposable, toDisposable } from 'vs/base/common/lifecycle';
import * as strings from 'vs/base/common/strings';
import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
import { Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { ITextModel } from 'vs/editor/common/model';
import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes';
import { InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from 'vs/editor/common/modes';
import { BaseGhostTextWidgetModel, GhostText, GhostTextWidgetModel } from 'vs/editor/contrib/inlineCompletions/ghostTextWidget';
import { EditOperation } from 'vs/editor/common/core/editOperation';
import { ICommandService } from 'vs/platform/commands/common/commands';

export class InlineCompletionsModel extends Disposable implements GhostTextWidgetModel {
protected readonly onDidChangeEmitter = new Emitter<void>();
Expand All @@ -25,7 +26,10 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge

private active: boolean = false;

constructor(private readonly editor: IActiveCodeEditor) {
constructor(
private readonly editor: IActiveCodeEditor,
private readonly commandService: ICommandService
) {
super();

this._register(this.editor.onDidChangeModelContent((e) => {
Expand Down Expand Up @@ -83,7 +87,7 @@ export class InlineCompletionsModel extends Disposable implements GhostTextWidge
if (this.completionSession.value) {
return;
}
this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active);
this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService);
this.completionSession.value.takeOwnership(
this.completionSession.value.onDidChange(() => {
this.onDidChangeEmitter.fire();
Expand Down Expand Up @@ -120,7 +124,7 @@ class CachedInlineCompletion {
public lastRange: Range;

constructor(
public readonly inlineCompletion: NormalizedInlineCompletion,
public readonly inlineCompletion: LiveInlineCompletion,
public readonly decorationId: string,
) {
this.lastRange = inlineCompletion.range;
Expand All @@ -130,19 +134,38 @@ class CachedInlineCompletion {
class InlineCompletionsSession extends BaseGhostTextWidgetModel {
public readonly minReservedLineCount = 0;

private updatePromise: CancelablePromise<NormalizedInlineCompletions> | undefined = undefined;
private updatePromise: CancelablePromise<LiveInlineCompletions> | undefined = undefined;
private cachedCompletions: CachedInlineCompletion[] | undefined = undefined;
private cachedCompletionsSource: LiveInlineCompletions | undefined = undefined;

private updateSoon = this._register(new RunOnceScheduler(() => this.update(), 50));
private readonly textModel = this.editor.getModel();

constructor(editor: IActiveCodeEditor, private readonly triggerPosition: Position, private readonly shouldUpdate: () => boolean) {
constructor(
editor: IActiveCodeEditor,
private readonly triggerPosition: Position,
private readonly shouldUpdate: () => boolean,
private readonly commandService: ICommandService,
) {
super(editor);
this._register(toDisposable(() => {
this.clearGhostTextPromise();
this.clearCache();
}));

let lastCompletionItem: InlineCompletion | undefined = undefined;
this._register(this.onDidChange(() => {
const currentCompletion = this.currentCompletion;
if (currentCompletion && currentCompletion.sourceInlineCompletion !== lastCompletionItem) {
lastCompletionItem = currentCompletion.sourceInlineCompletion;

const provider = currentCompletion.sourceProvider;
if (provider.handleItemDidShow) {
provider.handleItemDidShow(currentCompletion.sourceInlineCompletions, lastCompletionItem);
}
}
}));

this._register(this.editor.onDidChangeModelDecorations(e => {
if (!this.cachedCompletions) {
return;
Expand Down Expand Up @@ -219,14 +242,18 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel()) : undefined;
}

get currentCompletion(): NormalizedInlineCompletion | undefined {
get currentCompletion(): LiveInlineCompletion | undefined {
const completion = this.currentCachedCompletion;
if (!completion) {
return undefined;
}
return {
text: completion.inlineCompletion.text,
range: completion.lastRange
range: completion.lastRange,
command: completion.inlineCompletion.command,
sourceProvider: completion.inlineCompletion.sourceProvider,
sourceInlineCompletions: completion.inlineCompletion.sourceInlineCompletions,
sourceInlineCompletion: completion.inlineCompletion.sourceInlineCompletion,
};
}

Expand Down Expand Up @@ -262,15 +289,24 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
}))
);

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 {
if (this.cachedCompletions) {
this.editor.deltaDecorations(this.cachedCompletions.map(c => c.decorationId), []);
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');
}
this.cachedCompletionsSource.dispose();
this.cachedCompletionsSource = undefined;
}
}

Expand All @@ -292,14 +328,18 @@ class InlineCompletionsSession extends BaseGhostTextWidgetModel {
}
}

public commit(completion: NormalizedInlineCompletion): void {
public commit(completion: LiveInlineCompletion): void {
this.clearCache();
this.editor.executeEdits(
'inlineCompletions.accept',
[
EditOperation.replaceMove(completion.range, completion.text)
]
);
if (completion.command) {
this.commandService.executeCommand(completion.command.id, ...(completion.command.arguments || [])).then(undefined, onUnexpectedExternalError);
}

this.onDidChangeEmitter.fire();
}
}
Expand All @@ -308,12 +348,6 @@ export interface NormalizedInlineCompletion extends InlineCompletion {
range: Range;
}

/**
* Contains no duplicated items.
*/
export interface NormalizedInlineCompletions extends InlineCompletions<NormalizedInlineCompletion> {
}

export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCompletion, textModel: ITextModel): GhostText | undefined {
const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
if (!inlineCompletion.text.startsWith(valueToBeReplaced)) {
Expand All @@ -328,6 +362,20 @@ export function inlineCompletionToGhostText(inlineCompletion: NormalizedInlineCo
};
}

export interface LiveInlineCompletion extends InlineCompletion {
range: Range;
sourceProvider: InlineCompletionsProvider;
sourceInlineCompletion: InlineCompletion;
sourceInlineCompletions: InlineCompletions;
}

/**
* Contains no duplicated items.
*/
export interface LiveInlineCompletions extends InlineCompletions<LiveInlineCompletion> {
dispose(): void;
}

function getDefaultRange(position: Position, model: ITextModel): Range {
const word = model.getWordAtPosition(position);
const maxColumn = model.getLineMaxColumn(position.lineNumber);
Expand All @@ -343,25 +391,50 @@ async function provideInlineCompletions(
model: ITextModel,
context: InlineCompletionContext,
token: CancellationToken = CancellationToken.None
): Promise<NormalizedInlineCompletions> {
): Promise<LiveInlineCompletions> {
const defaultReplaceRange = getDefaultRange(position, model);

const providers = InlineCompletionsProviderRegistry.all(model);
const results = await Promise.all(
providers.map(provider => provider.provideInlineCompletions(model, position, context, token))
providers.map(
async provider => {
const completions = await provider.provideInlineCompletions(model, position, context, token);
return ({
completions,
provider,
dispose: () => {
if (completions) {
provider.freeInlineCompletions(completions);
}
}
});
}
)
);

const itemsByHash = new Map<string, NormalizedInlineCompletion>();
const itemsByHash = new Map<string, LiveInlineCompletion>();
for (const result of results) {
if (result) {
for (const item of result.items.map<NormalizedInlineCompletion>(item => ({
const completions = result.completions;
if (completions) {
for (const item of completions.items.map<LiveInlineCompletion>(item => ({
text: item.text,
range: item.range ? Range.lift(item.range) : defaultReplaceRange
range: item.range ? Range.lift(item.range) : defaultReplaceRange,
command: item.command,
sourceProvider: result.provider,
sourceInlineCompletions: completions,
sourceInlineCompletion: item
}))) {
itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item);
}
}
}

return { items: [...itemsByHash.values()] };
return {
items: [...itemsByHash.values()],
dispose: () => {
for (const result of results) {
result.dispose();
}
},
};
}

0 comments on commit 641c2b1

Please sign in to comment.