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
parent
238d81ad4f
commit
c51e00ee45
|
|
@ -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",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue