feat(agent): add experimental option: scope of indentation filter. (#652)

* feat(agent): add experimental option: scope of indentation filter.

* fix: add config to fix unit test for limitScopeByIndentation.
release-notes-05
Zhiming Ma 2023-10-30 10:59:09 +08:00 committed by GitHub
parent 238d81ad4f
commit c51e00ee45
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 130 additions and 21 deletions

View File

@ -21,6 +21,14 @@ export type AgentConfig = {
manually: number; manually: number;
}; };
}; };
postprocess: {
limitScopeByIndentation: {
// When completion is continuing the current line, limit the scope to:
// false(default): the line scope, meaning use the next indent level as the limit.
// true: the block scope, meaning use the current indent level as the limit.
experimentalKeepBlockScopeWhenCompletingLine: boolean;
};
};
logs: { logs: {
level: "debug" | "error" | "silent"; level: "debug" | "error" | "silent";
}; };
@ -61,6 +69,11 @@ export const defaultAgentConfig: AgentConfig = {
manually: 4000, // 4s manually: 4000, // 4s
}, },
}, },
postprocess: {
limitScopeByIndentation: {
experimentalKeepBlockScopeWhenCompletingLine: false,
},
},
logs: { logs: {
level: "silent", level: "silent",
}, },

View File

@ -545,7 +545,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
throw error; throw error;
} }
// Postprocess (pre-cache) // Postprocess (pre-cache)
completionResponse = await preCacheProcess(context, completionResponse); completionResponse = await preCacheProcess(context, this.config.postprocess, completionResponse);
if (options?.signal?.aborted) { if (options?.signal?.aborted) {
throw options.signal.reason; throw options.signal.reason;
} }
@ -554,7 +554,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
} }
} }
// Postprocess (post-cache) // Postprocess (post-cache)
completionResponse = await postCacheProcess(context, completionResponse); completionResponse = await postCacheProcess(context, this.config.postprocess, completionResponse);
if (options?.signal?.aborted) { if (options?.signal?.aborted) {
throw options.signal.reason; throw options.signal.reason;
} }

View File

