Skip to content
This repository was archived by the owner on Aug 27, 2024. It is now read-only.

Commit 2053a83

Browse files
committed
#18 Implement debounce with proper async support
1 parent 5048442 commit 2053a83

5 files changed

+173
-70
lines changed

src/FauxpilotCompletionProvider.ts

+32-47
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,35 @@
11
import { Configuration, CreateCompletionRequestPrompt, CreateCompletionResponse, OpenAIApi } from 'openai';
22
import { CancellationToken, InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, InlineCompletionList, Position, ProviderResult, Range, TextDocument, workspace } from 'vscode';
33
import { AxiosResponse } from 'axios';
4-
import { nextId } from './Uuid';
4+
import { debounce } from './utilities';
55

66
export class FauxpilotCompletionProvider implements InlineCompletionItemProvider {
7-
cachedPrompts: Map<string, number> = new Map<string, number>();
8-
7+
// TODO: Make dynamic
8+
// AFAIK VSCode creates provider once. As such, token will never be updated
99
private configuration: Configuration = new Configuration({
1010
apiKey: workspace.getConfiguration('fauxpilot').get("token")
1111
});
12+
// TODO: Make dynamic
13+
// AFAIK VSCode creates provider once. As such, server address will never be updated
1214
private openai: OpenAIApi = new OpenAIApi(this.configuration, `${workspace.getConfiguration('fauxpilot').get("server")}/${workspace.getConfiguration('fauxpilot').get("engine")}`);
13-
private request_status: string = "done";
15+
private readonly debouncedApiCall: any = debounce(
16+
// TODO: Extract to method.
17+
// I absolutely forgot how to handle 'this' context in JS. Simple extraction makes this
18+
// undefined. How to bind it?
19+
(prompt: string, position: Position) => {
20+
return new Promise(resolve => {
21+
console.debug("Requesting completion after debounce period");
22+
this.callOpenAi(prompt).then((response) => {
23+
resolve(this.toInlineCompletions(response.data, position));
24+
}).catch((error) => {
25+
console.error(error);
26+
resolve(([] as InlineCompletionItem[]));
27+
});
28+
});
29+
}, { timeout: workspace.getConfiguration('fauxpilot').get("suggestionDelay") as number, defaultReturn: [] });
30+
31+
constructor(private testCompletion?: any) {
32+
}
1433

1534
//@ts-ignore
1635
// because ASYNC and PROMISE
@@ -21,43 +40,14 @@ export class FauxpilotCompletionProvider implements InlineCompletionItemProvider
2140
}
2241

2342
const prompt = this.getPrompt(document, position);
24-
console.debug("Requesting completion for prompt", prompt);
2543

2644
if (this.isNil(prompt)) {
2745
console.debug("Prompt is empty, skipping");
2846
return Promise.resolve(([] as InlineCompletionItem[]));
2947
}
3048

31-
const currentTimestamp = Date.now();
32-
const currentId = nextId();
33-
this.cachedPrompts.set(currentId, currentTimestamp);
34-
35-
// check there is no newer request util this.request_status is done
36-
while (this.request_status === "pending") {
37-
await this.sleep(200);
38-
console.debug("current id = ", currentId, " request status = ", this.request_status);
39-
if (this.newestTimestamp() > currentTimestamp) {
40-
console.debug("newest timestamp=", this.newestTimestamp(), "current timestamp=", currentTimestamp);
41-
console.debug("Newer request is pending, skipping");
42-
this.cachedPrompts.delete(currentId);
43-
return Promise.resolve(([] as InlineCompletionItem[]));
44-
}
45-
}
46-
47-
console.debug("current id = ", currentId, "set request status to pending");
48-
this.request_status = "pending";
49-
return this.callOpenAi(prompt as String).then((response) => {
50-
console.debug("current id = ", currentId, "set request status to done");
51-
this.request_status = "done";
52-
this.cachedPrompts.delete(currentId);
53-
return this.toInlineCompletions(response.data, position);
54-
}).catch((error) => {
55-
console.debug("current id = ", currentId, "set request status to done");
56-
this.request_status = "done";
57-
this.cachedPrompts.delete(currentId);
58-
console.error(error);
59-
return ([] as InlineCompletionItem[]);
60-
});
49+
console.debug("Requesting completion for prompt", prompt);
50+
return this.debouncedApiCall(prompt, position);
6151
}
6252

6353
private getPrompt(document: TextDocument, position: Position): String | undefined {
@@ -72,27 +62,22 @@ export class FauxpilotCompletionProvider implements InlineCompletionItemProvider
7262
return value === undefined || value === null || value.length === 0;
7363
}
7464

