mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
chore: sync gemini-cli v0.1.19
This commit is contained in:
@@ -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) {
|
||||
|
||||
106
packages/vscode-ide-companion/src/extension.test.ts
Normal file
106
packages/vscode-ide-companion/src/extension.test.ts
Normal 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user