Skip to content

Commit

Permalink
chore(vscode): use codelens for inline edit actions / decorations (Ta…
Browse files Browse the repository at this point in the history
…bbyML#3000)

* refactor(vscode): disable noUnusedLocals and update build script. Extract InlineEditController

* refactor(inline-edit): reorganize InlineChatEdit

* restructure tsconfig.json

* build(clients/tabby-agent): update watch script to modify dist/protocol.d.ts on success, to help trigger watch mode of vscode extension

* streaming at line break

* Let CodeLensProvider using footer's markers

* turn off background color of header / footer to avoid flickering

* support cancel from codelens

* update

* update

* update

* update

* [autofix.ci] apply automated fixes

* fix

* chore(agent): when apply inline edit, apply the first edit as soon as possible so codelens can display immediately

* add highlight for in progress edit

* update

* [autofix.ci] apply automated fixes

* update

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
wsxiaoys and autofix-ci[bot] authored Aug 29, 2024
1 parent eafd8c6 commit 08a326c
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 236 deletions.
3 changes: 2 additions & 1 deletion clients/tabby-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"types": "./dist/protocol.d.ts",
"scripts": {
"build": "tsup --minify",
"watch": "tsup --watch",
"watch": "tsup --watch --onSuccess \"echo '' >> dist/protocol.d.ts\"",
"vscode:dev": "pnpm watch",
"openapi-codegen": "openapi-typescript ./openapi/tabby.json -o ./src/types/tabbyApi.d.ts",
"test": "mocha",
"lint": "eslint --ext .ts ./src && prettier --check .",
Expand Down
109 changes: 77 additions & 32 deletions clients/tabby-agent/src/lsp/ChatEditProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,25 +183,35 @@ export class ChatEditProvider {
}

async resolveEdit(params: ChatEditResolveParams): Promise<boolean> {
if (params.action === "cancel") {
this.mutexAbortController?.abort();
return false;
}

const document = this.documents.get(params.location.uri);
if (!document) {
return false;
}
const header = document.getText({
start: {
line: params.location.range.start.line,
character: 0,
},
end: {
line: params.location.range.start.line + 1,
character: 0,
},
});
const match = /^<<<<<<<.+(<.*>)\[(tabby-[0-9|a-z|A-Z]{6})\]/g.exec(header);
const markers = match?.[1];
if (!match || !markers) {

let markers;
let line = params.location.range.start.line;
for (; line < document.lineCount; line++) {
const lineText = document.getText({
start: { line, character: 0 },
end: { line: line + 1, character: 0 },
});

const match = /^>>>>>>> (tabby-[0-9|a-z|A-Z]{6}) (\[.*\])/g.exec(lineText);
markers = match?.[2];
if (markers) {
break;
}
}

if (!markers) {
return false;
}

const previewRange = {
start: {
line: params.location.range.start.line,
Expand Down Expand Up @@ -257,6 +267,32 @@ export class ChatEditProvider {
responseCommentTag?: string[],
): Promise<void> {
const applyEdit = async (edit: Edit, isFirst: boolean = false, isLast: boolean = false) => {
if (isFirst) {
const workspaceEdit: WorkspaceEdit = {
changes: {
[edit.location.uri]: [
{
range: edit.editedRange,
newText: `<<<<<<< ${edit.id}\n`,
},
],
},
};

await this.applyWorkspaceEdit({
edit: workspaceEdit,
options: {
undoStopBefore: true,
undoStopAfter: false,
},
});

edit.editedRange = {
start: { line: edit.editedRange.start.line + 1, character: 0 },
end: { line: edit.editedRange.start.line + 1, character: 0 },
};
}

const editedLines = this.generateChangesPreview(edit);
const workspaceEdit: WorkspaceEdit = {
changes: {
Expand All @@ -272,7 +308,7 @@ export class ChatEditProvider {
await this.applyWorkspaceEdit({
edit: workspaceEdit,
options: {
undoStopBefore: isFirst,
undoStopBefore: false,
undoStopAfter: isLast,
},
});
Expand Down Expand Up @@ -321,11 +357,17 @@ export class ChatEditProvider {
};

try {
if (!this.currentEdit) {
throw new Error("No current edit");
}

let inTag: "document" | "comment" | false = false;
let isFirstEdit = true;

// Insert the first line as early as possible so codelens can be shown
await applyEdit(this.currentEdit, true, false);

for await (const delta of stream) {
if (!this.currentEdit || !this.mutexAbortController || this.mutexAbortController.signal.aborted) {
if (!this.mutexAbortController || this.mutexAbortController.signal.aborted) {
break;
}

Expand All @@ -341,8 +383,9 @@ export class ChatEditProvider {
const closeTag = inTag === "document" ? responseDocumentTag[1] : responseCommentTag?.[1];
if (!closeTag || !openTag) break;
inTag = processBuffer(edit, inTag, openTag, closeTag);
await applyEdit(edit, isFirstEdit, false);
isFirstEdit = false;
if (delta.includes("\n")) {
await applyEdit(edit, false, false);
}
}
}

Expand Down Expand Up @@ -393,20 +436,14 @@ export class ChatEditProvider {
// [+] inserted
// [-] deleted
// [>] footer
// [x] stopped
// footer line
// >>>>>>> End of changes
private generateChangesPreview(edit: Edit): string[] {
const lines: string[] = [];
let markers = "";
// header
let stateDescription = "Editing in progress";
if (edit.state === "stopped") {
stateDescription = "Editing stopped";
} else if (edit.state == "completed") {
stateDescription = "Editing completed";
}
lines.push(`<<<<<<< ${stateDescription} {{markers}}[${edit.id}]`);
markers += "<";
// lines.push(`<<<<<<< ${stateDescription} {{markers}}[${edit.id}]`);
markers += "[";
// comments: split by new line or 80 chars
const commentLines = edit.comments
.trim()
Expand Down Expand Up @@ -468,22 +505,30 @@ export class ChatEditProvider {
lineIndex++;
}
if (inProgressChunk && lastDiff) {
pushDiffValue(lastDiff.value, "|");
if (edit.state === "stopped") {
pushDiffValue(lastDiff.value, "x");
} else {
pushDiffValue(lastDiff.value, "|");
}
}
while (lineIndex < diffs.length - inProgressChunk) {
const diff = diffs[lineIndex];
if (!diff) {
break;
}
pushDiffValue(diff.value, ".");
if (edit.state === "stopped") {
pushDiffValue(diff.value, "x");
} else {
pushDiffValue(diff.value, ".");
}
lineIndex++;
}
}
// footer
lines.push(`>>>>>>> ${stateDescription} {{markers}}[${edit.id}]`);
markers += ">";
lines.push(`>>>>>>> ${edit.id} {{markers}}`);
markers += "]";
// replace markers
lines[0] = lines[0]!.replace("{{markers}}", markers);
// lines[0] = lines[0]!.replace("{{markers}}", markers);
lines[lines.length - 1] = lines[lines.length - 1]!.replace("{{markers}}", markers);
return lines;
}
Expand Down
95 changes: 61 additions & 34 deletions clients/tabby-agent/src/lsp/CodeLensProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export class CodeLensProvider {
const codeLenses: CodeLens[] = [];
let lineInPreviewBlock = -1;
let previewBlockMarkers = "";
for (let line = 0; line < textDocument.lineCount; line++) {
for (let line = textDocument.lineCount - 1; line >= 0; line = line - 1) {
if (token.isCancellationRequested) {
return null;
}
Expand All @@ -64,56 +64,83 @@ export class CodeLensProvider {
start: { line: line, character: 0 },
end: { line: line, character: text.length - 1 },
};

const codeLensLocation: Location = { uri: uri, range: codeLensRange };
const lineCodeLenses: CodeLens[] = [];
if (lineInPreviewBlock < 0) {
const match = /^<<<<<<<.+(<.*>)\[(tabby-[0-9|a-z|A-Z]{6})\]/g.exec(text);
const markers = match?.[1];
const editId = match?.[2];
const match = /^>>>>>>> (tabby-[0-9|a-z|A-Z]{6}) (\[.*\])/g.exec(text);
const editId = match?.[1];
const markers = match?.[2];
if (match && markers && editId) {
lineInPreviewBlock = 0;
previewBlockMarkers = markers;

lineCodeLenses.push({
range: codeLensRange,
command: {
title: "Accept",
command: "tabby/chat/edit/resolve",
arguments: [{ location: codeLensLocation, action: "accept" }],
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
},
});
lineInPreviewBlock = 0;
lineCodeLenses.push({
range: codeLensRange,
command: {
title: "Discard",
command: "tabby/chat/edit/resolve",
arguments: [{ location: codeLensLocation, action: "discard" }],
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
line: changesPreviewLineType.footer,
},
});
}
} else {
const match = /^>>>>>>>.+(<.*>)\[(tabby-[0-9|a-z|A-Z]{6})\]/g.exec(text);
const editId = match?.[2];
const match = /^<<<<<<< (tabby-[0-9|a-z|A-Z]{6})/g.exec(text);
const editId = match?.[1];
if (match && editId) {
lineInPreviewBlock = -1;
lineCodeLenses.push({
range: codeLensRange,
data: {
type: codeLensType,
line: changesPreviewLineType.footer,
},
});

if (previewBlockMarkers.includes(".")) {
lineCodeLenses.push({
range: codeLensRange,
command: {
title: "$(sync~spin) Tabby is working...",
command: " ",
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
},
});
lineCodeLenses.push({
range: codeLensRange,
command: {
title: "Cancel",
command: "tabby/chat/edit/resolve",
arguments: [{ location: codeLensLocation, action: "cancel" }],
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
},
});
} else if (!previewBlockMarkers.includes("x")) {
lineCodeLenses.push({
range: codeLensRange,
command: {
title: "$(check)Accept",
command: "tabby/chat/edit/resolve",
arguments: [{ location: codeLensLocation, action: "accept" }],
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
},
});
lineCodeLenses.push({
range: codeLensRange,
command: {
title: "$(remove-close)Discard",
command: "tabby/chat/edit/resolve",
arguments: [{ location: codeLensLocation, action: "discard" }],
},
data: {
type: codeLensType,
line: changesPreviewLineType.header,
},
});
}
} else {
lineInPreviewBlock++;
const marker = previewBlockMarkers[lineInPreviewBlock];
const marker = previewBlockMarkers[previewBlockMarkers.length - lineInPreviewBlock - 1];
let codeLens: CodeLens | undefined = undefined;
switch (marker) {
case "#":
Expand Down
2 changes: 1 addition & 1 deletion clients/tabby-agent/src/lsp/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ export type ChatEditResolveParams = {
/**
* The action to take for this edit.
*/
action: "accept" | "discard";
action: "accept" | "discard" | "cancel";
};

/**
Expand Down
3 changes: 2 additions & 1 deletion clients/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -423,9 +423,10 @@
}
},
"scripts": {
"build": "tsc --noEmit && tsup --minify",
"build": "tsc -p ./tsconfig.build.json --noEmit && tsup --minify",
"watch": "tsc-watch --noEmit --onSuccess \"tsup\"",
"dev": "code --extensionDevelopmentPath=$PWD --disable-extensions && pnpm watch",
"vscode:dev": "pnpm run dev",
"dev:browser": "vscode-test-web --extensionDevelopmentPath=$PWD --browserType=chromium --port=3000 && pnpm watch",
"lint": "eslint --ext .ts ./src && prettier --check .",
"lint:fix": "eslint --fix --ext .ts ./src && prettier --write .",
Expand Down
Loading

0 comments on commit 08a326c

Please sign in to comment.