From 0cf44f7f9140bfec24fd7e7618e16c4179734966 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Fri, 27 Oct 2023 19:27:15 +0800 Subject: [PATCH 1/2] feat(agent): add experimental option: scope of indentation filter. --- clients/tabby-agent/src/AgentConfig.ts | 13 +++++++++++++ clients/tabby-agent/src/TabbyAgent.ts | 4 ++-- clients/tabby-agent/src/postprocess/index.ts | 5 ++++- .../src/postprocess/limitScopeByIndentation.ts | 17 +++++++++++++---- 4 files changed, 32 insertions(+), 7 deletions(-) diff --git a/clients/tabby-agent/src/AgentConfig.ts b/clients/tabby-agent/src/AgentConfig.ts index b227b75e90a..0c288be534f 100644 --- a/clients/tabby-agent/src/AgentConfig.ts +++ b/clients/tabby-agent/src/AgentConfig.ts @@ -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. + keepBlockScopeWhenCompletingLine: boolean; + }; + }; logs: { level: "debug" | "error" | "silent"; }; @@ -59,6 +67,11 @@ export const defaultAgentConfig: AgentConfig = { manually: 4000, // 4s }, }, + postprocess: { + limitScopeByIndentation: { + keepBlockScopeWhenCompletingLine: false, + }, + }, logs: { level: "silent", }, diff --git a/clients/tabby-agent/src/TabbyAgent.ts b/clients/tabby-agent/src/TabbyAgent.ts index 081a7847cfd..cb090a69470 100644 --- a/clients/tabby-agent/src/TabbyAgent.ts +++ b/clients/tabby-agent/src/TabbyAgent.ts @@ -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; } @@ -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; } diff --git a/clients/tabby-agent/src/postprocess/index.ts b/clients/tabby-agent/src/postprocess/index.ts index fe1622694bd..14e8c47f889 100644 --- a/clients/tabby-agent/src/postprocess/index.ts +++ b/clients/tabby-agent/src/postprocess/index.ts @@ -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"; @@ -10,6 +11,7 @@ import { dropBlank } from "./dropBlank"; export async function preCacheProcess( context: CompletionContext, + config: AgentConfig["postprocess"], response: CompletionResponse, ): Promise { return Promise.resolve(response) @@ -21,12 +23,13 @@ export async function preCacheProcess( export async function postCacheProcess( context: CompletionContext, + config: AgentConfig["postprocess"], response: CompletionResponse, ): Promise { 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)); diff --git a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts index 1a9498b1781..f4b9b98cb82 100644 --- a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts +++ b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts @@ -1,4 +1,5 @@ import { CompletionContext } from "../Agent"; +import { AgentConfig } from "../AgentConfig"; import { PostprocessFilter, logger } from "./base"; import { isBlank, splitLines } from "../utils"; @@ -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 }; @@ -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.keepBlockScopeWhenCompletingLine) { + 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) { @@ -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); @@ -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])) { @@ -135,4 +144,4 @@ export const limitScopeByIndentation: (context: CompletionContext) => Postproces } return input; }; -}; +} From 8a1d870f8236039ddfc8445a2504c97922b97875 Mon Sep 17 00:00:00 2001 From: Zhiming Ma Date: Mon, 30 Oct 2023 10:54:34 +0800 Subject: [PATCH 2/2] fix: add config to fix unit test for limitScopeByIndentation. --- clients/tabby-agent/src/AgentConfig.ts | 4 +- .../limitScopeByIndentation.test.ts | 112 +++++++++++++++--- .../postprocess/limitScopeByIndentation.ts | 2 +- 3 files changed, 101 insertions(+), 17 deletions(-) diff --git a/clients/tabby-agent/src/AgentConfig.ts b/clients/tabby-agent/src/AgentConfig.ts index 0c288be534f..e70e240e644 100644 --- a/clients/tabby-agent/src/AgentConfig.ts +++ b/clients/tabby-agent/src/AgentConfig.ts @@ -25,7 +25,7 @@ export type AgentConfig = { // 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. - keepBlockScopeWhenCompletingLine: boolean; + experimentalKeepBlockScopeWhenCompletingLine: boolean; }; }; logs: { @@ -69,7 +69,7 @@ export const defaultAgentConfig: AgentConfig = { }, postprocess: { limitScopeByIndentation: { - keepBlockScopeWhenCompletingLine: false, + experimentalKeepBlockScopeWhenCompletingLine: false, }, }, logs: { diff --git a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts index 83f2a37a1c8..5c31efa97c6 100644 --- a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts +++ b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.test.ts @@ -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` @@ -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.", () => { @@ -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 `)]}`.", () => { @@ -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.", () => { @@ -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.", () => { @@ -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) {║} @@ -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.", () => { @@ -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.", () => { @@ -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", () => { @@ -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", () => { @@ -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", () => { @@ -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.", () => { @@ -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); }); }); }); diff --git a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts index f4b9b98cb82..4e4c8f21dd6 100644 --- a/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts +++ b/clients/tabby-agent/src/postprocess/limitScopeByIndentation.ts @@ -59,7 +59,7 @@ function processContext( if (!isCurrentLineInCompletionBlank && !isCurrentLineInPrefixBlank) { // if two reference lines are contacted at current line, it is continuing uncompleted sentence - if (config.keepBlockScopeWhenCompletingLine) { + if (config.experimentalKeepBlockScopeWhenCompletingLine) { result.indentLevelLimit = referenceLineInPrefixIndent; } else { result.indentLevelLimit = referenceLineInPrefixIndent + 1; // + 1 for comparison, no matter how many spaces indent