Skip to content

Commit

Permalink
feat(vscode): show inline diff of charactors
Browse files Browse the repository at this point in the history
  • Loading branch information
zhanba committed Jan 19, 2025
1 parent 5aa27b5 commit 90e9e70
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 4 deletions.
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

0 comments on commit 90e9e70

Please sign in to comment.