diff --git a/clients/tabby-agent/src/codeLens.ts b/clients/tabby-agent/src/codeLens.ts index 848dadd5faf4..0008bf5ca2b8 100644 --- a/clients/tabby-agent/src/codeLens.ts +++ b/clients/tabby-agent/src/codeLens.ts @@ -7,10 +7,13 @@ import { WorkDoneProgressReporter, ResultProgressReporter, CodeLensParams, + Position, } from "vscode-languageserver"; import { ClientCapabilities, ServerCapabilities, CodeLens, CodeLensType, ChangesPreviewLineType } from "./protocol"; import { TextDocuments } from "./lsp/textDocuments"; import { TextDocument } from "vscode-languageserver-textdocument"; +import { getLogger } from "./logger"; +import { diffChars } from "diff"; const codeLensType: CodeLensType = "previewChanges"; const changesPreviewLineType = { @@ -25,6 +28,8 @@ const changesPreviewLineType = { deleted: "deleted" as ChangesPreviewLineType, }; +const logger = getLogger('CodeLensProvider'); + export class CodeLensProvider implements Feature { constructor(private readonly documents: TextDocuments) {} @@ -57,6 +62,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; @@ -166,6 +174,9 @@ export class CodeLensProvider implements Feature { line: changesPreviewLineType.waiting, }, }; + originLines.unshift(text) + editLines.unshift(text) + editCodeLenses.unshift(codeLens) break; case "|": codeLens = { @@ -175,6 +186,8 @@ export class CodeLensProvider implements Feature { line: changesPreviewLineType.inProgress, }, }; + editLines.unshift(text) + editCodeLenses.unshift(codeLens) break; case "=": codeLens = { @@ -184,6 +197,9 @@ export class CodeLensProvider implements Feature { line: changesPreviewLineType.unchanged, }, }; + originLines.unshift(text) + editLines.unshift(text) + editCodeLenses.unshift(codeLens) break; case "+": codeLens = { @@ -193,6 +209,8 @@ export class CodeLensProvider implements Feature { line: changesPreviewLineType.inserted, }, }; + editLines.unshift(text) + editCodeLenses.unshift(codeLens) break; case "-": codeLens = { @@ -202,6 +220,7 @@ export class CodeLensProvider implements Feature { line: changesPreviewLineType.deleted, }, }; + originLines.unshift(text) break; default: break; @@ -219,6 +238,10 @@ export class CodeLensProvider implements Feature { } } } + const charDecorationLenses = this.getCharDiffDecoration(originLines, editLines, editCodeLenses) + codeLenses.push(...charDecorationLenses) + logger.debug(`codeLenses: ${JSON.stringify(codeLenses)}`) + workDoneProgress?.done(); if (resultProgress) { return null; @@ -226,4 +249,61 @@ export class CodeLensProvider implements Feature { return codeLenses; } } + + getCharDiffDecoration(originLines: string[], editLines: string[], editCodeLenses: CodeLens[]) { + const editCharCodeLenses: CodeLens[] = [] + const changes = diffChars(originLines.join(''), editLines.join('')) + let index = 0; + changes.forEach(item => { + if (item.added) { + const position = this.getPositionFromIndex(index, editCodeLenses) + const codeLensRange: Range = { + start: position, + end: { line: position.line, character: position.character + (item.count??0) }, + }; + const codeLens = { + range: codeLensRange, + data: { + type: codeLensType, + text: 'inserted' as const, + }, + }; + editCharCodeLenses.push(codeLens) + } else if(item.removed) { + // nothing + } else { + index += item.count ?? 0 + } + }) + return editCharCodeLenses + } + + getPositionFromIndex(index: number, editCodeLenses: CodeLens[]): Position { + let line = 0; + let character = 0; + let length = 0; + for (let i = 0; i < editCodeLenses.length; i++) { + const item = editCodeLenses[i]; + const range = item?.range; + if (!range) { + continue + } + const rangeLength = range.end.character - range.start.character + length + if (index >= length && index <= rangeLength) { + line = range.start.line + character = index - length + return { + line, + character + } + } else { + length = rangeLength + } + } + + return { + line, + character + } + } } diff --git a/clients/tabby-agent/src/protocol.ts b/clients/tabby-agent/src/protocol.ts index 63006e711d5e..8f9477d7e2f9 100644 --- a/clients/tabby-agent/src/protocol.ts +++ b/clients/tabby-agent/src/protocol.ts @@ -275,6 +275,7 @@ export type CodeLens = LspCodeLens & { data?: { type: CodeLensType; line?: ChangesPreviewLineType; + text?: ChangesPreviewTextType; }; }; @@ -290,6 +291,8 @@ export type ChangesPreviewLineType = | "inserted" | "deleted"; +export type ChangesPreviewTextType = "inserted"| "deleted" + /** * Extends LSP method Completion Request(↩️) * diff --git a/clients/vscode/src/lsp/CodeLensMiddleware.ts b/clients/vscode/src/lsp/CodeLensMiddleware.ts index 9a9cfd28ef84..520f4c42a366 100644 --- a/clients/vscode/src/lsp/CodeLensMiddleware.ts +++ b/clients/vscode/src/lsp/CodeLensMiddleware.ts @@ -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, }); @@ -55,12 +60,16 @@ const decorationTypes: Record = { commentsFirstLine: decorationTypeComments, comments: decorationTypeComments, waiting: decorationTypePending, - inProgress: decorationTypeInserted, + inProgress: decorationTypeLineInserted, unchanged: decorationTypeUnchanged, - inserted: decorationTypeInserted, + inserted: decorationTypeLineInserted, deleted: decorationTypeDeleted, }; +const textDecorationTypes: Record = { + inserted: decorationTypeTextInserted, +} + export class CodeLensMiddleware implements VscodeLspCodeLensMiddleware { private readonly decorationMap = new Map>(); @@ -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; }