chore: sync gemini-cli v0.1.19

This commit is contained in:
tanzhenxin
2025-08-18 19:55:46 +08:00
244 changed files with 19407 additions and 5030 deletions

View File

@@ -4,10 +4,14 @@
* SPDX-License-Identifier: Apache-2.0
*/
import * as vscode from 'vscode';
import * as path from 'node:path';
import { DIFF_SCHEME } from './extension.js';
import {
IdeDiffAcceptedNotificationSchema,
IdeDiffClosedNotificationSchema,
} from '@qwen-code/qwen-code-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>();
@@ -50,11 +54,25 @@ export class DiffManager {
new vscode.EventEmitter<JSONRPCNotification>();
readonly onDidChange = this.onDidChangeEmitter.event;
private diffDocuments = new Map<string, DiffInfo>();
private readonly subscriptions: vscode.Disposable[] = [];
constructor(
private readonly logger: vscode.OutputChannel,
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.
@@ -126,18 +144,19 @@ export class DiffManager {
const rightDoc = await vscode.workspace.openTextDocument(uriToClose);
const modifiedContent = rightDoc.getText();
await this.closeDiffEditor(uriToClose);
this.onDidChangeEmitter.fire({
jsonrpc: '2.0',
method: 'ide/diffClosed',
params: {
filePath,
content: modifiedContent,
},
});
vscode.window.showInformationMessage(`Diff for ${filePath} closed.`);
} else {
vscode.window.showWarningMessage(`No open diff found for ${filePath}.`);
this.onDidChangeEmitter.fire(
IdeDiffClosedNotificationSchema.parse({
jsonrpc: '2.0',
method: 'ide/diffClosed',
params: {
filePath,
content: modifiedContent,
},
}),
);
return modifiedContent;
}
return;
}
/**
@@ -146,9 +165,7 @@ export class DiffManager {
async acceptDiff(rightDocUri: vscode.Uri) {
const diffInfo = this.diffDocuments.get(rightDocUri.toString());
if (!diffInfo) {
this.logger.appendLine(
`No diff info found for ${rightDocUri.toString()}`,
);
this.log(`No diff info found for ${rightDocUri.toString()}`);
return;
}
@@ -156,14 +173,16 @@ export class DiffManager {
const modifiedContent = rightDoc.getText();
await this.closeDiffEditor(rightDocUri);
this.onDidChangeEmitter.fire({
jsonrpc: '2.0',
method: 'ide/diffAccepted',
params: {
filePath: diffInfo.originalFilePath,
content: modifiedContent,
},
});
this.onDidChangeEmitter.fire(
IdeDiffAcceptedNotificationSchema.parse({
jsonrpc: '2.0',
method: 'ide/diffAccepted',
params: {
filePath: diffInfo.originalFilePath,
content: modifiedContent,
},
}),
);
}
/**
@@ -172,9 +191,7 @@ export class DiffManager {
async cancelDiff(rightDocUri: vscode.Uri) {
const diffInfo = this.diffDocuments.get(rightDocUri.toString());
if (!diffInfo) {
this.logger.appendLine(
`No diff info found for ${rightDocUri.toString()}`,
);
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;
@@ -184,14 +201,36 @@ export class DiffManager {
const modifiedContent = rightDoc.getText();
await this.closeDiffEditor(rightDocUri);
this.onDidChangeEmitter.fire({
jsonrpc: '2.0',
method: 'ide/diffClosed',
params: {
filePath: diffInfo.originalFilePath,
content: modifiedContent,
},
});
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) {

View File

@@ -0,0 +1,106 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { activate } from './extension.js';
vi.mock('vscode', () => ({
window: {
createOutputChannel: vi.fn(() => ({
appendLine: vi.fn(),
})),
showInformationMessage: vi.fn(),
createTerminal: vi.fn(() => ({
show: vi.fn(),
sendText: vi.fn(),
})),
onDidChangeActiveTextEditor: vi.fn(),
activeTextEditor: undefined,
tabGroups: {
all: [],
close: vi.fn(),
},
showTextDocument: vi.fn(),
},
workspace: {
workspaceFolders: [],
onDidCloseTextDocument: vi.fn(),
registerTextDocumentContentProvider: vi.fn(),
onDidChangeWorkspaceFolders: vi.fn(),
},
commands: {
registerCommand: vi.fn(),
executeCommand: vi.fn(),
},
Uri: {
joinPath: vi.fn(),
},
ExtensionMode: {
Development: 1,
Production: 2,
},
EventEmitter: vi.fn(() => ({
event: vi.fn(),
fire: vi.fn(),
dispose: vi.fn(),
})),
}));
describe('activate', () => {
let context: vscode.ExtensionContext;
beforeEach(() => {
context = {
subscriptions: [],
environmentVariableCollection: {
replace: vi.fn(),
},
globalState: {
get: vi.fn(),
update: vi.fn(),
},
extensionUri: {
fsPath: '/path/to/extension',
},
} as unknown as vscode.ExtensionContext;
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should show the info message on first activation', async () => {
const showInformationMessageMock = vi
.mocked(vscode.window.showInformationMessage)
.mockResolvedValue(undefined as never);
vi.mocked(context.globalState.get).mockReturnValue(undefined);
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalledWith(
'Gemini CLI Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Re-launch Gemini CLI',
);
});
it('should not show the info message on subsequent activations', async () => {
vi.mocked(context.globalState.get).mockReturnValue(true);
await activate(context);
expect(vscode.window.showInformationMessage).not.toHaveBeenCalled();
});
it('should launch the Gemini CLI when the user clicks the button', async () => {
const showInformationMessageMock = vi
.mocked(vscode.window.showInformationMessage)
.mockResolvedValue('Re-launch Gemini CLI' as never);
vi.mocked(context.globalState.get).mockReturnValue(undefined);
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalled();
await new Promise(process.nextTick); // Wait for the promise to resolve
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
'gemini-cli.runGeminiCLI',
);
});
});

View File

@@ -9,6 +9,7 @@ import { IDEServer } from './ide-server.js';
import { DiffContentProvider, DiffManager } from './diff-manager.js';
import { createLogger } from './utils/logger.js';
const INFO_MESSAGE_SHOWN_KEY = 'geminiCliInfoMessageShown';
const IDE_WORKSPACE_PATH_ENV_VAR = 'GEMINI_CLI_IDE_WORKSPACE_PATH';
export const DIFF_SCHEME = 'gemini-diff';
@@ -41,7 +42,7 @@ export async function activate(context: vscode.ExtensionContext) {
updateWorkspacePath(context);
const diffContentProvider = new DiffContentProvider();
const diffManager = new DiffManager(logger, diffContentProvider);
const diffManager = new DiffManager(log, diffContentProvider);
context.subscriptions.push(
vscode.workspace.onDidCloseTextDocument((doc) => {
@@ -81,6 +82,25 @@ export async function activate(context: vscode.ExtensionContext) {
log(`Failed to start IDE server: ${message}`);
}
if (!context.globalState.get(INFO_MESSAGE_SHOWN_KEY)) {
void vscode.window
.showInformationMessage(
'Gemini CLI Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Re-launch Gemini CLI',
)
.then(
(selection) => {
if (selection === 'Re-launch Gemini CLI') {
void vscode.commands.executeCommand('gemini-cli.runGeminiCLI');
}
},
(err) => {
log(`Failed to show information message: ${String(err)}`);
},
);
context.globalState.update(INFO_MESSAGE_SHOWN_KEY, true);
}
context.subscriptions.push(
vscode.workspace.onDidChangeWorkspaceFolders(() => {
updateWorkspacePath(context);

View File

@@ -5,15 +5,13 @@
*/
import * as vscode from 'vscode';
import { IdeContextNotificationSchema } from '@qwen-code/qwen-code-core';
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express, { Request, Response } from 'express';
import express, { type Request, type Response } from 'express';
import { randomUUID } from 'node:crypto';
import {
isInitializeRequest,
type JSONRPCNotification,
} from '@modelcontextprotocol/sdk/types.js';
import { Server as HTTPServer } from 'node:http';
import { type Server as HTTPServer } from 'node:http';
import { z } from 'zod';
import { DiffManager } from './diff-manager.js';
import { OpenFilesManager } from './open-files-manager.js';
@@ -28,11 +26,12 @@ function sendIdeContextUpdateNotification(
) {
const ideContext = openFilesManager.state;
const notification: JSONRPCNotification = {
const notification = IdeContextNotificationSchema.parse({
jsonrpc: '2.0',
method: 'ide/contextUpdate',
params: ideContext,
};
});
log(
`Sending IDE context update notification: ${JSON.stringify(
notification,
@@ -76,7 +75,7 @@ export class IDEServer {
});
context.subscriptions.push(onDidChangeSubscription);
const onDidChangeDiffSubscription = this.diffManager.onDidChange(
(notification: JSONRPCNotification) => {
(notification) => {
for (const transport of Object.values(transports)) {
transport.send(notification);
}
@@ -269,12 +268,13 @@ const createMcpServer = (diffManager: DiffManager) => {
}).shape,
},
async ({ filePath }: { filePath: string }) => {
await diffManager.closeDiff(filePath);
const content = await diffManager.closeDiff(filePath);
const response = { content: content ?? undefined };
return {
content: [
{
type: 'text',
text: `Closed diff for ${filePath}`,
text: JSON.stringify(response),
},
],
};