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

Merged
merged 2 commits into from
Feb 11, 2025
Merged
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
1 change: 1 addition & 0 deletions clients/tabby-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@typescript-eslint/parser": "^6.13.1",
"chai": "^4.3.7",
"chokidar": "^3.5.3",
"codiff": "^0.1.2",
"crypto-random-string": "^5.0.0",
"dedent": "^0.7.0",
"deep-equal": "^2.2.1",
Expand Down
56 changes: 56 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 { codeDiff } 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,10 @@ export class CodeLensProvider implements Feature {
const codeLenses: CodeLens[] = [];
let lineInPreviewBlock = -1;
let previewBlockMarkers = "";
const originLines: string[] = [];
const modifiedLines: string[] = [];
const modifiedCodeLenses: CodeLens[] = [];
const originCodeLenses: CodeLens[] = [];
for (let line = textDocument.lineCount - 1; line >= 0; line = line - 1) {
if (token.isCancellationRequested) {
return null;
Expand Down Expand Up @@ -166,6 +174,10 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.waiting,
},
};
originLines.unshift(text);
originCodeLenses.unshift(codeLens);
modifiedLines.unshift(text);
modifiedCodeLenses.unshift(codeLens);
break;
case "|":
codeLens = {
Expand All @@ -175,6 +187,8 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.inProgress,
},
};
modifiedLines.unshift(text);
modifiedCodeLenses.unshift(codeLens);
break;
case "=":
codeLens = {
Expand All @@ -184,6 +198,10 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.unchanged,
},
};
originLines.unshift(text);
originCodeLenses.unshift(codeLens);
modifiedLines.unshift(text);
modifiedCodeLenses.unshift(codeLens);
break;
case "+":
codeLens = {
Expand All @@ -193,6 +211,8 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.inserted,
},
};
modifiedLines.unshift(text);
modifiedCodeLenses.unshift(codeLens);
break;
case "-":
codeLens = {
Expand All @@ -202,6 +222,8 @@ export class CodeLensProvider implements Feature {
line: changesPreviewLineType.deleted,
},
};
originLines.unshift(text);
originCodeLenses.unshift(codeLens);
break;
default:
break;
Expand All @@ -219,6 +241,40 @@ export class CodeLensProvider implements Feature {
}
}
}

const { originRanges, modifiedRanges } = codeDiff(
originLines,
originCodeLenses.map((item) => item.range),
modifiedLines,
modifiedCodeLenses.map((item) => item.range),
);
const deletionDecorations = originRanges.map((range) => {
return {
range,
data: {
type: codeLensType,
text: "deleted" as const,
},
};
});

const insertionDecorations = modifiedRanges.map((range) => {
return {
range,
data: {
type: codeLensType,
text: "inserted" as const,
},
};
});

if (resultProgress) {
resultProgress.report([...deletionDecorations, ...insertionDecorations]);
} else {
codeLenses.push(...deletionDecorations, ...insertionDecorations);
}
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
94 changes: 94 additions & 0 deletions clients/tabby-agent/src/utils/diff.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { Range, Position } from "vscode-languageserver";
import { linesDiffComputers, Range as DiffRange } from "codiff";

interface CodeDiffResult {
originRanges: Range[];
modifiedRanges: Range[];
}

export function mapDiffRangeToEditorRange(diffRange: DiffRange, editorRanges: Range[]): Range | undefined {
if (diffRange.isEmpty()) {
return undefined;
}

const start: Position = {
line: editorRanges[diffRange.startLineNumber - 1]?.start.line ?? 0,
character: diffRange.startColumn - 1,
};

let end: Position;

/**
* In most case, start line and end line are equal in diff change.
* when start line and end line are different in change, it usually means a range that include a whole line.
* {
* "startLineNumber": 2,
* "startColumn": 1,
* "endLineNumber": 3,
* "endColumn": 1
* }
*
* In our case, the origin code and modified code are mixed tegether. so we should translate range to below to avoid wrong range mapping.
* {
* "startLineNumber": 2,
* "startColumn": 1,
* "endLineNumber": 2,
* "endColumn": // end of line 2
* }
*
*/
if (diffRange.isSingleLine()) {
end = {
line: editorRanges[diffRange.startLineNumber - 1]?.start.line ?? 0,
character: diffRange.endColumn - 1,
};
} else {
end = {
line: editorRanges[diffRange.startLineNumber - 1]?.start.line ?? 0,
character: editorRanges[diffRange.startLineNumber - 1]?.end.character ?? 0,
};
}

return {
start,
end,
};
}

/**
* Diff code and mapping the diff result range to editor range
*/
export function codeDiff(
originCode: string[],
originCodeRanges: Range[],
modifiedCode: string[],
modifiedCodeRanges: Range[],
): CodeDiffResult {
const originRanges: Range[] = [];
const modifiedRanges: Range[] = [];

const diffResult = linesDiffComputers.getDefault().computeDiff(originCode, modifiedCode, {
computeMoves: false,
ignoreTrimWhitespace: true,
maxComputationTimeMs: 100,
});

diffResult.changes.forEach((change) => {
change.innerChanges?.forEach((innerChange) => {
const originRange = mapDiffRangeToEditorRange(innerChange.originalRange, originCodeRanges);
if (originRange) {
originRanges.push(originRange);
}

const modifiedRange = mapDiffRangeToEditorRange(innerChange.modifiedRange, modifiedCodeRanges);
if (modifiedRange) {
modifiedRanges.push(modifiedRange);
}
});
});

return {
modifiedRanges,
originRanges,
};
}
40 changes: 31 additions & 9 deletions clients/vscode/src/lsp/CodeLensMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,26 +39,41 @@ 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 decorationTypeTextDeleted = window.createTextEditorDecorationType({
backgroundColor: new ThemeColor("diffEditor.removedTextBackground"),
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"),
const decorationTypeLineDeleted = window.createTextEditorDecorationType({
backgroundColor: new ThemeColor("diffEditor.removedLineBackground"),
isWholeLine: true,
rangeBehavior: DecorationRangeBehavior.ClosedClosed,
});
const decorationTypes: Record<string, TextEditorDecorationType> = {
const lineDecorationTypes: Record<string, TextEditorDecorationType> = {
header: decorationTypeHeader,
footer: decorationTypeFooter,
commentsFirstLine: decorationTypeComments,
comments: decorationTypeComments,
waiting: decorationTypePending,
inProgress: decorationTypeInserted,
inProgress: decorationTypeLineInserted,
unchanged: decorationTypeUnchanged,
inserted: decorationTypeInserted,
deleted: decorationTypeDeleted,
inserted: decorationTypeLineInserted,
deleted: decorationTypeLineDeleted,
};

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

export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware {
Expand Down Expand Up @@ -94,8 +109,15 @@ export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware {
codeLens.range.end.character,
);
const lineType = codeLens.data.line;
if (typeof lineType === "string" && lineType in decorationTypes) {
const decorationType = decorationTypes[lineType];
if (typeof lineType === "string" && lineType in lineDecorationTypes) {
const decorationType = lineDecorationTypes[lineType];
if (decorationType) {
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);
}
Expand Down
Loading