Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/chatSessions/copilotcli/vscode-node/tools/openDiff.ts
13406 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
7
import * as fs from 'fs/promises';
8
import * as vscode from 'vscode';
9
import { z } from 'zod';
10
import { ILogger } from '../../../../../platform/log/common/logService';
11
import { generateUuid } from '../../../../../util/vs/base/common/uuid';
12
import { DiffStateManager } from '../diffState';
13
import { ReadonlyContentProvider, createReadonlyUri } from '../readonlyContentProvider';
14
import { makeTextResult } from './utils';
15
16
function makeErrorResult(message: string): { content: [{ type: 'text'; text: string }]; isError: true } {
17
return {
18
content: [{ type: 'text', text: message }],
19
isError: true,
20
};
21
}
22
23
export function registerOpenDiffTool(server: McpServer, logger: ILogger, diffState: DiffStateManager, contentProvider: ReadonlyContentProvider, sessionId: string): void {
24
const schema = {
25
original_file_path: z.string().describe('Path to the original file'),
26
new_file_contents: z.string().describe('The new file contents to compare against'),
27
tab_name: z.string().describe('Name for the diff tab'),
28
};
29
server.registerTool(
30
'open_diff',
31
{
32
description: 'Opens a diff view comparing original file content with new content. Blocks until user accepts, rejects, or closes the diff.',
33
inputSchema: schema,
34
},
35
// @ts-ignore - TS2589: zod type instantiation too deep for server.tool() generics
36
async (args: { original_file_path: string; new_file_contents: string; tab_name: string }) => {
37
const { original_file_path, new_file_contents, tab_name } = args;
38
logger.info(`[DIFF] ===== OPEN_DIFF START ===== file=${original_file_path}, tab=${tab_name}`);
39
try {
40
// Read original file content for the readonly left side
41
// If the file doesn't exist, use empty string (new file scenario)
42
let originalContent: string;
43
try {
44
originalContent = await fs.readFile(original_file_path, 'utf-8');
45
} catch (err: unknown) {
46
const e = err as NodeJS.ErrnoException;
47
if (e.code === 'ENOENT') {
48
originalContent = '';
49
} else {
50
throw err;
51
}
52
}
53
54
// Create unique suffix for this diff
55
const uniqueSuffix = `${Date.now()}-${generateUuid()}`;
56
logger.info(`[DIFF] uniqueSuffix=${uniqueSuffix}`);
57
58
// Create readonly URIs for both sides
59
const originalUri = createReadonlyUri(original_file_path, `original-${uniqueSuffix}`);
60
const newUri = createReadonlyUri(original_file_path, `modified-${uniqueSuffix}`);
61
logger.info(`[DIFF] modifiedUri=${newUri.toString()}`);
62
63
// Set the content for both readonly documents
64
contentProvider.setContent(originalUri, originalContent);
65
contentProvider.setContent(newUri, new_file_contents);
66
67
const title = tab_name;
68
// Open diff with readonly virtual documents on both sides
69
logger.info('[DIFF] Calling vscode.diff command');
70
await vscode.commands.executeCommand('vscode.diff', originalUri, newUri, title, {
71
preview: false,
72
preserveFocus: true,
73
});
74
logger.info('[DIFF] vscode.diff command completed');
75
76
// Wait for user action: Accept, Reject, or tab close
77
const result = await new Promise<{ status: 'SAVED' | 'REJECTED'; trigger: string }>(resolve => {
78
const disposables: vscode.Disposable[] = [];
79
80
let cleanedUp = false;
81
const diffId = newUri.toString();
82
logger.info(`[DIFF] diffId=${diffId}`);
83
84
const cleanup = () => {
85
logger.info(`[DIFF] cleanup() called, cleanedUp=${cleanedUp}, diffId=${diffId}`);
86
if (cleanedUp) {
87
return;
88
}
89
cleanedUp = true;
90
disposables.forEach(d => { d.dispose(); });
91
diffState.unregister(diffId);
92
contentProvider.clearContent(originalUri);
93
contentProvider.clearContent(newUri);
94
logger.info('[DIFF] cleanup() done');
95
};
96
97
const closeDiffTab = async () => {
98
logger.info(`[DIFF] closeDiffTab() looking for modifiedUri=${newUri.toString()}`);
99
for (const group of vscode.window.tabGroups.all) {
100
for (const tab of group.tabs) {
101
if (tab.input instanceof vscode.TabInputTextDiff) {
102
const tabModifiedUri = tab.input.modified.toString();
103
if (tabModifiedUri === newUri.toString()) {
104
logger.info('[DIFF] Found matching tab, closing it');
105
try {
106
await vscode.window.tabGroups.close(tab);
107
logger.info('[DIFF] Tab closed');
108
} catch (e: unknown) {
109
logger.info(`[DIFF] Tab close error: ${e instanceof Error ? e.message : String(e)}`);
110
}
111
return;
112
}
113
}
114
}
115
}
116
logger.info('[DIFF] No matching tab found');
117
};
118
119
const wrappedResolve = (result: { status: 'SAVED' | 'REJECTED'; trigger: string }) => {
120
logger.info(`[DIFF] wrappedResolve() status=${result.status}, trigger=${result.trigger}`);
121
cleanup();
122
void closeDiffTab();
123
resolve(result);
124
};
125
126
// Register this diff so editor title buttons can access it
127
logger.info('[DIFF] Registering diff');
128
diffState.register({
129
diffId,
130
sessionId,
131
tabName: tab_name,
132
originalUri,
133
modifiedUri: newUri,
134
newContents: new_file_contents,
135
cleanup,
136
resolve: wrappedResolve,
137
});
138
139
// Watch for tab close
140
disposables.push(
141
vscode.window.tabGroups.onDidChangeTabs(event => {
142
logger.info(`[DIFF] onDidChangeTabs: opened=${event.opened.length}, closed=${event.closed.length}, changed=${event.changed.length}, myDiffId=${diffId}`);
143
for (const closedTab of event.closed) {
144
if (closedTab.input instanceof vscode.TabInputTextDiff) {
145
logger.info(`[DIFF] closedTab modifiedUri=${closedTab.input.modified.toString()}`);
146
}
147
const diff = diffState.getByTab(closedTab);
148
logger.info(`[DIFF] getActiveDiffByTab returned: ${diff?.diffId ?? 'undefined'}`);
149
if (diff && diff.diffId === diffId) {
150
logger.info(`[DIFF] MATCH - Tab closed manually: ${tab_name}`);
151
cleanup();
152
// Do NOT resolve - leave Promise pending, client handles timeout
153
return;
154
}
155
}
156
})
157
);
158
logger.info('[DIFF] Setup complete, waiting for user action');
159
});
160
161
logger.info(`[DIFF] ===== OPEN_DIFF END ===== result=${result.status}`);
162
return makeTextResult({
163
success: true,
164
result: result.status,
165
trigger: result.trigger,
166
tab_name: tab_name,
167
message: result.status === 'SAVED'
168
? `User accepted changes for ${original_file_path}`
169
: `User rejected changes for ${original_file_path}`
170
});
171
} catch (err: unknown) {
172
logger.error(`[DIFF] ERROR: ${err instanceof Error ? err.message : String(err)}`);
173
return makeErrorResult(`Failed to open diff: ${err instanceof Error ? err.message : String(err)}`);
174
}
175
}
176
);
177
}
178
179