singularity-forge/vscode-extension/src/line-decorations.ts
2026-05-05 14:31:16 +02:00

148 lines
4.3 KiB
TypeScript

import * as vscode from "vscode";
import type { SfChangeTracker } from "./change-tracker.js";
/**
* Provides line-level editor decorations for files modified by the SF agent.
* Shows subtle background highlights on changed lines and gutter icons.
*/
export class SfLineDecorationManager implements vscode.Disposable {
private readonly addedDecoration: vscode.TextEditorDecorationType;
private readonly modifiedDecoration: vscode.TextEditorDecorationType;
private readonly gutterDecoration: vscode.TextEditorDecorationType;
private disposables: vscode.Disposable[] = [];
constructor(private readonly tracker: SfChangeTracker) {
this.addedDecoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
backgroundColor: "rgba(78, 201, 176, 0.07)",
overviewRulerColor: "rgba(78, 201, 176, 0.5)",
overviewRulerLane: vscode.OverviewRulerLane.Left,
});
this.modifiedDecoration = vscode.window.createTextEditorDecorationType({
isWholeLine: true,
backgroundColor: "rgba(204, 167, 0, 0.07)",
overviewRulerColor: "rgba(204, 167, 0, 0.5)",
overviewRulerLane: vscode.OverviewRulerLane.Left,
});
this.gutterDecoration = vscode.window.createTextEditorDecorationType({
gutterIconPath: new vscode.ThemeIcon("hubot").id, // fallback
gutterIconSize: "contain",
// Use a colored left border as a gutter indicator (more reliable than icons)
borderWidth: "0 0 0 3px",
borderStyle: "solid",
borderColor: "rgba(78, 201, 176, 0.4)",
});
this.disposables.push(
this.addedDecoration,
this.modifiedDecoration,
this.gutterDecoration,
);
// Refresh decorations when tracked files change
this.disposables.push(
tracker.onDidChange(() => this.refreshAll()),
vscode.window.onDidChangeActiveTextEditor(() => this.refreshAll()),
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor;
if (editor && e.document === editor.document) {
this.refreshEditor(editor);
}
}),
);
}
private refreshAll(): void {
for (const editor of vscode.window.visibleTextEditors) {
this.refreshEditor(editor);
}
}
private refreshEditor(editor: vscode.TextEditor): void {
const filePath = editor.document.uri.fsPath;
const original = this.tracker.getOriginal(filePath);
if (original === undefined) {
// No tracked changes for this file — clear decorations
editor.setDecorations(this.addedDecoration, []);
editor.setDecorations(this.modifiedDecoration, []);
editor.setDecorations(this.gutterDecoration, []);
return;
}
const currentLines = editor.document.getText().split("\n");
const originalLines = original.split("\n");
const { added, modified } = diffLines(originalLines, currentLines);
const addedRanges = added.map((line) => {
const range = new vscode.Range(
line,
0,
line,
currentLines[line]?.length ?? 0,
);
return {
range,
hoverMessage: new vscode.MarkdownString("$(hubot) *Added by SF Agent*"),
};
});
const modifiedRanges = modified.map((line) => {
const range = new vscode.Range(
line,
0,
line,
currentLines[line]?.length ?? 0,
);
return {
range,
hoverMessage: new vscode.MarkdownString(
"$(hubot) *Modified by SF Agent*",
),
};
});
const gutterRanges = [...added, ...modified].map((line) => ({
range: new vscode.Range(line, 0, line, 0),
}));
editor.setDecorations(this.addedDecoration, addedRanges);
editor.setDecorations(this.modifiedDecoration, modifiedRanges);
editor.setDecorations(this.gutterDecoration, gutterRanges);
}
dispose(): void {
for (const d of this.disposables) {
d.dispose();
}
}
}
/**
* Simple line-level diff: compare original vs current line-by-line.
* Returns arrays of line numbers that were added or modified.
*/
function diffLines(
originalLines: string[],
currentLines: string[],
): { added: number[]; modified: number[] } {
const added: number[] = [];
const modified: number[] = [];
const maxShared = Math.min(originalLines.length, currentLines.length);
for (let i = 0; i < maxShared; i++) {
if (originalLines[i] !== currentLines[i]) {
modified.push(i);
}
}
// Lines beyond original length are "added"
for (let i = originalLines.length; i < currentLines.length; i++) {
added.push(i);
}
return { added, modified };
}