75-
private newestTimestamp() {
76-
return Array.from(this.cachedPrompts.values()).reduce((a, b) => Math.max(a, b));
77-
}
78-
79-
private sleep(milliseconds: number) {
80-
return new Promise(r => setTimeout(r, milliseconds));
81-
};
82-
8365
private callOpenAi(prompt: String): Promise<AxiosResponse<CreateCompletionResponse, any>> {
8466
console.debug("Calling OpenAi", prompt);
8567

68+
// FIXME: I do not understand my own comment below. To verify
8669
//check if inline completion is enabled
87-
const stop_words = workspace.getConfiguration('fauxpilot').get("inlineCompletion") ? ["\n"] : [];
88-
console.debug("Calling OpenAi with stop words = ", stop_words);
89-
return this.openai.createCompletion({
70+
const stopWords = workspace.getConfiguration('fauxpilot').get("inlineCompletion") ? ["\n"] : [];
71+
console.debug("Calling OpenAi with stop words = ", stopWords);
72+
// FIXME: how to mock in mocha?
73+
// Current implementation works by injecting alternative provider via constructor
74+
return (this.testCompletion ?? this.openai).createCompletion({
9075
model: workspace.getConfiguration('fauxpilot').get("model") ?? "<<UNSET>>",
9176
prompt: prompt as CreateCompletionRequestPrompt,
9277
/* eslint-disable-next-line @typescript-eslint/naming-convention */
9378
max_tokens: workspace.getConfiguration('fauxpilot').get("maxTokens"),
9479
temperature: workspace.getConfiguration('fauxpilot').get("temperature"),
95-
stop: stop_words
80+
stop: stopWords
9681
});
9782
}
9883

src/Uuid.ts

-8
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import assert = require("assert");
2+
import { FauxpilotCompletionProvider } from "../../FauxpilotCompletionProvider";
3+
import { CancellationToken, InlineCompletionContext, InlineCompletionItem, InlineCompletionItemProvider, InlineCompletionList, Position, ProviderResult, Range, TextDocument, workspace } from 'vscode';
4+
import { AxiosResponse } from 'axios';
5+
import { CreateCompletionResponse } from "openai";
6+
7+
suite('provideInlineCompletionItems', () => {
8+
test('Normal completion', async () => {
9+
const provider = new FauxpilotCompletionProvider(testCompletion([{ text: "Example response" }]));
10+
const result = await provider.provideInlineCompletionItems(
11+
documentStub("Example prompt"),
12+
positionStub(),
13+
null as any,
14+
null as any
15+
);
16+
assert.equal((result as any)[0].insertText, "Example response");
17+
});
18+
test('Debounced completion', async () => {
19+
// Rewrite as before/after each
20+
let output: any[] = [];
21+
const originalLog = console.log;
22+
const originalDebug = console.debug;
23+
console.log = (message?: any, ...optional: any[]) => {
24+
output.push([message, ...optional]);
25+
originalLog(message, ...optional);
26+
};
27+
console.debug = (message?: any, ...optional: any[]) => {
28+
output.push([message, ...optional]);
29+
originalDebug(message, ...optional);
30+
};
31+
32+
const provider = new FauxpilotCompletionProvider(testCompletion([{ text: "Example response" }]));
33+
(provider.provideInlineCompletionItems(
34+
documentStub("Example prompt 1"),
35+
positionStub(),
36+
null as any,
37+
null as any
38+
) as Promise<any>).then(console.debug);
39+
const result = await provider.provideInlineCompletionItems(
40+
documentStub("Example prompt 2"),
41+
positionStub(),
42+
null as any,
43+
null as any
44+
);
45+
46+
console.debug = originalDebug;
47+
console.log = originalLog;
48+
49+
assert.equal((result as any)[0].insertText, "Example response");
50+
assert.deepEqual(output, [
51+
[
52+
"Requesting completion for prompt",
53+
"Example prompt 1"
54+
],
55+
[
56+
"Requesting completion for prompt",
57+
"Example prompt 2"
58+
],
59+
[
60+
"Resolved previous debounce with defaults"
61+
],
62+
[
63+
[]
64+
],
65+
[
66+
"Resolved debounce"
67+
],
68+
[
69+
"Requesting completion after debounce period"
70+
],
71+
[
72+
"Calling OpenAi",
73+
"Example prompt 2"
74+
],
75+
[
76+
"Calling OpenAi with stop words = ",
77+
["\n"]
78+
]
79+
]);
80+
});
81+
});
82+
83+
function positionStub(): Position {
84+
return {
85+
line: 0,
86+
character: 0
87+
} as any;
88+
}
89+
90+
function documentStub(out?: any): TextDocument {
91+
return {
92+
getText: () => out
93+
} as any;
94+
}
95+
96+
function testCompletion(choices: { text: string }[]) {
97+
return {
98+
createCompletion: async (params: any): Promise<AxiosResponse<CreateCompletionResponse, any>> => {
99+
console.warn("DEBUG COMPLETION", params);
100+
const result: CreateCompletionResponse = {
101+
choices
102+
};
103+
return {
104+
data: result
105+
} as AxiosResponse;
106+
}
107+
};
108+
}

src/test/suite/uuid.test.ts

-15
This file was deleted.

src/utilities.ts

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export function debounce<T>(fn: (ths: any, ...args: any[]) => Promise<T>,
2+
{ timeout = 300, defaultReturn }: { timeout: number, defaultReturn?: T }) {
3+
let timer: NodeJS.Timeout;
4+
let previousPromise: any;
5+
return async (...args: any[]) => {
6+
// Resolve any previous pending promises, so that we will never leave
7+
// them dangling
8+
// TODO: Extract debug logging wrapper
9+
previousPromise?.((() => {
10+
console.debug("Resolved previous debounce with defaults");
11+
return defaultReturn;
12+
})());
13+
clearTimeout(timer);
14+
return new Promise(resolve => {
15+
// Add previous promise, so that we can resolve it with empty upon the
16+
// next (debounced) call
17+
previousPromise = resolve;
18+
timer = setTimeout(() => {
19+
// TODO: Extract debug logging wrapper
20+
resolve((() => {
21+
console.debug("Resolved debounce");
22+
// Because we are actually calling the API, we must resolved
23+
// all previous debounced calls with empty, so we ensure that
24+
// there is no dangling resolved promise that would be called
25+
// during the next debounced call
26+
previousPromise = undefined;
27+
// @ts-ignore
28+
return fn.apply(this, args);
29+
})());
30+
}, timeout);
31+
});
32+
};
33+
}

0 commit comments

Comments
 (0)