feat: open edit tool changes in VS Code diff view with line focus
Tool-card clicks for edit-like actions now open a VS Code diff instead of only the file Diff view focuses the first changed line for faster review Uses a virtual readonly original buffer to avoid opening unsaved draft tabs
This commit is contained in:
@@ -169,6 +169,130 @@ const readUriAsAttachment = async (
|
||||
}
|
||||
};
|
||||
|
||||
type ParsedDiffHunk = {
|
||||
newStart: number;
|
||||
oldLines: string[];
|
||||
newLines: string[];
|
||||
};
|
||||
|
||||
const VIRTUAL_DIFF_SCHEME = 'openchamber-diff';
|
||||
const virtualDiffContents = new Map<string, string>();
|
||||
let virtualDiffCounter = 0;
|
||||
let virtualDiffProviderDisposable: vscode.Disposable | null = null;
|
||||
|
||||
const ensureVirtualDiffProviderRegistered = (ctx?: BridgeContext): void => {
|
||||
if (virtualDiffProviderDisposable) {
|
||||
return;
|
||||
}
|
||||
|
||||
virtualDiffProviderDisposable = vscode.workspace.registerTextDocumentContentProvider(
|
||||
VIRTUAL_DIFF_SCHEME,
|
||||
{
|
||||
provideTextDocumentContent: (uri: vscode.Uri) => {
|
||||
const key = new URLSearchParams(uri.query).get('key') || '';
|
||||
return virtualDiffContents.get(key) ?? '';
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (ctx?.context) {
|
||||
ctx.context.subscriptions.push(virtualDiffProviderDisposable);
|
||||
}
|
||||
};
|
||||
|
||||
const createVirtualOriginalDiffUri = (modifiedPath: string, content: string): vscode.Uri => {
|
||||
const key = `${Date.now()}-${++virtualDiffCounter}`;
|
||||
virtualDiffContents.set(key, content);
|
||||
|
||||
if (virtualDiffContents.size > 100) {
|
||||
const firstKey = virtualDiffContents.keys().next().value;
|
||||
if (typeof firstKey === 'string') {
|
||||
virtualDiffContents.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
const fileName = path.basename(modifiedPath) || 'file';
|
||||
return vscode.Uri.from({
|
||||
scheme: VIRTUAL_DIFF_SCHEME,
|
||||
path: `/${fileName} (before)`,
|
||||
query: `key=${encodeURIComponent(key)}`,
|
||||
});
|
||||
};
|
||||
|
||||
const parseUnifiedDiffHunks = (patch: string): ParsedDiffHunk[] => {
|
||||
if (typeof patch !== 'string' || patch.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const lines = patch.split('\n');
|
||||
const hunks: ParsedDiffHunk[] = [];
|
||||
let current: ParsedDiffHunk | null = null;
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.replace(/\r$/, '');
|
||||
const headerMatch = line.match(/^@@\s+-\d+(?:,\d+)?\s+\+(\d+)(?:,\d+)?\s+@@/);
|
||||
if (headerMatch) {
|
||||
if (current) {
|
||||
hunks.push(current);
|
||||
}
|
||||
const newStart = Number.parseInt(headerMatch[1] ?? '', 10);
|
||||
current = {
|
||||
newStart: Number.isFinite(newStart) ? Math.max(1, newStart) : 1,
|
||||
oldLines: [],
|
||||
newLines: [],
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!current) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith(' ')) {
|
||||
const text = line.slice(1);
|
||||
current.oldLines.push(text);
|
||||
current.newLines.push(text);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('+') && !line.startsWith('+++')) {
|
||||
current.newLines.push(line.slice(1));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('-') && !line.startsWith('---')) {
|
||||
current.oldLines.push(line.slice(1));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (current) {
|
||||
hunks.push(current);
|
||||
}
|
||||
|
||||
return hunks;
|
||||
};
|
||||
|
||||
const reconstructOriginalContentFromPatch = (modifiedContent: string, patch: string): string | null => {
|
||||
const hunks = parseUnifiedDiffHunks(patch);
|
||||
if (hunks.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = modifiedContent.split('\n');
|
||||
for (let index = hunks.length - 1; index >= 0; index -= 1) {
|
||||
const hunk = hunks[index];
|
||||
if (!hunk) {
|
||||
continue;
|
||||
}
|
||||
const startIndex = Math.max(0, hunk.newStart - 1);
|
||||
const replaceCount = hunk.newLines.length;
|
||||
lines.splice(startIndex, replaceCount, ...hunk.oldLines);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
};
|
||||
|
||||
const isPathInside = (candidatePath: string, parentPath: string): boolean => {
|
||||
const normalizedCandidate = path.resolve(candidatePath);
|
||||
const normalizedParent = path.resolve(parentPath);
|
||||
@@ -2783,16 +2907,44 @@ export async function handleBridgeMessage(message: BridgeRequest, ctx?: BridgeCo
|
||||
}
|
||||
|
||||
case 'editor:openDiff': {
|
||||
const { original, modified, label } = payload as { original: string; modified: string; label?: string };
|
||||
const { original, modified, label, line, patch } = payload as {
|
||||
original: string;
|
||||
modified: string;
|
||||
label?: string;
|
||||
line?: number;
|
||||
patch?: string;
|
||||
};
|
||||
try {
|
||||
// If the paths are just content, we need to create virtual documents or temp files.
|
||||
// However, 'editor:openDiff' usually implies comparing two URIs.
|
||||
// If the payload contains file paths:
|
||||
const originalUri = vscode.Uri.file(original);
|
||||
const modifiedUri = vscode.Uri.file(modified);
|
||||
const title = label || `${path.basename(original)} ↔ ${path.basename(modified)}`;
|
||||
|
||||
const modifiedDoc = await vscode.workspace.openTextDocument(modifiedUri);
|
||||
let originalUri = original ? vscode.Uri.file(original) : modifiedUri;
|
||||
|
||||
if (typeof patch === 'string' && patch.trim().length > 0) {
|
||||
const originalContent = reconstructOriginalContentFromPatch(modifiedDoc.getText(), patch);
|
||||
if (typeof originalContent === 'string') {
|
||||
ensureVirtualDiffProviderRegistered(ctx);
|
||||
originalUri = createVirtualOriginalDiffUri(modified, originalContent);
|
||||
}
|
||||
}
|
||||
|
||||
const leftLabel = original ? path.basename(original) : `${path.basename(modified)} (before)`;
|
||||
const title = label || `${leftLabel} ↔ ${path.basename(modified)}`;
|
||||
|
||||
await vscode.commands.executeCommand('vscode.diff', originalUri, modifiedUri, title);
|
||||
|
||||
if (typeof line === 'number' && Number.isFinite(line)) {
|
||||
const targetLine = Math.max(0, Math.trunc(line) - 1);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
const targetEditor = vscode.window.visibleTextEditors.find(
|
||||
(editor) => editor.document.uri.toString() === modifiedUri.toString(),
|
||||
);
|
||||
if (targetEditor) {
|
||||
const target = new vscode.Position(targetLine, 0);
|
||||
targetEditor.selection = new vscode.Selection(target, target);
|
||||
targetEditor.revealRange(new vscode.Range(target, target), vscode.TextEditorRevealType.InCenter);
|
||||
}
|
||||
}
|
||||
|
||||
return { id, type, success: true };
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
Reference in New Issue
Block a user