feat(agent): add experimental postprocess limit scope by syntax. (#758)

* feat(agent): add tree-sitter parser.

* feat(agent): make parser updating tree cache optional.

* feat(agent): add experimental limit scopy by syntax.

* test(agent): update unit test for limitScopeBySyntax.
extract-routes
Zhiming Ma 2023-11-13 02:11:31 +08:00 committed by GitHub
parent d7180ec7b9
commit 568b7b41a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 822 additions and 23 deletions

View File

@ -18,6 +18,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
lfs: true
- name: Setup JDK - name: Setup JDK
uses: actions/setup-java@v3 uses: actions/setup-java@v3
with: with:

View File

@ -22,6 +22,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
lfs: true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

View File

@ -24,6 +24,8 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v3 uses: actions/checkout@v3
with:
lfs: true
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:

1
clients/tabby-agent/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
*.wasm filter=lfs diff=lfs merge=lfs -text

View File

@ -23,11 +23,11 @@
"@types/node": "^18.12.0", "@types/node": "^18.12.0",
"chai": "^4.3.7", "chai": "^4.3.7",
"dedent": "^0.7.0", "dedent": "^0.7.0",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"mocha": "^10.2.0", "mocha": "^10.2.0",
"openapi-typescript": "^6.6.1", "openapi-typescript": "^6.6.1",
"prettier": "^3.0.0", "prettier": "^3.0.0",
"rimraf": "^5.0.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"tsup": "^7.1.0", "tsup": "^7.1.0",
"typescript": "^5.0.3" "typescript": "^5.0.3"
@ -47,6 +47,7 @@
"rotating-file-stream": "^3.1.0", "rotating-file-stream": "^3.1.0",
"stats-logscale": "^1.0.7", "stats-logscale": "^1.0.7",
"toml": "^3.0.0", "toml": "^3.0.0",
"uuid": "^9.0.0" "uuid": "^9.0.0",
"web-tree-sitter": "^0.20.8"
} }
} }

View File

@ -21,11 +21,15 @@ export type AgentConfig = {
timeout: number; timeout: number;
}; };
postprocess: { postprocess: {
limitScopeByIndentation: { limitScope: {
// When completion is continuing the current line, limit the scope to: // Prefer to use syntax parser than indentation
// false(default): the line scope, meaning use the next indent level as the limit. experimentalSyntax: boolean;
// true: the block scope, meaning use the current indent level as the limit. indentation: {
experimentalKeepBlockScopeWhenCompletingLine: boolean; // 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: {
@ -66,8 +70,11 @@ export const defaultAgentConfig: AgentConfig = {
timeout: 4000, // ms timeout: 4000, // ms
}, },
postprocess: { postprocess: {
limitScopeByIndentation: { limitScope: {
experimentalKeepBlockScopeWhenCompletingLine: false, experimentalSyntax: false,
indentation: {
experimentalKeepBlockScopeWhenCompletingLine: false,
},
}, },
}, },
logs: { logs: {

View File

@ -529,7 +529,7 @@ export class TabbyAgent extends EventEmitter implements Agent {
throw options.signal.reason; throw options.signal.reason;
} }
// Build cache // Build cache
this.completionCache.buildCache(context, completionResponse); this.completionCache.buildCache(context, JSON.parse(JSON.stringify(completionResponse)));
} }
} }
// Postprocess (post-cache) // Postprocess (post-cache)

View File

