Add Open Diff button to open VS Code's native side-by-side diff editor

- Add _openDiffEditor method using vscode.diff command
- Store temp files in extension's storageUri instead of workspace
- Clean up temp files when diff editor is closed
- Force side-by-side mode when opening diff
- Add Open Diff button with red/green icon in summary row

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
andrepimenta
2025-12-01 16:20:11 +00:00
parent 2b1ad70f6b
commit 6c37394015
3 changed files with 157 additions and 6 deletions

View File

@@ -297,6 +297,9 @@ class ClaudeChatProvider {
case 'openFile':
this._openFileInEditor(message.filePath);
return;
case 'openDiff':
this._openDiffEditor(message.oldContent, message.newContent, message.filePath);
return;
case 'createImageFile':
this._createImageFile(message.imageData, message.imageType);
return;
@@ -2340,6 +2343,74 @@ class ClaudeChatProvider {
}
}
private async _openDiffEditor(oldContent: string, newContent: string, filePath: string) {
try {
const storageUri = this._context.storageUri;
if (!storageUri) {
vscode.window.showErrorMessage('No storage location available');
return;
}
const baseName = path.basename(filePath);
const ext = path.extname(filePath);
const nameWithoutExt = baseName.slice(0, -ext.length) || baseName;
const timestamp = Date.now();
// Create temp files in extension's storage directory
const tempDirUri = vscode.Uri.joinPath(storageUri, 'diff-temp');
// Ensure temp directory exists
try {
await vscode.workspace.fs.createDirectory(tempDirUri);
} catch {
// Directory might already exist, ignore error
}
const oldUri = vscode.Uri.joinPath(tempDirUri, `${nameWithoutExt}.old.${timestamp}${ext}`);
const newUri = vscode.Uri.joinPath(tempDirUri, `${nameWithoutExt}.new.${timestamp}${ext}`);
// Write content to temp files using VS Code filesystem API
await vscode.workspace.fs.writeFile(oldUri, Buffer.from(oldContent, 'utf8'));
await vscode.workspace.fs.writeFile(newUri, Buffer.from(newContent, 'utf8'));
// Ensure side-by-side diff mode is enabled
const diffConfig = vscode.workspace.getConfiguration('diffEditor');
const wasInlineMode = diffConfig.get('renderSideBySide') === false;
if (wasInlineMode) {
await diffConfig.update('renderSideBySide', true, vscode.ConfigurationTarget.Global);
}
// Open diff editor
await vscode.commands.executeCommand('vscode.diff', oldUri, newUri, `${baseName} (Changes)`);
// Track which files need to be cleaned up
const filesToCleanup = new Set([oldUri.toString(), newUri.toString()]);
// Listen for document close events to clean up temp files
const closeListener = vscode.workspace.onDidCloseTextDocument(async (doc) => {
if (filesToCleanup.has(doc.uri.toString())) {
filesToCleanup.delete(doc.uri.toString());
try {
await vscode.workspace.fs.delete(doc.uri, { useTrash: false });
} catch {
// File might already be deleted, ignore
}
// Dispose listener when both files are cleaned up
if (filesToCleanup.size === 0) {
closeListener.dispose();
}
}
});
// Also add to disposables to clean up on extension deactivate
this._disposables.push(closeListener);
} catch (error) {
vscode.window.showErrorMessage(`Failed to open diff editor: ${error}`);
console.error('Error opening diff editor:', error);
}
}
private async _createImageFile(imageData: string, imageType: string) {
try {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];

View File

@@ -16,6 +16,23 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
let planModeEnabled = false;
let thinkingModeEnabled = false;
// Storage for diff data to enable "Open Diff" functionality
const diffDataStore = {};
function openDiffEditor(diffId) {
const data = diffDataStore[diffId];
if (!data) {
console.error('Diff data not found for id:', diffId);
return;
}
vscode.postMessage({
type: 'openDiff',
oldContent: data.oldString,
newContent: data.newString,
filePath: data.filePath
});
}
function shouldAutoScroll(messagesDiv) {
const threshold = 100; // pixels from bottom
const scrollTop = messagesDiv.scrollTop;
@@ -509,9 +526,23 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
const newLines = newString.split('\\n');
const diff = computeLineDiff(oldLines, newLines);
// Generate unique ID for this diff
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
// Store diff data for "Open Diff" functionality
diffDataStore[diffId] = {
oldString: oldString,
newString: newString,
filePath: filePath
};
let html = '';
const formattedPath = formatFilePath(filePath);
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(filePath) + '\\\')">' + formattedPath + '</div>\\n';
// Header with file path
html += '<div class="diff-file-header">';
html += '<div class="diff-file-path" onclick="openFileInEditor(\\\'' + escapeHtml(filePath) + '\\\')">' + formattedPath + '</div>';
html += '</div>\\n';
// Calculate line range
let firstLine = startLine;
@@ -534,7 +565,6 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
let oldLineNum = startLine;
let newLineNum = startLine;
const maxLines = 6;
const diffId = 'diff_' + Math.random().toString(36).substr(2, 9);
let lineIndex = 0;
// First pass: build all line HTML
@@ -598,7 +628,12 @@ const getScript = (isTelemetryEnabled: boolean) => `<script>
}
if (summary) {
html += '<div class="diff-summary">Summary: ' + summary + '</div>';
html += '<div class="diff-summary-row">';
html += '<span class="diff-summary">Summary: ' + summary + '</span>';
html += '<button class="diff-open-btn" onclick="openDiffEditor(\\\'' + diffId + '\\\')" title="Open side-by-side diff in VS Code">';
html += '<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><rect x="9" y="1" width="6" height="14" rx="1" fill="none" stroke="currentColor" stroke-opacity="0.5"/><line x1="2.5" y1="4" x2="5.5" y2="4" stroke="#f85149" stroke-width="1.5"/><line x1="2.5" y1="7" x2="5.5" y2="7" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="2.5" y1="10" x2="5.5" y2="10" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="4" x2="13.5" y2="4" stroke="currentColor" stroke-opacity="0.4" stroke-width="1.5"/><line x1="10.5" y1="7" x2="13.5" y2="7" stroke="#3fb950" stroke-width="1.5"/><line x1="10.5" y1="10" x2="13.5" y2="10" stroke="#3fb950" stroke-width="1.5"/></svg>';
html += 'Open Diff</button>';
html += '</div>';
}
return html;

View File

@@ -1150,14 +1150,21 @@ const styles = `
margin: 12px 0;
}
.diff-summary {
.diff-summary-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-top: 8px;
padding: 8px 12px;
padding: 6px 12px;
border-top: 1px solid var(--vscode-panel-border);
background-color: var(--vscode-editor-background);
}
.diff-summary {
color: var(--vscode-descriptionForeground);
font-size: 11px;
font-weight: 500;
background-color: var(--vscode-editor-background);
}
.diff-preview {
@@ -1169,6 +1176,14 @@ const styles = `
}
/* File path display styles */
.diff-file-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.diff-file-path {
padding: 8px 12px;
border: 1px solid var(--vscode-panel-border);
@@ -1176,6 +1191,7 @@ const styles = `
font-size: 12px;
cursor: pointer;
transition: all 0.2s ease;
flex: 1;
}
.diff-file-path:hover {
@@ -1187,6 +1203,35 @@ const styles = `
transform: translateY(1px);
}
.diff-open-btn {
display: inline-flex;
align-items: center;
gap: 5px;
background: transparent;
border: 1px solid var(--vscode-button-secondaryBorder, var(--vscode-panel-border));
color: var(--vscode-foreground);
padding: 4px 10px;
border-radius: 3px;
font-size: 11px;
cursor: pointer;
transition: all 0.2s ease;
white-space: nowrap;
}
.diff-open-btn svg {
flex-shrink: 0;
}
.diff-open-btn:hover {
background: var(--vscode-button-secondaryHoverBackground, rgba(255, 255, 255, 0.1));
border-color: var(--vscode-focusBorder);
opacity: 1;
}
.diff-open-btn:active {
transform: translateY(1px);
}
.file-path-short,
.file-path-truncated {
font-family: var(--vscode-editor-font-family);