269 lines
7.3 KiB
TypeScript
269 lines
7.3 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import {
|
|
IdeDiffAcceptedNotificationSchema,
|
|
IdeDiffClosedNotificationSchema,
|
|
} from '@google/gemini-cli-core';
|
|
import { type JSONRPCNotification } from '@modelcontextprotocol/sdk/types.js';
|
|
import * as path from 'node:path';
|
|
import * as vscode from 'vscode';
|
|
import { DIFF_SCHEME } from './extension.js';
|
|
|
|
export class DiffContentProvider implements vscode.TextDocumentContentProvider {
|
|
private content = new Map<string, string>();
|
|
private onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
|
|
|
|
get onDidChange(): vscode.Event<vscode.Uri> {
|
|
return this.onDidChangeEmitter.event;
|
|
}
|
|
|
|
provideTextDocumentContent(uri: vscode.Uri): string {
|
|
return this.content.get(uri.toString()) ?? '';
|
|
}
|
|
|
|
setContent(uri: vscode.Uri, content: string): void {
|
|
this.content.set(uri.toString(), content);
|
|
this.onDidChangeEmitter.fire(uri);
|
|
}
|
|
|
|
deleteContent(uri: vscode.Uri): void {
|
|
this.content.delete(uri.toString());
|
|
}
|
|
|
|
getContent(uri: vscode.Uri): string | undefined {
|
|
return this.content.get(uri.toString());
|
|
}
|
|
}
|
|
|
|
// Information about a diff view that is currently open.
|
|
interface DiffInfo {
|
|
originalFilePath: string;
|
|
newContent: string;
|
|
rightDocUri: vscode.Uri;
|
|
}
|
|
|
|
/**
|
|
* Manages the state and lifecycle of diff views within the IDE.
|
|
*/
|
|
export class DiffManager {
|
|
private readonly onDidChangeEmitter =
|
|
new vscode.EventEmitter<JSONRPCNotification>();
|
|
readonly onDidChange = this.onDidChangeEmitter.event;
|
|
private diffDocuments = new Map<string, DiffInfo>();
|
|
private readonly subscriptions: vscode.Disposable[] = [];
|
|
|
|
constructor(
|
|
private readonly log: (message: string) => void,
|
|
private readonly diffContentProvider: DiffContentProvider,
|
|
) {
|
|
this.subscriptions.push(
|
|
vscode.window.onDidChangeActiveTextEditor((editor) => {
|
|
this.onActiveEditorChange(editor);
|
|
}),
|
|
);
|
|
this.onActiveEditorChange(vscode.window.activeTextEditor);
|
|
}
|
|
|
|
dispose() {
|
|
for (const subscription of this.subscriptions) {
|
|
subscription.dispose();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates and shows a new diff view.
|
|
*/
|
|
async showDiff(filePath: string, newContent: string) {
|
|
const fileUri = vscode.Uri.file(filePath);
|
|
|
|
const rightDocUri = vscode.Uri.from({
|
|
scheme: DIFF_SCHEME,
|
|
path: filePath,
|
|
// cache busting
|
|
query: `rand=${Math.random()}`,
|
|
});
|
|
this.diffContentProvider.setContent(rightDocUri, newContent);
|
|
|
|
this.addDiffDocument(rightDocUri, {
|
|
originalFilePath: filePath,
|
|
newContent,
|
|
rightDocUri,
|
|
});
|
|
|
|
const diffTitle = `${path.basename(filePath)} ↔ Modified`;
|
|
await vscode.commands.executeCommand(
|
|
'setContext',
|
|
'gemini.diff.isVisible',
|
|
true,
|
|
);
|
|
|
|
let leftDocUri;
|
|
try {
|
|
await vscode.workspace.fs.stat(fileUri);
|
|
leftDocUri = fileUri;
|
|
} catch {
|
|
// We need to provide an empty document to diff against.
|
|
// Using the 'untitled' scheme is one way to do this.
|
|
leftDocUri = vscode.Uri.from({
|
|
scheme: 'untitled',
|
|
path: filePath,
|
|
});
|
|
}
|
|
|
|
await vscode.commands.executeCommand(
|
|
'vscode.diff',
|
|
leftDocUri,
|
|
rightDocUri,
|
|
diffTitle,
|
|
{
|
|
preview: false,
|
|
preserveFocus: true,
|
|
},
|
|
);
|
|
await vscode.commands.executeCommand(
|
|
'workbench.action.files.setActiveEditorWriteableInSession',
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Closes an open diff view for a specific file.
|
|
*/
|
|
async closeDiff(filePath: string) {
|
|
let uriToClose: vscode.Uri | undefined;
|
|
for (const [uriString, diffInfo] of this.diffDocuments.entries()) {
|
|
if (diffInfo.originalFilePath === filePath) {
|
|
uriToClose = vscode.Uri.parse(uriString);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (uriToClose) {
|
|
const rightDoc = await vscode.workspace.openTextDocument(uriToClose);
|
|
const modifiedContent = rightDoc.getText();
|
|
await this.closeDiffEditor(uriToClose);
|
|
this.onDidChangeEmitter.fire(
|
|
IdeDiffClosedNotificationSchema.parse({
|
|
jsonrpc: '2.0',
|
|
method: 'ide/diffClosed',
|
|
params: {
|
|
filePath,
|
|
content: modifiedContent,
|
|
},
|
|
}),
|
|
);
|
|
return modifiedContent;
|
|
}
|
|
return;
|
|
}
|
|
|
|
/**
|
|
* User accepts the changes in a diff view. Does not apply changes.
|
|
*/
|
|
async acceptDiff(rightDocUri: vscode.Uri) {
|
|
const diffInfo = this.diffDocuments.get(rightDocUri.toString());
|
|
if (!diffInfo) {
|
|
this.log(`No diff info found for ${rightDocUri.toString()}`);
|
|
return;
|
|
}
|
|
|
|
const rightDoc = await vscode.workspace.openTextDocument(rightDocUri);
|
|
const modifiedContent = rightDoc.getText();
|
|
await this.closeDiffEditor(rightDocUri);
|
|
|
|
this.onDidChangeEmitter.fire(
|
|
IdeDiffAcceptedNotificationSchema.parse({
|
|
jsonrpc: '2.0',
|
|
method: 'ide/diffAccepted',
|
|
params: {
|
|
filePath: diffInfo.originalFilePath,
|
|
content: modifiedContent,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Called when a user cancels a diff view.
|
|
*/
|
|
async cancelDiff(rightDocUri: vscode.Uri) {
|
|
const diffInfo = this.diffDocuments.get(rightDocUri.toString());
|
|
if (!diffInfo) {
|
|
this.log(`No diff info found for ${rightDocUri.toString()}`);
|
|
// Even if we don't have diff info, we should still close the editor.
|
|
await this.closeDiffEditor(rightDocUri);
|
|
return;
|
|
}
|
|
|
|
const rightDoc = await vscode.workspace.openTextDocument(rightDocUri);
|
|
const modifiedContent = rightDoc.getText();
|
|
await this.closeDiffEditor(rightDocUri);
|
|
|
|
this.onDidChangeEmitter.fire(
|
|
IdeDiffClosedNotificationSchema.parse({
|
|
jsonrpc: '2.0',
|
|
method: 'ide/diffClosed',
|
|
params: {
|
|
filePath: diffInfo.originalFilePath,
|
|
content: modifiedContent,
|
|
},
|
|
}),
|
|
);
|
|
}
|
|
|
|
private async onActiveEditorChange(editor: vscode.TextEditor | undefined) {
|
|
let isVisible = false;
|
|
if (editor) {
|
|
isVisible = this.diffDocuments.has(editor.document.uri.toString());
|
|
if (!isVisible) {
|
|
for (const document of this.diffDocuments.values()) {
|
|
if (document.originalFilePath === editor.document.uri.fsPath) {
|
|
isVisible = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
await vscode.commands.executeCommand(
|
|
'setContext',
|
|
'gemini.diff.isVisible',
|
|
isVisible,
|
|
);
|
|
}
|
|
|
|
private addDiffDocument(uri: vscode.Uri, diffInfo: DiffInfo) {
|
|
this.diffDocuments.set(uri.toString(), diffInfo);
|
|
}
|
|
|
|
private async closeDiffEditor(rightDocUri: vscode.Uri) {
|
|
const diffInfo = this.diffDocuments.get(rightDocUri.toString());
|
|
await vscode.commands.executeCommand(
|
|
'setContext',
|
|
'gemini.diff.isVisible',
|
|
false,
|
|
);
|
|
|
|
if (diffInfo) {
|
|
this.diffDocuments.delete(rightDocUri.toString());
|
|
this.diffContentProvider.deleteContent(rightDocUri);
|
|
}
|
|
|
|
// Find and close the tab corresponding to the diff view
|
|
for (const tabGroup of vscode.window.tabGroups.all) {
|
|
for (const tab of tabGroup.tabs) {
|
|
const input = tab.input as {
|
|
modified?: vscode.Uri;
|
|
original?: vscode.Uri;
|
|
};
|
|
if (input && input.modified?.toString() === rightDocUri.toString()) {
|
|
await vscode.window.tabGroups.close(tab);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|