feat(agent): format indentation if not match with editor config. (#911)
parent
edd33a326d
commit
c049f23a0c
|
|
@ -6,6 +6,7 @@ export type CompletionRequest = {
|
||||||
language: string;
|
language: string;
|
||||||
text: string;
|
text: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
indentation?: string;
|
||||||
clipboard?: string;
|
clipboard?: string;
|
||||||
manually?: boolean;
|
manually?: boolean;
|
||||||
};
|
};
|
||||||
|
|
@ -36,6 +37,7 @@ function isAtLineEndExcludingAutoClosedChar(suffix: string) {
|
||||||
export class CompletionContext {
|
export class CompletionContext {
|
||||||
filepath: string;
|
filepath: string;
|
||||||
language: string;
|
language: string;
|
||||||
|
indentation?: string;
|
||||||
text: string;
|
text: string;
|
||||||
position: number;
|
position: number;
|
||||||
|
|
||||||
|
|
@ -57,6 +59,7 @@ export class CompletionContext {
|
||||||
this.language = request.language;
|
this.language = request.language;
|
||||||
this.text = request.text;
|
this.text = request.text;
|
||||||
this.position = request.position;
|
this.position = request.position;
|
||||||
|
this.indentation = request.indentation;
|
||||||
|
|
||||||
this.prefix = request.text.slice(0, request.position);
|
this.prefix = request.text.slice(0, request.position);
|
||||||
this.suffix = request.text.slice(request.position);
|
this.suffix = request.text.slice(request.position);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
import { expect } from "chai";
|
||||||
|
import { documentContext, inline } from "./testUtils";
|
||||||
|
import { formatIndentation } from "./formatIndentation";
|
||||||
|
|
||||||
|
describe("postprocess", () => {
|
||||||
|
describe("formatIndentation", () => {
|
||||||
|
it("should format indentation if first line of completion is over indented.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
function clamp(n: number, max: number, min: number): number {
|
||||||
|
║
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
indentation: " ",
|
||||||
|
language: "typescript",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├ return Math.max(Math.min(n, max), min);┤
|
||||||
|
`;
|
||||||
|
const expected = inline`
|
||||||
|
├return Math.max(Math.min(n, max), min);┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format indentation if first line of completion is wrongly indented.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
function clamp(n: number, max: number, min: number): number {
|
||||||
|
║
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
indentation: " ",
|
||||||
|
language: "typescript",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├ return Math.max(Math.min(n, max), min);┤
|
||||||
|
`;
|
||||||
|
const expected = inline`
|
||||||
|
├ return Math.max(Math.min(n, max), min);┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format indentation if completion lines is over indented.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
def findMax(arr):║
|
||||||
|
`,
|
||||||
|
indentation: " ",
|
||||||
|
language: "python",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
const expected = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should format indentation if completion lines is wrongly indented.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
def findMax(arr):║
|
||||||
|
`,
|
||||||
|
indentation: " ",
|
||||||
|
language: "python",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
const expected = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep it unchanged if it no indentation specified.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
def findMax(arr):║
|
||||||
|
`,
|
||||||
|
indentation: undefined,
|
||||||
|
language: "python",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(completion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep it unchanged if there is indentation in the context.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
def hello():
|
||||||
|
return "world"
|
||||||
|
|
||||||
|
def findMax(arr):║
|
||||||
|
`,
|
||||||
|
indentation: "\t",
|
||||||
|
language: "python",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(completion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should keep it unchanged if it is well indented.", () => {
|
||||||
|
const context = {
|
||||||
|
...documentContext`
|
||||||
|
def findMax(arr):║
|
||||||
|
`,
|
||||||
|
indentation: " ",
|
||||||
|
language: "python",
|
||||||
|
};
|
||||||
|
const completion = inline`
|
||||||
|
├
|
||||||
|
max = arr[0]
|
||||||
|
for i in range(1, len(arr)):
|
||||||
|
if arr[i] > max:
|
||||||
|
max = arr[i]
|
||||||
|
return max
|
||||||
|
}┤
|
||||||
|
`;
|
||||||
|
expect(formatIndentation(context)(completion)).to.eq(completion);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { CompletionContext } from "../Agent";
|
||||||
|
import { PostprocessFilter, logger } from "./base";
|
||||||
|
import { isBlank, splitLines } from "../utils";
|
||||||
|
|
||||||
|
function detectIndentation(lines: string[]): string | null {
|
||||||
|
const matches = {
|
||||||
|
"\t": 0,
|
||||||
|
" ": 0,
|
||||||
|
" ": 0,
|
||||||
|
};
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.match(/^\t/)) {
|
||||||
|
matches["\t"]++;
|
||||||
|
} else {
|
||||||
|
const spaces = line.match(/^ */)[0].length;
|
||||||
|
if (spaces > 0) {
|
||||||
|
if (spaces % 4 === 0) {
|
||||||
|
matches[" "]++;
|
||||||
|
}
|
||||||
|
if (spaces % 2 === 0) {
|
||||||
|
matches[" "]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matches["\t"] > 0) {
|
||||||
|
return "\t";
|
||||||
|
}
|
||||||
|
if (matches[" "] > matches[" "]) {
|
||||||
|
return " ";
|
||||||
|
}
|
||||||
|
if (matches[" "] > 0) {
|
||||||
|
return " ";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIndentLevel(line: string, indentation: string): number {
|
||||||
|
if (indentation === "\t") {
|
||||||
|
return line.match(/^\t*/g)[0].length;
|
||||||
|
} else {
|
||||||
|
const spaces = line.match(/^ */)[0].length;
|
||||||
|
return spaces / indentation.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatIndentation(context: CompletionContext): PostprocessFilter {
|
||||||
|
return (input) => {
|
||||||
|
const { prefixLines, suffixLines, indentation } = context;
|
||||||
|
const inputLines = splitLines(input);
|
||||||
|
|
||||||
|
// if no indentation is specified
|
||||||
|
if (!indentation) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if there is any indentation in context, the server output should have learned from it
|
||||||
|
const prefixLinesForDetection = isBlank(prefixLines[prefixLines.length - 1])
|
||||||
|
? prefixLines.slice(0, prefixLines.length - 1)
|
||||||
|
: prefixLines;
|
||||||
|
if (prefixLines.length > 1 && detectIndentation(prefixLinesForDetection) !== null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
const suffixLinesForDetection = suffixLines.slice(1);
|
||||||
|
if (suffixLines.length > 1 && detectIndentation(suffixLinesForDetection) !== null) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the input is well indented with specific indentation
|
||||||
|
const inputLinesForDetection = inputLines.map((line, index) => {
|
||||||
|
return index === 0 ? prefixLines[prefixLines.length - 1] + line : line;
|
||||||
|
});
|
||||||
|
const inputIndentation = detectIndentation(inputLinesForDetection);
|
||||||
|
if (inputIndentation === null || inputIndentation === indentation) {
|
||||||
|
return input;
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, do formatting
|
||||||
|
const formatted = inputLinesForDetection.map((line, index) => {
|
||||||
|
const level = getIndentLevel(inputLinesForDetection[index], inputIndentation);
|
||||||
|
if (level === 0) {
|
||||||
|
return inputLines[index];
|
||||||
|
}
|
||||||
|
const rest = line.slice(inputIndentation.length * level);
|
||||||
|
if (index === 0) {
|
||||||
|
// for first line
|
||||||
|
if (!isBlank(prefixLines[prefixLines.length - 1])) {
|
||||||
|
return inputLines[0];
|
||||||
|
} else {
|
||||||
|
return indentation.repeat(level).slice(prefixLines[prefixLines.length - 1].length) + rest;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// for next lines
|
||||||
|
return indentation.repeat(level) + rest;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
logger.debug({ prefixLines, suffixLines, inputLines, formatted }, "Format indentation.");
|
||||||
|
return formatted.join("");
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,7 @@ import { removeRepetitiveBlocks } from "./removeRepetitiveBlocks";
|
||||||
import { removeRepetitiveLines } from "./removeRepetitiveLines";
|
import { removeRepetitiveLines } from "./removeRepetitiveLines";
|
||||||
import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition";
|
import { removeLineEndsWithRepetition } from "./removeLineEndsWithRepetition";
|
||||||
import { limitScope } from "./limitScope";
|
import { limitScope } from "./limitScope";
|
||||||
|
import { formatIndentation } from "./formatIndentation";
|
||||||
import { trimSpace } from "./trimSpace";
|
import { trimSpace } from "./trimSpace";
|
||||||
import { dropDuplicated } from "./dropDuplicated";
|
import { dropDuplicated } from "./dropDuplicated";
|
||||||
import { dropBlank } from "./dropBlank";
|
import { dropBlank } from "./dropBlank";
|
||||||
|
|
@ -33,6 +34,7 @@ export async function postCacheProcess(
|
||||||
.then(applyFilter(removeRepetitiveBlocks(context), context))
|
.then(applyFilter(removeRepetitiveBlocks(context), context))
|
||||||
.then(applyFilter(removeRepetitiveLines(context), context))
|
.then(applyFilter(removeRepetitiveLines(context), context))
|
||||||
.then(applyFilter(limitScope(context, config["limitScope"]), context))
|
.then(applyFilter(limitScope(context, config["limitScope"]), context))
|
||||||
|
.then(applyFilter(formatIndentation(context), 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));
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,7 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl
|
||||||
language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers
|
language: document.languageId, // https://code.visualstudio.com/docs/languages/identifiers
|
||||||
text: additionalContext.prefix + document.getText() + additionalContext.suffix,
|
text: additionalContext.prefix + document.getText() + additionalContext.suffix,
|
||||||
position: additionalContext.prefix.length + document.offsetAt(position),
|
position: additionalContext.prefix.length + document.offsetAt(position),
|
||||||
|
indentation: this.getEditorIndentation(),
|
||||||
clipboard: await env.clipboard.readText(),
|
clipboard: await env.clipboard.readText(),
|
||||||
manually: context.triggerKind === InlineCompletionTriggerKind.Invoke,
|
manually: context.triggerKind === InlineCompletionTriggerKind.Invoke,
|
||||||
};
|
};
|
||||||
|
|
@ -166,6 +167,21 @@ export class TabbyCompletionProvider extends EventEmitter implements InlineCompl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private getEditorIndentation(): string | undefined {
|
||||||
|
const editor = window.activeTextEditor;
|
||||||
|
if (!editor) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { insertSpaces, tabSize } = editor.options;
|
||||||
|
if (insertSpaces && typeof tabSize === "number" && tabSize > 0) {
|
||||||
|
return " ".repeat(tabSize);
|
||||||
|
} else if (!insertSpaces) {
|
||||||
|
return "\t";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
private updateConfiguration() {
|
private updateConfiguration() {
|
||||||
if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) {
|
if (!workspace.getConfiguration("editor").get("inlineSuggest.enabled", true)) {
|
||||||
this.triggerMode = "disabled";
|
this.triggerMode = "disabled";
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue