Skip to content

Commit

Permalink
fix(agent): fix support for html/jsx auto closing chars. (TabbyML#1265)
Browse files Browse the repository at this point in the history
* fix(agent): fix support for html/jsx auto closing chars.

* test(agent): add test for jsx tags.
  • Loading branch information
icycodes authored Jan 29, 2024
1 parent 4eb913b commit bb2afea
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 57 deletions.
42 changes: 26 additions & 16 deletions clients/tabby-agent/src/CompletionCache.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { LRUCache } from "lru-cache";
import { CompletionContext, CompletionResponse } from "./CompletionContext";
import { rootLogger } from "./logger";
import { splitLines, autoClosingPairOpenings, autoClosingPairClosings, findUnpairedAutoClosingChars } from "./utils";
import { splitLines, autoClosingPairs, findUnpairedAutoClosingChars } from "./utils";

type CompletionCacheKey = CompletionContext;
type CompletionCacheValue = CompletionResponse;
Expand Down Expand Up @@ -173,27 +173,37 @@ export class CompletionCache {
return result;
}

// FIXME: add unit tests
// "function(" => ["function()"]
// "call([" => ["call([]", "call([])" ]
// "function(arg" => ["function(arg)" ]
private generateAutoClosedPrefixes(prefix: string): string[] {
const result: string[] = [];
const unpaired = findUnpairedAutoClosingChars(prefix);
for (
let checkIndex = 0, autoClosing = "";
checkIndex < this.options.prebuildCache.autoClosingPairCheck.max;
checkIndex++
) {
if (unpaired.length > checkIndex) {
const found = autoClosingPairOpenings.indexOf(unpaired[unpaired.length - 1 - checkIndex]);
if (found < 0) {
break;
}
autoClosing = autoClosing + autoClosingPairClosings[found];
result.push(prefix + autoClosing);
} else {
break;
}
let checkIndex = 0;
let autoClosing = "";
while (checkIndex < unpaired.length && checkIndex < this.options.prebuildCache.autoClosingPairCheck.max) {
autoClosingPairs
.filter((pair) => {
let pattern;
if ("open" in pair) {
pattern = pair.open;
} else {
pattern = pair.openOrClose;
}
return pattern.chars === unpaired[unpaired.length - 1 - checkIndex];
})
.forEach((pair) => {
let pattern;
if ("close" in pair) {
pattern = pair.close;
} else {
pattern = pair.openOrClose;
}
autoClosing += pattern.chars;
result.push(prefix + autoClosing);
});
checkIndex++;
}
return result;
}
Expand Down
7 changes: 2 additions & 5 deletions clients/tabby-agent/src/CompletionContext.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { splitLines, autoClosingPairClosings } from "./utils";
import { splitLines, regOnlyAutoClosingCloseChars } from "./utils";
import hashObject from "object-hash";

export type CompletionRequest = {
Expand Down Expand Up @@ -28,10 +28,7 @@ export type CompletionResponse = {
};

function isAtLineEndExcludingAutoClosedChar(suffix: string) {
return suffix
.trimEnd()
.split("")
.every((char) => autoClosingPairClosings.includes(char));
return suffix.trimEnd().match(regOnlyAutoClosingCloseChars);
}

export class CompletionContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,70 @@ describe("postprocess", () => {
};
expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected);
});

it("should handle html tags", () => {
const context = {
...documentContext`
<html></h║>
`,
language: "html",
};
const choice = {
index: 0,
text: inline`
├tml>┤
`,
replaceRange: {
start: context.position,
end: context.position,
},
};
const expected = {
index: 0,
text: inline`
├tml>┤
`,
replaceRange: {
start: context.position,
end: context.position + 1,
},
};
expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected);
});

it("should handle jsx tags", () => {
const context = {
...documentContext`
root.render(
<React.StrictMode>
<App m║/>
</React.StrictMode>
);
`,
language: "javascriptreact",
};
const choice = {
index: 0,
text: inline`
├essage={message} />┤
`,
replaceRange: {
start: context.position,
end: context.position,
},
};
const expected = {
index: 0,
text: inline`
├essage={message} />┤
`,
replaceRange: {
start: context.position,
end: context.position + 2,
},
};
expect(calculateReplaceRangeByBracketStack(choice, context)).to.deep.equal(expected);
});
});

describe("calculateReplaceRangeByBracketStack: bad cases", () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ export function calculateReplaceRangeByBracketStack(
return choice;
}
const completionText = choice.text.slice(context.position - choice.replaceRange.start);
const unpaired = findUnpairedAutoClosingChars(completionText);
const unpaired = findUnpairedAutoClosingChars(completionText).join("");
if (isBlank(unpaired)) {
return choice;
}
Expand Down
2 changes: 1 addition & 1 deletion clients/tabby-agent/src/postprocess/formatIndentation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ function detectIndentation(lines: string[]): string | null {

function getIndentLevel(line: string, indentation: string): number {
if (indentation === "\t") {
return line.match(/^\t*/g)?.[0].length ?? 0;
return line.match(/^\t*/)?.[0].length ?? 0;
} else {
const spaces = line.match(/^ */)?.[0].length ?? 0;
return spaces / indentation.length;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
description = 'Replace range: jsx tags: case 01'

[config]
# use default config

[context]
filepath = 'app.jsx'
language = 'javascriptreact'
# indentation = ' ' # not specified
text = '''
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
const message = <div>Hello world!</div>
root.render(
<React.StrictMode>
<App ├message={message} />┤/>
</React.StrictMode>
);
'''

[expected]
text = '''
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
const message = <div>Hello world!</div>
root.render(
<React.StrictMode>
<App ├message={message} />┤/>╣
</React.StrictMode>
);
'''
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
description = 'Replace range: jsx tags: case 02'

[config]
# use default config

[context]
filepath = 'app.jsx'
language = 'javascriptreact'
# indentation = ' ' # not specified
text = '''
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
const message = <div>Hello world!</div>
root.render(
<React.StrictMode>
<App message={message} />
</├React.StrictMode>┤>
);
'''

[expected]
text = '''
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
const message = <div>Hello world!</div>
root.render(
<React.StrictMode>
<App message={message} />
</├React.StrictMode>┤>╣
);
'''
Loading

0 comments on commit bb2afea

Please sign in to comment.