mirror of
https://github.com/andrepimenta/claude-code-chat.git
synced 2025-12-08 17:09:44 +00:00
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:
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user