Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions clients/tabby-agent/src/AgentConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ export type AgentConfig = {
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: {
level: "debug" | "error" | "silent";
};
Expand Down Expand Up @@ -59,6 +67,11 @@ export const defaultAgentConfig: AgentConfig = {
manually: 4000, // 4s
},
},
postprocess: {
limitScopeByIndentation: {
experimentalKeepBlockScopeWhenCompletingLine: false,
},
},
logs: {
level: "silent",
},
Expand Down
4 changes: 2 additions & 2 deletions clients/tabby-agent/src/TabbyAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
throw error;
}
// Postprocess (pre-cache)
completionResponse = await preCacheProcess(context, completionResponse);
completionResponse = await preCacheProcess(context, this.config.postprocess, completionResponse);
if (options?.signal?.aborted) {
throw options.signal.reason;
}
Expand All @@ -550,7 +550,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
}
}
// Postprocess (post-cache)
completionResponse = await postCacheProcess(context, completionResponse);
completionResponse = await postCacheProcess(context, this.config.postprocess, completionResponse);
if (options?.signal?.aborted) {
throw options.signal.reason;
}
Expand Down
5 changes: 4 additions & 1 deletion clients/tabby-agent/src/postprocess/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CompletionContext, CompletionResponse } from "../Agent";
import { AgentConfig } from "../AgentConfig";
import { applyFilter } from "./base";
import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks";
import { removeRepetitiveLines } from "./removeRepetitiveLines";
Expand All @@ -10,6 +11,7 @@ import { dropBlank } from "./dropBlank";

export async function preCacheProcess(
context: CompletionContext,
config: AgentConfig["postprocess"],
response: CompletionResponse,
): Promise<CompletionResponse> {
return Promise.resolve(response)
Expand All @@ -21,12 +23,13 @@ export async function preCacheProcess(

export async function postCacheProcess(
context: CompletionContext,
config: AgentConfig["postprocess"],
response: CompletionResponse,
): Promise<CompletionResponse> {
return Promise.resolve(response)
.then(applyFilter(removeRepetitiveBlocks(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(trimSpace(context), context))
.then(applyFilter(dropBlank(), context));
Expand Down
112 changes: 98 additions & 14 deletions clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { documentContext, inline } from "./testUtils";
import { limitScopeByIndentation } from "./limitScopeByIndentation";

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.", () => {
const context = {
...documentContext`
Expand All @@ -16,7 +19,7 @@ describe("postprocess", () => {
├message);
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.", () => {
Expand All @@ -30,7 +33,7 @@ describe("postprocess", () => {
const completion = inline`
├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 `)]}`.", () => {
Expand All @@ -51,7 +54,7 @@ describe("postprocess", () => {
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.", () => {
Expand All @@ -68,7 +71,7 @@ describe("postprocess", () => {
const expected = inline`
├ 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.", () => {
Expand Down Expand Up @@ -96,10 +99,10 @@ describe("postprocess", () => {
const expected = inline`
├("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 = {
...documentContext`
function findMax(arr) {║}
Expand Down Expand Up @@ -129,7 +132,7 @@ describe("postprocess", () => {
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.", () => {
Expand Down Expand Up @@ -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.", () => {
Expand Down Expand Up @@ -192,7 +195,7 @@ describe("postprocess", () => {
return max;┤
┴┴
`;
expect(limitScopeByIndentation(context)(completion)).to.eq(expected);
expect(limitScopeByIndentationDefault(context)(completion)).to.eq(expected);
});

it("should allow only one level closing bracket", () => {
Expand All @@ -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", () => {
Expand All @@ -231,7 +234,7 @@ describe("postprocess", () => {
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", () => {
Expand All @@ -250,7 +253,7 @@ describe("postprocess", () => {
`;
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.", () => {
Expand All @@ -277,7 +280,88 @@ describe("postprocess", () => {
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);
});
});
});
17 changes: 13 additions & 4 deletions clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CompletionContext } from "../Agent";
import { AgentConfig } from "../AgentConfig";
import { PostprocessFilter, logger } from "./base";
import { isBlank, splitLines } from "../utils";

Expand All @@ -17,6 +18,7 @@ function processContext(
lines: string[],
prefixLines: string[],
suffixLines: string[],
config: AgentConfig["postprocess"]["limitScopeByIndentation"],
): { indentLevelLimit: number; allowClosingLine: (closingLine: string) => boolean } {
let allowClosingLine = false;
let result = { indentLevelLimit: 0, allowClosingLine: (closingLine: string) => allowClosingLine };
Expand Down Expand Up @@ -57,7 +59,11 @@ function processContext(
if (!isCurrentLineInCompletionBlank && !isCurrentLineInPrefixBlank) {
// 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
allowClosingLine = !!lines[1] && calcIndentLevel(lines[1]) > referenceLineInPrefixIndent;
} else if (referenceLineInCompletionIndent > referenceLineInPrefixIndent) {
Expand Down Expand Up @@ -95,7 +101,10 @@ function processContext(
return result;
}

export const limitScopeByIndentation: (context: CompletionContext) => PostprocessFilter = (context) => {
export function limitScopeByIndentation(
context: CompletionContext,
config: AgentConfig["postprocess"]["limitScopeByIndentation"],
): PostprocessFilter {
return (input) => {
const { prefix, suffix, prefixLines, suffixLines } = context;
const inputLines = splitLines(input);
Expand All @@ -105,7 +114,7 @@ export const limitScopeByIndentation: (context: CompletionContext) => Postproces
return null;
}
}
const indentContext = processContext(inputLines, prefixLines, suffixLines);
const indentContext = processContext(inputLines, prefixLines, suffixLines, config);
let index;
for (index = 1; index < inputLines.length; index++) {
if (isBlank(inputLines[index])) {
Expand Down Expand Up @@ -135,4 +144,4 @@ export const limitScopeByIndentation: (context: CompletionContext) => Postproces
}
return input;
};
};
}