Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(vscode): show inline diff of characters #3709

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions clients/tabby-agent/src/codeLens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
import { ClientCapabilities, ServerCapabilities, CodeLens, CodeLensType, ChangesPreviewLineType } from "./protocol";
import { TextDocuments } from "./lsp/textDocuments";
import { TextDocument } from "vscode-languageserver-textdocument";
import { getLogger } from "./logger";
import { calcCharDiffRange } from "./utils/diff";

const codeLensType: CodeLensType = "previewChanges";
const changesPreviewLineType = {
Expand All @@ -25,6 +27,8 @@ const changesPreviewLineType = {
deleted: "deleted" as ChangesPreviewLineType,
};

const logger = getLogger("CodeLensProvider");

export class CodeLensProvider implements Feature {
constructor(private readonly documents: TextDocuments<TextDocument>) {}

Expand Down Expand Up @@ -57,6 +61,9 @@ export class CodeLensProvider implements Feature {
const codeLenses: CodeLens[] = [];
let lineInPreviewBlock = -1;
let previewBlockMarkers = "";
const originLines: string[] = [];
const editLines: string[] = [];
const editCodeLenses: CodeLens[] = [];
for (let line = textDocument.lineCount - 1; line >= 0; line = line - 1) {
if (token.isCancellationRequested) {
return null;
Expand Down Expand Up @@ -166,6 +173,9 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.waiting,
},
};
originLines.unshift(text);
editLines.unshift(text);
editCodeLenses.unshift(codeLens);
break;
case "|":
codeLens = {
Expand All @@ -175,6 +185,8 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.inProgress,
},
};
editLines.unshift(text);
editCodeLenses.unshift(codeLens);
break;
case "=":
codeLens = {
Expand All @@ -184,6 +196,9 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.unchanged,
},
};
originLines.unshift(text);
editLines.unshift(text);
editCodeLenses.unshift(codeLens);
break;
case "+":
codeLens = {
Expand All @@ -193,6 +208,8 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.inserted,
},
};
editLines.unshift(text);
editCodeLenses.unshift(codeLens);
break;
case "-":
codeLens = {
Expand All @@ -202,6 +219,7 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.deleted,
},
};
originLines.unshift(text);
break;
default:
break;
Expand All @@ -219,6 +237,22 @@ export class CodeLensProvider implements Feature {
}
}
}
const charDiffDecorationLenses = calcCharDiffRange(
originLines.join(""),
editLines.join(""),
editCodeLenses.map((item) => item.range),
).map<CodeLens>((codeLensRange) => {
return {
range: codeLensRange,
data: {
type: codeLensType,
text: "inserted" as const,
},
};
});
codeLenses.push(...charDiffDecorationLenses);
logger.debug(`codeLenses: ${JSON.stringify(codeLenses)}`);

workDoneProgress?.done();
if (resultProgress) {
return null;
Expand Down
3 changes: 3 additions & 0 deletions clients/tabby-agent/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ export type CodeLens = LspCodeLens & {
data?: {
type: CodeLensType;
line?: ChangesPreviewLineType;
text?: ChangesPreviewTextType;
};
};

Expand All @@ -290,6 +291,8 @@ export type ChangesPreviewLineType =
| "inserted"
| "deleted";

export type ChangesPreviewTextType = "inserted" | "deleted";

/**
* Extends LSP method Completion Request(↩️)
*
Expand Down
78 changes: 78 additions & 0 deletions clients/tabby-agent/src/utils/diff.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Range } from "vscode-languageserver";
import { calcCharDiffRange } from "./diff";
import { expect } from "chai";

describe("diff", () => {
describe("calcCharDiffRange", () => {
it("diff chars test case 1", () => {
/**
* <<<<<<< tabby-000000
* Deleted Line
* Unchanged Line
* Added Line
* Modified Line before
* Modified Line after
* >>>>>>> tabby-000000 [-=+-+]
*/
const originText = `Deleted Line
Unchanged Line
Modified Line (Before Changes)`;
const editText = `Unchanged Line
Added Line
Modified Line (After Changes)`;
const editTextRanges: Range[] = [
{ start: { line: 2, character: 0 }, end: { line: 2, character: 14 } },
{ start: { line: 3, character: 0 }, end: { line: 3, character: 10 } },
{ start: { line: 5, character: 0 }, end: { line: 5, character: 29 } },
];
const ranges = calcCharDiffRange(originText, editText, editTextRanges);
const diffRanges = [
{ start: { line: 2, character: 0 }, end: { line: 2, character: 7 } },
{ start: { line: 3, character: 0 }, end: { line: 3, character: 3 } },
{ start: { line: 5, character: 15 }, end: { line: 5, character: 18 } },
];
expect(ranges).to.deep.equal(diffRanges);
});

