Skip to content

Commit

Permalink
feat(agent): add context support for auto complete widget in InlineCo…
Browse files Browse the repository at this point in the history
…mpletion (#3362)

* feat(codeCompletion): add support for auto complete widget in CompletionContext

* feat(codeCompletion): improve auto complete widget handling in CompletionContext

* fix: update minCompletionChars to 4

* chore(codeCompletion): remove some comments and logger call

* feat(codeCompletion): optimize completion item filtering in CompletionContext
  • Loading branch information
Sma1lboy authored Nov 18, 2024
1 parent d260d12 commit 500ae05
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 23 deletions.
85 changes: 78 additions & 7 deletions clients/tabby-agent/src/codeCompletion/contexts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ export type CompletionRequest = {
declarations?: Declaration[];
relevantSnippetsFromChangedFiles?: CodeSnippet[];
relevantSnippetsFromOpenedFiles?: CodeSnippet[];
//auto complete part
autoComplete?: {
completionItem?: string;
insertPosition?: number;
insertSeg?: string;
currSeg?: string;
};
};

export type Declaration = {
Expand Down Expand Up @@ -76,25 +83,38 @@ export class CompletionContext {
mode: "default" | "fill-in-line";
hash: string;

// example of auto complete part
// cons| -> console
// completionItem: console
// insertPosition: 4
// insertSeg: ole
// currSeg: cons
completionItem: string = "";
insertPosition: number = 0;
insertSeg: string = "";
currSeg: string = "";
withCorrectCompletionItem: boolean = false; // weather we are using completionItem or not

constructor(request: CompletionRequest) {
this.filepath = request.filepath;
this.language = request.language;
this.text = request.text;
this.position = request.position;
this.indentation = request.indentation;
this.position = request.position;
this.text = request.text;
this.prefix = this.text.slice(0, this.position);
this.suffix = this.text.slice(this.position);

if (request.autoComplete?.completionItem) {
this.handleAutoComplete(request);
}

this.prefix = request.text.slice(0, request.position);
this.suffix = request.text.slice(request.position);
this.prefixLines = splitLines(this.prefix);
this.suffixLines = splitLines(this.suffix);
this.currentLinePrefix = this.prefixLines[this.prefixLines.length - 1] ?? "";
this.currentLineSuffix = this.suffixLines[0] ?? "";

this.clipboard = request.clipboard?.trim() ?? "";

this.workspace = request.workspace;
this.git = request.git;

this.declarations = request.declarations;
this.relevantSnippetsFromChangedFiles = request.relevantSnippetsFromChangedFiles;
this.snippetsFromOpenedFiles = request.relevantSnippetsFromOpenedFiles;
Expand All @@ -111,6 +131,10 @@ export class CompletionContext {
clipboard: this.clipboard,
declarations: this.declarations,
relevantSnippetsFromChangedFiles: this.relevantSnippetsFromChangedFiles,
completionItem: this.completionItem,
insertPosition: this.insertPosition,
insertSeg: this.insertSeg,
currSeg: this.currSeg,
});
}

Expand All @@ -121,6 +145,7 @@ export class CompletionContext {

// Generate a CompletionContext based on this CompletionContext.
// Simulate as if the user input new text based on this CompletionContext.
// FIXME: generate the context according to `selectedCompletionInfo`
forward(delta: string) {
return new CompletionContext({
filepath: this.filepath,
Expand All @@ -132,9 +157,55 @@ export class CompletionContext {
git: this.git,
declarations: this.declarations,
relevantSnippetsFromChangedFiles: this.relevantSnippetsFromChangedFiles,
relevantSnippetsFromOpenedFiles: this.snippetsFromOpenedFiles,
autoComplete: {
completionItem: this.completionItem,
insertPosition: this.insertPosition,
insertSeg: this.insertSeg,
currSeg: this.currSeg,
},
});
}

/**
* The method handles the auto complete part of the completion request.
* @param request completion request
* @returns void
*/
private handleAutoComplete(request: CompletionRequest): void {
if (!request.autoComplete?.completionItem) return;
this.completionItem = request.autoComplete.completionItem;
this.currSeg = request.autoComplete.currSeg ?? "";
this.insertSeg = request.autoComplete.insertSeg ?? "";

// check if the completion item is the same as the insert segment
if (!this.completionItem.startsWith(this.insertSeg)) {
return;
}

const prefixText = request.text.slice(0, request.position);
const lastIndex = prefixText.lastIndexOf(this.currSeg);

if (lastIndex !== -1) {
this.insertPosition = lastIndex + this.currSeg.length;

this.text = request.text.slice(0, lastIndex) + this.completionItem + request.text.slice(this.insertPosition);

this.position = lastIndex + this.completionItem.length;

this.prefix = this.text.slice(0, this.position);
this.suffix = this.text.slice(this.position);
this.withCorrectCompletionItem = true;
}
}
isWithCorrectAutoComplete(): boolean {
return this.withCorrectCompletionItem;
}

getFullCompletionItem(): string | null {
return this.isWithCorrectAutoComplete() ? this.completionItem : null;
}

// Build segments for TabbyApi
buildSegments(config: ConfigData["completion"]["prompt"]): TabbyApiComponents["schemas"]["Segments"] {
// prefix && suffix
Expand Down
26 changes: 25 additions & 1 deletion clients/tabby-agent/src/codeCompletion/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,30 @@ export class CompletionProvider implements Feature {
return null;
}
result.request.manually = params.context?.triggerKind === InlineCompletionTriggerKind.Invoked;

if (params.context.selectedCompletionInfo) {
const customContext = params.context as {
triggerKind: InlineCompletionTriggerKind;
selectedCompletionInfo?: {
range: [{ line: number; character: number }, { line: number; character: number }];
text: string;
};
};
if (!customContext.selectedCompletionInfo) {
return result;
}
const info = customContext.selectedCompletionInfo;

const rangeLength = info.range[1].character - info.range[0].character;
const currentText = info.text.substring(0, rangeLength);

result.request.autoComplete = {
completionItem: info.text,
insertSeg: info.text.slice(currentText.length),
currSeg: currentText,
};
this.logger.debug("received AutoCompleteWidgetItem: " + JSON.stringify(params.context.selectedCompletionInfo));
}
return result;
}

Expand Down Expand Up @@ -407,8 +431,8 @@ export class CompletionProvider implements Feature {
},
signals,
);

solution = new CompletionSolution(context);

// Fetch the completion
this.logger.info(`Fetching completion...`);
try {
Expand Down
34 changes: 25 additions & 9 deletions clients/tabby-agent/src/codeCompletion/solution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ export class CompletionItem {
readonly currentLine: string; // first item of `lines`
readonly isBlank: boolean; // whether the item is a blank line.

readonly processedText: string; // text to be inserted.
readonly processedRange: { start: number; end: number }; // range to be replaced.

constructor(
// The context which the completion was generated for.
readonly context: CompletionContext,
Expand All @@ -54,6 +57,25 @@ export class CompletionItem {
this.lines = splitLines(this.text);
this.currentLine = this.lines[0] ?? "";
this.isBlank = isBlank(this.text);

// if with auto complete item, insert the completion item with the predicted text
if (this.context.isWithCorrectAutoComplete()) {
this.processedText = this.context.insertSeg + this.fullText;
this.processedRange = {
start: this.context.insertPosition,
end: this.context.insertPosition,
};
} else {
this.processedText = this.fullText;
this.processedRange = {
start: this.context.currentLinePrefix.endsWith(this.replacePrefix)
? this.context.position - this.replacePrefix.length
: this.context.position,
end: this.context.currentLineSuffix.startsWith(this.replaceSuffix)
? this.context.position + this.replaceSuffix.length
: this.context.position,
};
}
}

static createBlankItem(context: CompletionContext): CompletionItem {
Expand Down Expand Up @@ -106,6 +128,7 @@ export class CompletionItem {
forward(chars: number): CompletionItem {
if (chars <= 0) return this;
const delta = this.text.substring(0, chars);

// Forward in the current line
if (chars < this.currentLine.length) {
return new CompletionItem(
Expand Down Expand Up @@ -141,15 +164,8 @@ export class CompletionItem {

toInlineCompletionItem(): InlineCompletionItem {
return {
insertText: this.fullText,
range: {
start: this.context.currentLinePrefix.endsWith(this.replacePrefix)
? this.context.position - this.replacePrefix.length
: this.context.position,
end: this.context.currentLineSuffix.startsWith(this.replaceSuffix)
? this.context.position + this.replaceSuffix.length
: this.context.position,
},
insertText: this.processedText,
range: this.processedRange,
data: { eventId: this.eventId },
};
}
Expand Down
6 changes: 0 additions & 6 deletions clients/vscode/src/InlineCompletionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,6 @@ export class InlineCompletionProvider extends EventEmitter implements InlineComp
return null;
}

// Check if autocomplete widget is visible
if (context.selectedCompletionInfo !== undefined) {
this.logger.debug("Autocomplete widget is visible, skipping.");
return null;
}

if (token.isCancellationRequested) {
this.logger.debug("Completion request is canceled before send request.");
return null;
Expand Down

0 comments on commit 500ae05

Please sign in to comment.