@ -4,7 +4,7 @@ import { applyFilter } from "./base";
import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks"; import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks";
import { removeRepetitiveLines } from "./removeRepetitiveLines"; import { removeRepetitiveLines } from "./removeRepetitiveLines";
import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition"; import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition";
import { limitScopeByIndentation } from "./limitScopeByIndentation"; import { limitScope } from "./limitScope";
import { trimSpace } from "./trimSpace"; import { trimSpace } from "./trimSpace";
import { dropDuplicated } from "./dropDuplicated"; import { dropDuplicated } from "./dropDuplicated";
import { dropBlank } from "./dropBlank"; import { dropBlank } from "./dropBlank";
@ -30,7 +30,7 @@ export async function postCacheProcess(
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, config["limitScopeByIndentation"]), context)) .then(applyFilter(limitScope(context, config["limitScope"]), 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

@ -0,0 +1,24 @@
import { CompletionContext } from "../Agent";
import { AgentConfig } from "../AgentConfig";
import { isBrowser } from "../env";
import { PostprocessFilter } from "./base";
import { limitScopeByIndentation } from "./limitScopeByIndentation";
import { limitScopeBySyntax, supportedLanguages } from "./limitScopeBySyntax";
export function limitScope(
context: CompletionContext,
config: AgentConfig["postprocess"]["limitScope"],
): PostprocessFilter {
return isBrowser
? (input) => {
// syntax parser is not supported in browser yet
return limitScopeByIndentation(context, config["indentation"])(input);
}
: (input) => {
if (config.experimentalSyntax && supportedLanguages.indexOf(context.language) >= 0) {
return limitScopeBySyntax(context)(input);
} else {
return limitScopeByIndentation(context, config["indentation"])(input);
}
};
}

View File

@ -18,7 +18,7 @@ function processContext(
lines: string[], lines: string[],
prefixLines: string[], prefixLines: string[],
suffixLines: string[], suffixLines: string[],
config: AgentConfig["postprocess"]["limitScopeByIndentation"], config: AgentConfig["postprocess"]["limitScope"]["indentation"],
): { 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 };
@ -103,14 +103,14 @@ function processContext(
export function limitScopeByIndentation( export function limitScopeByIndentation(
context: CompletionContext, context: CompletionContext,
config: AgentConfig["postprocess"]["limitScopeByIndentation"], config: AgentConfig["postprocess"]["limitScope"]["indentation"],
): PostprocessFilter { ): PostprocessFilter {
return (input) => { return (input) => {
const { prefix, suffix, prefixLines, suffixLines } = context; const { prefixLines, suffixLines } = context;
const inputLines = splitLines(input); const inputLines = splitLines(input);
if (context.mode === "fill-in-line") { if (context.mode === "fill-in-line") {
if (inputLines.length > 1) { if (inputLines.length > 1) {
logger.debug({ input, prefix, suffix }, "Drop content with multiple lines"); logger.debug({ inputLines, prefixLines, suffixLines }, "Drop content with multiple lines");
return null; return null;
} }
} }
@ -139,7 +139,10 @@ export function limitScopeByIndentation(
} }
} }
if (index < inputLines.length) { if (index < inputLines.length) {
logger.debug({ input, prefix, suffix, scopeEndAt: index }, "Remove content out of scope"); logger.debug(
{ inputLines, prefixLines, suffixLines, scopeLineCount: index },
"Remove content out of indent scope",
);
return inputLines.slice(0, index).join("").trimEnd(); return inputLines.slice(0, index).join("").trimEnd();
} }
return input; return input;

View File

@ -0,0 +1,409 @@
import { expect } from "chai";
import { documentContext, inline } from "./testUtils";
import { limitScopeBySyntax } from "./limitScopeBySyntax";
describe("postprocess", () => {
describe("limitScopeBySyntax javascript", () => {
it("should limit scope at function_declaration.", async () => {
const context = {
...documentContext`
function findMax(arr) {}
`,
language: "javascript",
};
const completion = inline`
let max = arr[0];
for (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 max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should limit scope at function_declaration", async () => {
const context = {
...documentContext`
function findMax(arr) {
let max = arr[0];
}
`,
language: "javascript",
};
const completion = inline`
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
return max;
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(completion);
});
it("should limit scope at function_declaration", async () => {
const context = {
...documentContext`
function findMax(arr) {
let max = arr[0];
for (let i = 1; i < arr.length; i++) {
if (arr[i] > max) {
max = arr[i];
}
}
}
`,
language: "javascript",
};
const completion = inline`
return max;
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(completion);
});
it("should limit scope at for_statement.", async () => {
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];
}
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should limit scope at current node if no parent scope found.", async () => {
const context = {
...documentContext`
let a =
`,
language: "javascript",
};
const completion = inline`
1;
let b = 2;
`;
const expected = inline`
1;
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should handle the bad case of limitScopeByIndentation", async () => {
const context = {
...documentContext`
function sortWords(input) {
const output = input.trim()
.split("\n")
.map((line) => line.split(" "))
}
`,
language: "javascript",
};
const completion = inline`
.flat()
.sort()
.join(" ");
console.log(output);
return output;
}
sortWord("world hello");
`;
const expected = inline`
.flat()
.sort()
.join(" ");
console.log(output);
return output;
};
`;
expect(await limitScopeBySyntax(context)(completion)).not.to.eq(expected);
});
});
describe("limitScopeBySyntax python", () => {
it("should limit scope at function_definition.", async () => {
const context = {
...documentContext`
def find_min(arr):
`,
language: "python",
};
const completion = inline`
min = arr[0]
for i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
return min
print(find_min([1, 2, 3, 4, 5]))
`;
const expected = inline`
min = arr[0]
for i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
return min
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should limit scope at function_definition.", async () => {
const context = {
...documentContext`
def find_min(arr):
min = arr[0]
`,
language: "python",
};
const completion = inline`
for i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
return min
print(find_min([1, 2, 3, 4, 5]))
`;
const expected = inline`
for i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
return min
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should limit scope at function_definition.", async () => {
const context = {
...documentContext`
def find_min(arr):
min = arr[0]
for i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
`,
language: "python",
};
const completion = inline`
return min
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(completion);
});
it("should limit scope at for_statement.", async () => {
const context = {
...documentContext`
def find_min(arr):
max = arr[0]
for
`,
language: "python",
};
const completion = inline`
i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
return min
`;
const expected = inline`
i in range(1, len(arr)):
if arr[i] < min:
min = arr[i]
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should handle the bad case of limitScopeByIndentation", async () => {
const context = {
...documentContext`
def findMax(arr):
`,
language: "python",
};
const completion = inline`
max = arr[0]
for i in range(1, len(arr)):
if arr[i] > max:
max = arr[i]
return max
findMax([1, 2, 3, 4, 5])
`;
const expected = inline`
max = arr[0]
for i in range(1, len(arr)):
if arr[i] > max:
max = arr[i]
return max
`;
expect(await limitScopeBySyntax(context)(completion)).not.to.eq(expected);
});
});
describe("limitScopeBySyntax go", () => {
it("should limit scope at function_declaration.", async () => {
const context = {
...documentContext`
func findMin(arr []int) int {}
`,
language: "go",
};
const completion = inline`
min := math.MaxInt64
for _, v := range arr {
if v < min {
min = v
}
}
return min
}
func main() {
arr := []int{5, 2, 9, 8, 1, 3}
fmt.Println(findMin(arr)) // Output: 1
}
`;
const expected = inline`
min := math.MaxInt64
for _, v := range arr {
if v < min {
min = v
}
}
return min
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
it("should limit scope at for_statement.", async () => {
const context = {
...documentContext`
func findMin(arr []int) int {
min := math.MaxInt64
for
`,
language: "go",
};
const completion = inline`
_, v := range arr {
if v < min {
min = v
}
}
return min
}
`;
const expected = inline`
_, v := range arr {
if v < min {
min = v
}
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
});
describe("limitScopeBySyntax rust", () => {
it("should limit scope at function_item.", async () => {
const context = {
...documentContext`
fn find_min(arr: &[i32]) -> i32 {}
`,
language: "rust",
};
const completion = inline`
*arr.iter().min().unwrap()
}
fn main() {
let arr = vec![5, 2, 9, 8, 1, 3];
println!("{}", find_min(&arr)); // Output: 1
}
`;
const expected = inline`
*arr.iter().min().unwrap()
}
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
});
describe("limitScopeBySyntax ruby", () => {
it("should limit scope at for.", async () => {
const context = {
...documentContext`
def fibonacci(n)
`,
language: "ruby",
};
const completion = inline`
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
puts fibonacci(10)
`;
const expected = inline`
return n if n <= 1
fibonacci(n - 1) + fibonacci(n - 2)
end
`;
expect(await limitScopeBySyntax(context)(completion)).to.eq(expected);
});
});
});

View File

@ -0,0 +1,86 @@
import type TreeSitterParser from "web-tree-sitter";
import { getParser, languagesConfigs } from "../syntax/parser";
import { typeList } from "../syntax/typeList";
import { CompletionContext } from "../Agent";
import { PostprocessFilter, logger } from "./base";
export const supportedLanguages = Object.keys(languagesConfigs);
function findLineBegin(text: string, position: number): number {
let lastNonBlankCharPos = position - 1;
while (lastNonBlankCharPos >= 0 && text[lastNonBlankCharPos].match(/\s/)) {
lastNonBlankCharPos--;
}
if (lastNonBlankCharPos < 0) {
return 0;
}
const lineBegin = text.lastIndexOf("\n", lastNonBlankCharPos);
if (lineBegin < 0) {
return 0;
}
const line = text.slice(lineBegin + 1, position);
const indentation = line.search(/\S/);
return lineBegin + 1 + indentation;
}
function findLineEnd(text: string, position: number): number {
let firstNonBlankCharPos = position;
while (firstNonBlankCharPos < text.length && text[firstNonBlankCharPos].match(/\s/)) {
firstNonBlankCharPos++;
}
if (firstNonBlankCharPos >= text.length) {
return text.length;
}
const lineEnd = text.indexOf("\n", firstNonBlankCharPos);
if (lineEnd < 0) {
return text.length;
}
return lineEnd;
}
function findScope(node: TreeSitterParser.SyntaxNode, typeList: string[][]): TreeSitterParser.SyntaxNode {
for (const types of typeList) {
let scope = node;
while (scope) {
if (types.indexOf(scope.type) >= 0) {
return scope;
}
scope = scope.parent;
}
}
return node;
}
export function limitScopeBySyntax(context: CompletionContext): PostprocessFilter {
return async (input) => {
const { position, text, language, prefix, suffix } = context;
if (supportedLanguages.indexOf(language) < 0) {
return input;
}
const languageConfig = languagesConfigs[language];
const parser = await getParser(languageConfig);
const updatedText = prefix + input + suffix;
const updatedTree = parser.parse(updatedText);
const lineBegin = findLineBegin(updatedText, position);
const lineEnd = findLineEnd(updatedText, position);
const scope = findScope(updatedTree.rootNode.namedDescendantForIndex(lineBegin, lineEnd), typeList[languageConfig]);
if (scope.endIndex < position + input.length) {
logger.debug(
{
languageConfig,
text,
updatedText,
position,
lineBegin,
lineEnd,
scope: { type: scope.type, start: scope.startIndex, end: scope.endIndex },
},
"Remove content out of syntax scope",
);
return input.slice(0, scope.endIndex - position);
}
return input;
};
}

View File

@ -1,12 +1,13 @@
import dedent from "dedent"; import dedent from "dedent";
import { v4 as uuid } from "uuid";
import { CompletionContext } from "../CompletionContext"; import { CompletionContext } from "../CompletionContext";
// `║` is the cursor position // `║` is the cursor position
export function documentContext(strings): CompletionContext { export function documentContext(strings): CompletionContext {
const doc = dedent(strings); const doc = dedent(strings);
return new CompletionContext({ return new CompletionContext({
filepath: null, filepath: uuid(),
language: null, language: "",
text: doc.replace(/║/, ""), text: doc.replace(/║/, ""),
position: doc.indexOf("║"), position: doc.indexOf("║"),
}); });

View File

@ -0,0 +1,45 @@
import TreeSitterParser from "web-tree-sitter";
import { isTest } from "../env";
// https://code.visualstudio.com/docs/languages/identifiers
export const languagesConfigs: Record<string, string> = {
javascript: "tsx",
typescript: "tsx",
javascriptreact: "tsx",
typescriptreact: "tsx",
python: "python",
go: "go",
rust: "rust",
ruby: "ruby",
};
var treeSitterInitialized = false;
async function createParser(languageConfig: string): Promise<TreeSitterParser> {
if (!treeSitterInitialized) {
await TreeSitterParser.init({
locateFile(scriptName: string, scriptDirectory: string) {
const paths = isTest ? [scriptDirectory, scriptName] : [scriptDirectory, "wasm", scriptName];
return require("path").join(...paths);
},
});
treeSitterInitialized = true;
}
const parser = new TreeSitterParser();
const langWasmPaths = isTest
? [process.cwd(), "wasm", `tree-sitter-${languageConfig}.wasm`]
: [__dirname, "wasm", `tree-sitter-${languageConfig}.wasm`];
parser.setLanguage(await TreeSitterParser.Language.load(require("path").join(...langWasmPaths)));
return parser;
}
const parsers = new Map<string, TreeSitterParser>();
export async function getParser(languageConfig: string): Promise<TreeSitterParser> {
let parser = parsers.get(languageConfig);
if (!parser) {
parser = await createParser(languageConfig);
parsers.set(languageConfig, parser);
}
return parser;
}

View File

@ -0,0 +1,116 @@
// https://github.com/tree-sitter/tree-sitter-typescript/blob/master/src/node-types.json
export const typeList = {
tsx: [
[
"jsx_element",
"jsx_self_closing_element",
// exclude sentence level nodes for now
// "expression_statement",
// "lexical_declaration",
"for_statement",
"for_in_statement",
"if_statement",
"while_statement",
"do_statement",
"switch_statement",
"try_statement",
"with_statement",
"labeled_statement",
"class_declaration",
"abstract_class_declaration",
"interface_declaration",
"enum_declaration",
"type_alias_declaration",
"function_declaration",
"generator_function_declaration",
"ambient_declaration",
"method_definition",
"import_statement",
"export_statement",
"module",
],
],
// https://github.com/tree-sitter/tree-sitter-python/blob/master/src/node-types.json
python: [
[
"for_statement",
"if_statement",
"while_statement",
"match_statement",
"try_statement",
"with_statement",
"function_definition",
"decorated_definition",
"class_definition",
"import_statement",
"import_from_statement",
],
],
// https://github.com/tree-sitter/tree-sitter-go/blob/master/src/node-types.json
go: [
[
"for_statement",
"if_statement",
"expression_switch_statement",
"type_switch_statement",
"select_statement",
"labeled_statement",
"function_declaration",
"method_declaration",
"type_declaration",
"import_declaration",
"package_clause",
],
],
// https://github.com/tree-sitter/tree-sitter-rust/blob/master/src/node-types.json
rust: [
[
"for_expression",
"if_expression",
"while_expression",
"loop_expression",
"match_expression",
"try_expression",
"function_item",
"type_item",
"enum_item",
"struct_item",
"union_item",
"trait_item",
"impl_item",
"use_declaration",
],
],
// https://github.com/tree-sitter/tree-sitter-ruby/blob/master/src/node-types.json
ruby: [
[
"for",
"if",
"unless",
"while",
"until",
"case",
"class",
"singleton_class",
"method",
"singleton_method",
"module",
],
],
};

View File

@ -81,7 +81,10 @@ describe("agent golden test", () => {
timeout: 4000, timeout: 4000,
}, },
postprocess: { postprocess: {
limitScopeByIndentation: { experimentalKeepBlockScopeWhenCompletingLine: false }, limitScope: {
experimentalSyntax: false,
indentation: { experimentalKeepBlockScopeWhenCompletingLine: false },
},
}, },
logs: { level: "debug" }, logs: { level: "debug" },
anonymousUsageTracking: { disable: true }, anonymousUsageTracking: { disable: true },
@ -111,7 +114,7 @@ describe("agent golden test", () => {
absolutePath: path.join(__dirname, "golden", file), absolutePath: path.join(__dirname, "golden", file),
}; };
}); });
goldenFiles.forEach((goldenFile, index) => { goldenFiles.forEach((goldenFile) => {
it(goldenFile.path, async () => { it(goldenFile.path, async () => {
const test = await createGoldenTest(goldenFile.absolutePath); const test = await createGoldenTest(goldenFile.absolutePath);
requestId++; requestId++;
@ -130,7 +133,7 @@ describe("agent golden test", () => {
absolutePath: path.join(__dirname, "bad_cases", file), absolutePath: path.join(__dirname, "bad_cases", file),
}; };
}); });
badCasesFiles.forEach((goldenFile, index) => { badCasesFiles.forEach((goldenFile) => {
it(goldenFile.path, async () => { it(goldenFile.path, async () => {
const test = await createGoldenTest(goldenFile.absolutePath); const test = await createGoldenTest(goldenFile.absolutePath);
requestId++; requestId++;
@ -143,6 +146,35 @@ describe("agent golden test", () => {
}); });
}); });
it("updateConfig experimental", async () => {
requestId++;
const updateConfigRequest = [
requestId,
{
func: "updateConfig",
args: ["postprocess.limitScope.experimentalSyntax", true],
},
];
agent.stdin.write(JSON.stringify(updateConfigRequest) + "\n");
await waitForResponse(requestId);
const expectedConfig = { ...config };
expectedConfig.postprocess.limitScope.experimentalSyntax = true;
expect(output.shift()).to.deep.equal([0, { event: "configUpdated", config: expectedConfig }]);
expect(output.shift()).to.deep.equal([requestId, true]);
});
badCasesFiles.forEach((goldenFile) => {
it("experimental: " + goldenFile.path, async () => {
const test = await createGoldenTest(goldenFile.absolutePath);
requestId++;
const request = [requestId, { func: "provideCompletions", args: [test.request] }];
agent.stdin.write(JSON.stringify(request) + "\n");
await waitForResponse(requestId);
const response = output.shift();
expect(response[0]).to.equal(requestId);
expect(response[1].choices).to.deep.equal(test.expected.choices);
});
});
after(() => { after(() => {
agent.kill(); agent.kill();
}); });

View File

@ -1,4 +1,5 @@
import { defineConfig } from "tsup"; import { defineConfig } from "tsup";
import { copy } from "esbuild-plugin-copy";
import { polyfillNode } from "esbuild-plugin-polyfill-node"; import { polyfillNode } from "esbuild-plugin-polyfill-node";
import { dependencies } from "./package.json"; import { dependencies } from "./package.json";
@ -16,6 +17,7 @@ export default async () => [
name: "node-cjs", name: "node-cjs",
entry: ["src/index.ts"], entry: ["src/index.ts"],
platform: "node", platform: "node",
target: "node18",
format: ["cjs"], format: ["cjs"],
sourcemap: true, sourcemap: true,
esbuildOptions(options) { esbuildOptions(options) {
@ -71,10 +73,21 @@ export default async () => [
name: "cli", name: "cli",
entry: ["src/cli.ts"], entry: ["src/cli.ts"],
platform: "node", platform: "node",
target: "node18",
noExternal: Object.keys(dependencies), noExternal: Object.keys(dependencies),
treeshake: "smallest", treeshake: "smallest",
minify: true, minify: true,
sourcemap: true, sourcemap: true,
esbuildPlugins: [
copy({
assets: [
{
from: "./wasm/*",
to: "./wasm",
},
],
}),
],
esbuildOptions(options) { esbuildOptions(options) {
defineEnvs(options, { browser: false }); defineEnvs(options, { browser: false });
}, },

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1b69c5af834fd23053238e484c7fe9ed2f121d5b1fe32242af78576d67e49f1e
size 240169

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:72d0f97ba6c3134d7873ec5c9d0fd3c1f5137f4eac4dda0709993d92809e62b6
size 474189

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:1190cddd839b78c2aec737573399a71c23fe9a546d3543f86304c4c68ca73852
size 990787

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:273f9ce6f2c595ad4e63b3195513b61974ae1ec513efcce39da1afa90574ef38
size 844087

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:060422a330f9c819a10e7310788d336dbcb53cc6a4be0e91d40f644564080f97
size 1182114

View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:17382e1a69bd628107e8dfe37d31d57f7ba948e5f2da77e56a8aa010488dc5ae
size 186526

View File

@ -208,6 +208,7 @@
"@vscode/test-web": "^0.0.44", "@vscode/test-web": "^0.0.44",
"@vscode/vsce": "^2.15.0", "@vscode/vsce": "^2.15.0",
"assert": "^2.0.0", "assert": "^2.0.0",
"esbuild-plugin-copy": "^2.1.1",
"esbuild-plugin-polyfill-node": "^0.3.0", "esbuild-plugin-polyfill-node": "^0.3.0",
"eslint": "^8.20.0", "eslint": "^8.20.0",
"glob": "^8.0.3", "glob": "^8.0.3",

View File

@ -1,4 +1,5 @@
import { defineConfig } from "tsup"; import { defineConfig } from "tsup";
import { copy } from "esbuild-plugin-copy";
import { polyfillNode } from "esbuild-plugin-polyfill-node"; import { polyfillNode } from "esbuild-plugin-polyfill-node";
import { dependencies } from "./package.json"; import { dependencies } from "./package.json";
@ -8,8 +9,19 @@ export default () => [
entry: ["src/extension.ts"], entry: ["src/extension.ts"],
outDir: "dist/node", outDir: "dist/node",
platform: "node", platform: "node",
target: "node18",
external: ["vscode"], external: ["vscode"],
noExternal: Object.keys(dependencies), noExternal: Object.keys(dependencies),
esbuildPlugins: [
copy({
assets: [
{
from: "../tabby-agent/dist/wasm/*.wasm",
to: "./wasm",
},
],
}),
],
clean: true, clean: true,
}), }),
defineConfig({ defineConfig({

View File

@ -900,7 +900,7 @@ chalk@^2.4.2:
escape-string-regexp "^1.0.5" escape-string-regexp "^1.0.5"
supports-color "^5.3.0" supports-color "^5.3.0"
chalk@^4.0.0, chalk@^4.1.0: chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
@ -1390,6 +1390,16 @@ es-get-iterator@^1.1.3:
isarray "^2.0.5" isarray "^2.0.5"
stop-iteration-iterator "^1.0.0" stop-iteration-iterator "^1.0.0"
esbuild-plugin-copy@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz#638308ecfd679e4c7c76b71c62f7dd9a4cc7f901"
integrity sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==
dependencies:
chalk "^4.1.2"
chokidar "^3.5.3"
fs-extra "^10.0.1"
globby "^11.0.3"
esbuild-plugin-polyfill-node@^0.3.0: esbuild-plugin-polyfill-node@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/esbuild-plugin-polyfill-node/-/esbuild-plugin-polyfill-node-0.3.0.tgz#e7e3804b8272df51ae4f8ebfb7445a03712504cb" resolved "https://registry.yarnpkg.com/esbuild-plugin-polyfill-node/-/esbuild-plugin-polyfill-node-0.3.0.tgz#e7e3804b8272df51ae4f8ebfb7445a03712504cb"
@ -1706,6 +1716,15 @@ fs-constants@^1.0.0:
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-extra@^10.0.1:
version "10.1.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==
dependencies:
graceful-fs "^4.2.0"
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-extra@^11.1.1: fs-extra@^11.1.1:
version "11.1.1" version "11.1.1"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.1.tgz#da69f7c39f3b002378b0954bb6ae7efdc0876e2d"
@ -4117,6 +4136,11 @@ vscode-uri@^3.0.7:
resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8"
integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA==
web-tree-sitter@^0.20.8:
version "0.20.8"
resolved "https://registry.yarnpkg.com/web-tree-sitter/-/web-tree-sitter-0.20.8.tgz#1e371cb577584789cadd75cb49c7ddfbc99d04c8"
integrity sha512-weOVgZ3aAARgdnb220GqYuh7+rZU0Ka9k9yfKtGAzEYMa6GgiCzW9JjQRJyCJakvibQW+dfjJdihjInKuuCAUQ==
webidl-conversions@^4.0.2: webidl-conversions@^4.0.2:
version "4.0.2" version "4.0.2"
resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"