it("diff chars test case 2", () => {
/**
* <<<<<<< tabby-8td8Ip
* examples/assets/downloads/*
* !examples/assets/downloads/.tracked
* examples/headless/outputs/*
* !examples/headless/outputs/.tracked
*
* examples/Assets/downloads/*
* !examples/Assets/downloads/.tracked
* examples/Headless/outputs/*
* !examples/Headless/outputs/.tracked
* >>>>>>> tabby-8td8Ip [----+++++]
*/
const originText = `examples/assets/downloads/*
!examples/assets/downloads/.tracked
examples/headless/outputs/*
!examples/headless/outputs/.tracked`;
const editText = `
examples/Assets/downloads/*
!examples/Assets/downloads/.tracked
examples/Headless/outputs/*
!examples/Headless/outputs/.tracked`;
const editTextRanges: Range[] = [
{ start: { line: 9, character: 0 }, end: { line: 9, character: 0 } },
{ start: { line: 10, character: 0 }, end: { line: 10, character: 27 } },
{ start: { line: 11, character: 0 }, end: { line: 11, character: 35 } },
{ start: { line: 12, character: 0 }, end: { line: 12, character: 27 } },
{ start: { line: 13, character: 0 }, end: { line: 13, character: 35 } },
];
const ranges = calcCharDiffRange(originText, editText, editTextRanges);
const diffRanges = [
{ start: { line: 9, character: 0 }, end: { line: 9, character: 1 } },
{ start: { line: 10, character: 9 }, end: { line: 10, character: 10 } },
{ start: { line: 11, character: 10 }, end: { line: 11, character: 11 } },
{ start: { line: 12, character: 9 }, end: { line: 12, character: 10 } },
{ start: { line: 13, character: 10 }, end: { line: 13, character: 11 } },
];
expect(ranges).to.deep.equal(diffRanges);
});
});
});
52 changes: 52 additions & 0 deletions clients/tabby-agent/src/utils/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Position, Range } from "vscode-languageserver";
import { diffChars } from "diff";

export function calcCharDiffRange(originText: string, editText: string, editTextRanges: Range[]): Range[] {
const diffRanges: Range[] = [];
const changes = diffChars(originText, editText);
let index = 0;
changes.forEach((item) => {
if (item.added) {
const position = getPositionFromIndex(index, editTextRanges);
const addedRange: Range = {
start: position,
end: { line: position.line, character: position.character + (item.count ?? 0) },
};
diffRanges.push(addedRange);
index += item.count ?? 0;
} else if (item.removed) {
// nothing
} else {
index += item.count ?? 0;
}
});
return diffRanges;
}

export function getPositionFromIndex(index: number, ranges: Range[]): Position {
let line = 0;
let character = 0;
let length = 0;
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i];
if (!range) {
continue;
}
const rangeLength = range.end.character - range.start.character + length + 1;
if (index >= length && index < rangeLength) {
line = range.start.line;
character = index - length;
return {
line,
character,
};
} else {
length = rangeLength;
}
}

return {
line,
character,
};
}
24 changes: 20 additions & 4 deletions clients/vscode/src/lsp/CodeLensMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,18 @@ const decorationTypePending = window.createTextEditorDecorationType({
isWholeLine: true,
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
});
const decorationTypeInserted = window.createTextEditorDecorationType({
const decorationTypeTextInserted = window.createTextEditorDecorationType({
backgroundColor: new ThemeColor("diffEditor.insertedTextBackground"),
isWholeLine: false,
rangeBehavior: DecorationRangeBehavior.ClosedOpen,
});
const decorationTypeLineInserted = window.createTextEditorDecorationType({
backgroundColor: new ThemeColor("diffEditor.insertedLineBackground"),
isWholeLine: true,
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
});
const decorationTypeDeleted = window.createTextEditorDecorationType({
backgroundColor: new ThemeColor("diffEditor.removedTextBackground"),
backgroundColor: new ThemeColor("diffEditor.removedLineBackground"),
isWholeLine: true,
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
});
Expand All @@ -55,12 +60,16 @@ const decorationTypes: Record<string, TextEditorDecorationType> = {
commentsFirstLine: decorationTypeComments,
comments: decorationTypeComments,
waiting: decorationTypePending,
inProgress: decorationTypeInserted,
inProgress: decorationTypeLineInserted,
unchanged: decorationTypeUnchanged,
inserted: decorationTypeInserted,
inserted: decorationTypeLineInserted,
deleted: decorationTypeDeleted,
};

const textDecorationTypes: Record<string, TextEditorDecorationType> = {
inserted: decorationTypeTextInserted,
};

export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware {
private readonly decorationMap = new Map<TextEditor, Map<TextEditorDecorationType, Range[]>>();

Expand Down Expand Up @@ -100,6 +109,13 @@ export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware {
this.addDecorationRange(editor, decorationType, decorationRange);
}
}
const textType = codeLens.data.text;
if (typeof textType === "string" && textType in textDecorationTypes) {
const decorationType = textDecorationTypes[textType];
if (decorationType) {
this.addDecorationRange(editor, decorationType, decorationRange);
}
}
if (codeLens.data.line === "header") {
return codeLens;
}
Expand Down
Loading