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