@ -1,4 +1,5 @@
import { CompletionContext, CompletionResponse } from "../Agent"; import { CompletionContext, CompletionResponse } from "../Agent";
import { AgentConfig } from "../AgentConfig";
import { applyFilter } from "./base"; import { applyFilter } from "./base";
import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks";
import { removeRepetitiveLines } from "./removeRepetitiveLines"; import { removeRepetitiveLines } from "./removeRepetitiveLines";
@ -10,6 +11,7 @@ import { dropBlank } from "./dropBlank";
export async function preCacheProcess( export async function preCacheProcess(
context: CompletionContext, context: CompletionContext,
config: AgentConfig["postprocess"],
response: CompletionResponse, response: CompletionResponse,
): Promise<CompletionResponse> { ): Promise<CompletionResponse> {
return Promise.resolve(response) return Promise.resolve(response)
@ -21,12 +23,13 @@ export async function preCacheProcess(
export async function postCacheProcess( export async function postCacheProcess(
context: CompletionContext, context: CompletionContext,
config: AgentConfig["postprocess"],
response: CompletionResponse, response: CompletionResponse,
): Promise<CompletionResponse> { ): Promise<CompletionResponse> {
return Promise.resolve(response) return Promise.resolve(response)
.then(applyFilter(removeRepetitiveBlocks(context), context)) .then(applyFilter(removeRepetitiveBlocks(context), context))
.then(applyFilter(removeRepetitiveLines(context), context)) .then(applyFilter(removeRepetitiveLines(context), context))
.then(applyFilter(limitScopeByIndentation(context), context)) .then(applyFilter(limitScopeByIndentation(context, config["limitScopeByIndentation"]), context))
.then(applyFilter(dropDuplicated(context), context)) .then(applyFilter(dropDuplicated(context), context))
.then(applyFilter(trimSpace(context), context)) .then(applyFilter(trimSpace(context), context))
.then(applyFilter(dropBlank(), context)); .then(applyFilter(dropBlank(), context));

View File

@ -3,7 +3,10 @@ import { documentContext, inline } from "./testUtils";
import { limitScopeByIndentation } from "./limitScopeByIndentation"; import { limitScopeByIndentation } from "./limitScopeByIndentation";
describe("postprocess", () => { describe("postprocess", () => {
describe("limitScopeByIndentation", () => { describe("limitScopeByIndentation: default config", () => {
let limitScopeByIndentationDefault = (context) => {
return limitScopeByIndentation(context, { experimentalKeepBlockScopeWhenCompletingLine: false });
};
it("should drop multiline completions, when the suffix have meaningful chars in the current line.", () => { it("should drop multiline completions, when the suffix have meaningful chars in the current line.", () => {
const context = { const context = {
...documentContext` ...documentContext`
@ -16,7 +19,7 @@ describe("postprocess", () => {
message); message);
throw error; throw error;
`; `;
expect(limitScopeByIndentation(context)(completion)).to.be.null; expect(limitScopeByIndentationDefault(context)(completion)).to.be.null;
}); });
it("should allow singleline completions, when the suffix have meaningful chars in the current line.", () => { it("should allow singleline completions, when the suffix have meaningful chars in the current line.", () => {
@ -30,7 +33,7 @@ describe("postprocess", () => {
const completion = inline` const completion = inline`
error, error,
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(completion); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(completion);
}); });
it("should allow multiline completions, when the suffix only have auto-closed chars that will be replaced in the current line, such as `)]}`.", () => { it("should allow multiline completions, when the suffix only have auto-closed chars that will be replaced in the current line, such as `)]}`.", () => {
@ -51,7 +54,7 @@ describe("postprocess", () => {
return max; return max;
} }
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(completion); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(completion);
}); });
it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => { it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => {
@ -68,7 +71,7 @@ describe("postprocess", () => {
const expected = inline` const expected = inline`
1; 1;
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => { it("should limit scope at sentence end, when completion is continuing uncompleted sentence in the prefix.", () => {
@ -96,10 +99,10 @@ describe("postprocess", () => {
const expected = inline` const expected = inline`
("Parsing", { json }); ("Parsing", { json });
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should limit scope at next indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => { it("should limit scope at next indent level, including closing line, when completion is starting a new indent level in next line.", () => {
const context = { const context = {
...documentContext` ...documentContext`
function findMax(arr) {} function findMax(arr) {}
@ -129,7 +132,7 @@ describe("postprocess", () => {
return max; return max;
} }
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should limit scope at next indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => { it("should limit scope at next indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => {
@ -160,7 +163,7 @@ describe("postprocess", () => {
} }
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should limit scope at current indent level, exclude closing line, when completion starts new sentences at same indent level.", () => { it("should limit scope at current indent level, exclude closing line, when completion starts new sentences at same indent level.", () => {
@ -192,7 +195,7 @@ describe("postprocess", () => {
return max; return max;
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should allow only one level closing bracket", () => { it("should allow only one level closing bracket", () => {
@ -216,7 +219,7 @@ describe("postprocess", () => {
} }
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
}); });
it("should allow level closing bracket at current line, it looks same as starts new sentences", () => { it("should allow level closing bracket at current line, it looks same as starts new sentences", () => {
@ -231,7 +234,7 @@ describe("postprocess", () => {
const completion = inline` const completion = inline`
} }
`; `;
expect(limitScopeByIndentation(context)(completion)).to.be.eq(completion); expect(limitScopeByIndentationDefault(context)(completion)).to.be.eq(completion);
}); });
it("should not allow level closing bracket, when the suffix lines have same indent level", () => { it("should not allow level closing bracket, when the suffix lines have same indent level", () => {
@ -250,7 +253,7 @@ describe("postprocess", () => {
`; `;
const expected = inline` const expected = inline`
`; `;
expect(limitScopeByIndentation(context)(completion)).to.be.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.be.eq(expected);
}); });
it("should use indent level of previous line, when current line is empty.", () => { it("should use indent level of previous line, when current line is empty.", () => {
@ -277,7 +280,88 @@ describe("postprocess", () => {
return null; return null;
`; `;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected); expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
});
});
describe("limitScopeByIndentation: with experimentalKeepBlockScopeWhenCompletingLine on", () => {
let limitScopeByIndentationKeepBlock = (context) => {
return limitScopeByIndentation(context, { experimentalKeepBlockScopeWhenCompletingLine: true });
};
it("should limit scope at block end, when completion is continuing uncompleted sentence in the prefix.", () => {
const context = {
...documentContext`
let a =
`,
language: "javascript",
};
const completion = inline`
1;
let b = 2;
`;
expect(limitScopeByIndentationKeepBlock(context)(completion)).to.eq(completion);
});
it("should limit scope at block end, when completion is continuing uncompleted sentence in the prefix.", () => {
const context = {
...documentContext`
function safeParse(json) {
try {
console.log
}
}
`,
language: "javascript",
};
const completion = inline`
("Parsing", { json });
return JSON.parse(json);
} catch (e) {
return null;
}
}
`;
const expected = inline`
("Parsing", { json });
return JSON.parse(json);
} catch (e) {
return null;
`;
expect(limitScopeByIndentationKeepBlock(context)(completion)).to.eq(expected);
});
it("should limit scope at same indent level, including closing line, when completion is continuing uncompleted sentence in the prefix, and starting a new indent level in next line.", () => {
const context = {
...documentContext`
function findMax(arr) {
let max = arr[0];
for
}
`,
language: "javascript",
};
const completion = inline`
(let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
console.log(findMax([1, 2, 3, 4, 5]));
`;
const expected = inline`
(let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
`;
expect(limitScopeByIndentationKeepBlock(context)(completion)).to.eq(expected);
}); });
}); });
}); });

View File

@ -1,4 +1,5 @@
import { CompletionContext } from "../Agent"; import { CompletionContext } from "../Agent";
import { AgentConfig } from "../AgentConfig";
import { PostprocessFilter, logger } from "./base"; import { PostprocessFilter, logger } from "./base";
import { isBlank, splitLines } from "../utils"; import { isBlank, splitLines } from "../utils";
@ -17,6 +18,7 @@ function processContext(
lines: string[], lines: string[],
prefixLines: string[], prefixLines: string[],
suffixLines: string[], suffixLines: string[],
config: AgentConfig["postprocess"]["limitScopeByIndentation"],
): { indentLevelLimit: number; allowClosingLine: (closingLine: string) => boolean } { ): { indentLevelLimit: number; allowClosingLine: (closingLine: string) => boolean } {
let allowClosingLine = false; let allowClosingLine = false;
let result = { indentLevelLimit: 0, allowClosingLine: (closingLine: string) => allowClosingLine }; let result = { indentLevelLimit: 0, allowClosingLine: (closingLine: string) => allowClosingLine };
@ -57,7 +59,11 @@ function processContext(
if (!isCurrentLineInCompletionBlank && !isCurrentLineInPrefixBlank) { if (!isCurrentLineInCompletionBlank && !isCurrentLineInPrefixBlank) {
// if two reference lines are contacted at current line, it is continuing uncompleted sentence // if two reference lines are contacted at current line, it is continuing uncompleted sentence
result.indentLevelLimit = referenceLineInPrefixIndent + 1; // + 1 for comparison, no matter how many spaces indent if (config.experimentalKeepBlockScopeWhenCompletingLine) {
result.indentLevelLimit = referenceLineInPrefixIndent;
} else {
result.indentLevelLimit = referenceLineInPrefixIndent + 1; // + 1 for comparison, no matter how many spaces indent
}
// allow closing line if first line is opening a new indent block // allow closing line if first line is opening a new indent block
allowClosingLine = !!lines[1] && calcIndentLevel(lines[1]) > referenceLineInPrefixIndent; allowClosingLine = !!lines[1] && calcIndentLevel(lines[1]) > referenceLineInPrefixIndent;
} else if (referenceLineInCompletionIndent > referenceLineInPrefixIndent) { } else if (referenceLineInCompletionIndent > referenceLineInPrefixIndent) {
@ -95,7 +101,10 @@ function processContext(
return result; return result;
} }
export const limitScopeByIndentation: (context: CompletionContext) => PostprocessFilter = (context) => { export function limitScopeByIndentation(
context: CompletionContext,
config: AgentConfig["postprocess"]["limitScopeByIndentation"],
): PostprocessFilter {
return (input) => { return (input) => {
const { prefix, suffix, prefixLines, suffixLines } = context; const { prefix, suffix, prefixLines, suffixLines } = context;
const inputLines = splitLines(input); const inputLines = splitLines(input);
@ -105,7 +114,7 @@ export const limitScopeByIndentation: (context: CompletionContext) => Postproces
return null; return null;
} }
} }
const indentContext = processContext(inputLines, prefixLines, suffixLines); const indentContext = processContext(inputLines, prefixLines, suffixLines, config);
let index; let index;
for (index = 1; index < inputLines.length; index++) { for (index = 1; index < inputLines.length; index++) {
if (isBlank(inputLines[index])) { if (isBlank(inputLines[index])) {
@ -135,4 +144,4 @@ export const limitScopeByIndentation: (context: CompletionContext) => Postproces
} }
return input; return input;
}; };
}; }