# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)

This commit is contained in:
tanzhenxin
2025-09-01 14:48:55 +08:00
committed by GitHub
parent 1610c1586e
commit 2572faf726
292 changed files with 19401 additions and 5941 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
# Qwen Code Companion
The Qwen Code Companion extension seamlessly integrates [Qwen Code](https://github.com/QwenLM/qwen-code) into your IDE.
The Qwen Code Companion extension seamlessly integrates [Qwen Code](https://github.com/QwenLM/qwen-code). This extension is compatible with both VS Code and VS Code forks.
# Features

View File

@@ -0,0 +1,30 @@
# Local Development
## Running the Extension
To run the extension locally for development:
1. From the root of the repository, install dependencies:
```bash
npm install
```
2. Open this directory (`packages/vscode-ide-companion`) in VS Code.
3. Compile the extension:
```bash
npm run compile
```
4. Press `F5` (fn+f5 on mac) to open a new Extension Development Host window with the extension running.
To watch for changes and have the extension rebuild automatically, run:
```bash
npm run watch
```
## Running Tests
To run the automated tests, run:
```bash
npm run test
```

View File

@@ -47,6 +47,7 @@ async function main() {
/* add to the end of plugins array */
esbuildProblemMatcherPlugin,
],
loader: { '.node': 'file' },
});
if (watch) {
await ctx.watch();

View File

@@ -43,16 +43,34 @@ async function getDependencyLicense(depName, depVersion) {
repositoryUrl = depPackageJson.repository?.url || repositoryUrl;
const licenseFile = depPackageJson.licenseFile
? path.join(path.dirname(depPackageJsonPath), depPackageJson.licenseFile)
: path.join(path.dirname(depPackageJsonPath), 'LICENSE');
const packageDir = path.dirname(depPackageJsonPath);
const licenseFileCandidates = [
depPackageJson.licenseFile,
'LICENSE',
'LICENSE.md',
'LICENSE.txt',
'LICENSE-MIT.txt',
].filter(Boolean);
try {
licenseContent = await fs.readFile(licenseFile, 'utf-8');
} catch (e) {
console.warn(
`Warning: Failed to read license file for ${depName}: ${e.message}`,
);
let licenseFile;
for (const candidate of licenseFileCandidates) {
const potentialFile = path.join(packageDir, candidate);
if (await fs.stat(potentialFile).catch(() => false)) {
licenseFile = potentialFile;
break;
}
}
if (licenseFile) {
try {
licenseContent = await fs.readFile(licenseFile, 'utf-8');
} catch (e) {
console.warn(
`Warning: Failed to read license file for ${depName}: ${e.message}`,
);
}
} else {
console.warn(`Warning: Could not find license file for ${depName}`);
}
} catch (e) {
console.warn(
@@ -68,14 +86,49 @@ async function getDependencyLicense(depName, depVersion) {
};
}
function collectDependencies(packageName, packageLock, dependenciesMap) {
if (dependenciesMap.has(packageName)) {
return;
}
const packageInfo = packageLock.packages[`node_modules/${packageName}`];
if (!packageInfo) {
console.warn(
`Warning: Could not find package info for ${packageName} in package-lock.json.`,
);
return;
}
dependenciesMap.set(packageName, packageInfo.version);
if (packageInfo.dependencies) {
for (const depName of Object.keys(packageInfo.dependencies)) {
collectDependencies(depName, packageLock, dependenciesMap);
}
}
}
async function main() {
try {
const packageJsonPath = path.join(packagePath, 'package.json');
const packageJsonContent = await fs.readFile(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(packageJsonContent);
const dependencies = packageJson.dependencies || {};
const dependencyEntries = Object.entries(dependencies);
const packageLockJsonPath = path.join(projectRoot, 'package-lock.json');
const packageLockJsonContent = await fs.readFile(
packageLockJsonPath,
'utf-8',
);
const packageLockJson = JSON.parse(packageLockJsonContent);
const allDependencies = new Map();
const directDependencies = Object.keys(packageJson.dependencies);
for (const depName of directDependencies) {
collectDependencies(depName, packageLockJson, allDependencies);
}
const dependencyEntries = Array.from(allDependencies.entries());
const licensePromises = dependencyEntries.map(([depName, depVersion]) =>
getDependencyLicense(depName, depVersion),

View File

@@ -0,0 +1,211 @@
/**
* @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 * as path from 'path';
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(),
file: (path: string) => ({ fsPath: path }),
},
ExtensionMode: {
Development: 1,
Production: 2,
},
EventEmitter: vi.fn(() => ({
event: vi.fn(),
fire: vi.fn(),
dispose: vi.fn(),
})),
}));
describe('activate with multiple folders', () => {
let context: vscode.ExtensionContext;
let onDidChangeWorkspaceFoldersCallback: (
e: vscode.WorkspaceFoldersChangeEvent,
) => void;
beforeEach(() => {
context = {
subscriptions: [],
environmentVariableCollection: {
replace: vi.fn(),
},
globalState: {
get: vi.fn().mockReturnValue(true),
update: vi.fn(),
},
extensionUri: {
fsPath: '/path/to/extension',
},
} as unknown as vscode.ExtensionContext;
vi.mocked(vscode.workspace.onDidChangeWorkspaceFolders).mockImplementation(
(callback) => {
onDidChangeWorkspaceFoldersCallback = callback;
return { dispose: vi.fn() };
},
);
});
afterEach(() => {
vi.restoreAllMocks();
});
it('should set a single folder path', async () => {
const workspaceFoldersSpy = vi.spyOn(
vscode.workspace,
'workspaceFolders',
'get',
);
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: '/foo/bar' } },
] as vscode.WorkspaceFolder[]);
await activate(context);
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
'/foo/bar',
);
});
it('should set multiple folder paths, separated by OS-specific path delimiter', async () => {
const workspaceFoldersSpy = vi.spyOn(
vscode.workspace,
'workspaceFolders',
'get',
);
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: '/foo/bar' } },
{ uri: { fsPath: '/baz/qux' } },
] as vscode.WorkspaceFolder[]);
await activate(context);
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
['/foo/bar', '/baz/qux'].join(path.delimiter),
);
});
it('should set an empty string if no folders are open', async () => {
const workspaceFoldersSpy = vi.spyOn(
vscode.workspace,
'workspaceFolders',
'get',
);
workspaceFoldersSpy.mockReturnValue([]);
await activate(context);
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
'',
);
});
it('should update the path when workspace folders change', async () => {
const workspaceFoldersSpy = vi.spyOn(
vscode.workspace,
'workspaceFolders',
'get',
);
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: '/foo/bar' } },
] as vscode.WorkspaceFolder[]);
await activate(context);
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
'/foo/bar',
);
// Simulate adding a folder
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: '/foo/bar' } },
{ uri: { fsPath: '/baz/qux' } },
] as vscode.WorkspaceFolder[]);
onDidChangeWorkspaceFoldersCallback({
added: [{ uri: { fsPath: '/baz/qux' } } as vscode.WorkspaceFolder],
removed: [],
});
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
['/foo/bar', '/baz/qux'].join(path.delimiter),
);
// Simulate removing a folder
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: '/baz/qux' } },
] as vscode.WorkspaceFolder[]);
onDidChangeWorkspaceFoldersCallback({
added: [],
removed: [{ uri: { fsPath: '/foo/bar' } } as vscode.WorkspaceFolder],
});
expect(context.environmentVariableCollection.replace).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
'/baz/qux',
);
});
it.skipIf(process.platform !== 'win32')(
'should handle windows paths',
async () => {
const workspaceFoldersSpy = vi.spyOn(
vscode.workspace,
'workspaceFolders',
'get',
);
workspaceFoldersSpy.mockReturnValue([
{ uri: { fsPath: 'c:/foo/bar' } },
{ uri: { fsPath: 'd:/baz/qux' } },
] as vscode.WorkspaceFolder[]);
await activate(context);
expect(
context.environmentVariableCollection.replace,
).toHaveBeenCalledWith(
'QWEN_CODE_IDE_WORKSPACE_PATH',
'c:/foo/bar;d:/baz/qux',
);
},
);
});

View File

@@ -25,6 +25,7 @@ vi.mock('vscode', () => ({
close: vi.fn(),
},
showTextDocument: vi.fn(),
showWorkspaceFolderPick: vi.fn(),
},
workspace: {
workspaceFolders: [],
@@ -80,8 +81,7 @@ describe('activate', () => {
vi.mocked(context.globalState.get).mockReturnValue(undefined);
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalledWith(
'Qwen Code Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Run Qwen Code',
'Qwen Code Companion extension successfully installed.',
);
});
@@ -99,8 +99,10 @@ describe('activate', () => {
await activate(context);
expect(showInformationMessageMock).toHaveBeenCalled();
await new Promise(process.nextTick); // Wait for the promise to resolve
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
'qwen-code.runQwenCode',
);
const commandCallback = vi
.mocked(vscode.commands.registerCommand)
.mock.calls.find((call) => call[0] === 'qwen-code.runQwenCode')?.[1];
expect(commandCallback).toBeDefined();
});
});

View File

@@ -5,6 +5,7 @@
*/
import * as vscode from 'vscode';
import * as path from 'path';
import { IDEServer } from './ide-server.js';
import { DiffContentProvider, DiffManager } from './diff-manager.js';
import { createLogger } from './utils/logger.js';
@@ -20,11 +21,13 @@ let log: (message: string) => void = () => {};
function updateWorkspacePath(context: vscode.ExtensionContext) {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (workspaceFolders && workspaceFolders.length === 1) {
const workspaceFolder = workspaceFolders[0];
if (workspaceFolders && workspaceFolders.length > 0) {
const workspacePaths = workspaceFolders
.map((folder) => folder.uri.fsPath)
.join(path.delimiter);
context.environmentVariableCollection.replace(
IDE_WORKSPACE_PATH_ENV_VAR,
workspaceFolder.uri.fsPath,
workspacePaths,
);
} else {
context.environmentVariableCollection.replace(
@@ -77,21 +80,9 @@ export async function activate(context: vscode.ExtensionContext) {
}
if (!context.globalState.get(INFO_MESSAGE_SHOWN_KEY)) {
void vscode.window
.showInformationMessage(
'Qwen Code Companion extension successfully installed. Please restart your terminal to enable full IDE integration.',
'Run Qwen Code',
)
.then(
(selection) => {
if (selection === 'Run Qwen Code') {
void vscode.commands.executeCommand('qwen-code.runQwenCode');
}
},
(err) => {
log(`Failed to show information message: ${String(err)}`);
},
);
void vscode.window.showInformationMessage(
'Qwen Code Companion extension successfully installed.',
);
context.globalState.update(INFO_MESSAGE_SHOWN_KEY, true);
}
@@ -99,11 +90,33 @@ export async function activate(context: vscode.ExtensionContext) {
vscode.workspace.onDidChangeWorkspaceFolders(() => {
updateWorkspacePath(context);
}),
vscode.commands.registerCommand('qwen-code.runQwenCode', () => {
const qwenCmd = 'qwen';
const terminal = vscode.window.createTerminal(`Qwen Code`);
terminal.show();
terminal.sendText(qwenCmd);
vscode.commands.registerCommand('qwen-code.runQwenCode', async () => {
const workspaceFolders = vscode.workspace.workspaceFolders;
if (!workspaceFolders || workspaceFolders.length === 0) {
vscode.window.showInformationMessage(
'No folder open. Please open a folder to run Qwen Code.',
);
return;
}
let selectedFolder: vscode.WorkspaceFolder | undefined;
if (workspaceFolders.length === 1) {
selectedFolder = workspaceFolders[0];
} else {
selectedFolder = await vscode.window.showWorkspaceFolderPick({
placeHolder: 'Select a folder to run Qwen Code in',
});
}
if (selectedFolder) {
const qwenCmd = 'qwen';
const terminal = vscode.window.createTerminal({
name: `Qwen Code (${selectedFolder.name})`,
cwd: selectedFolder.uri.fsPath,
});
terminal.show();
terminal.sendText(qwenCmd);
}
}),
vscode.commands.registerCommand('qwen-code.showNotices', async () => {
const noticePath = vscode.Uri.joinPath(

View File

@@ -12,6 +12,9 @@ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/
import express, { type Request, type Response } from 'express';
import { randomUUID } from 'node:crypto';
import { type Server as HTTPServer } from 'node:http';
import * as path from 'node:path';
import * as fs from 'node:fs/promises';
import * as os from 'node:os';
import { z } from 'zod';
import { DiffManager } from './diff-manager.js';
import { OpenFilesManager } from './open-files-manager.js';
@@ -46,11 +49,16 @@ export class IDEServer {
private server: HTTPServer | undefined;
private context: vscode.ExtensionContext | undefined;
private log: (message: string) => void;
private portFile: string;
diffManager: DiffManager;
constructor(log: (message: string) => void, diffManager: DiffManager) {
this.log = log;
this.diffManager = diffManager;
this.portFile = path.join(
os.tmpdir(),
`gemini-ide-server-${process.ppid}.json`,
);
}
async start(context: vscode.ExtensionContext) {
@@ -197,6 +205,10 @@ export class IDEServer {
port.toString(),
);
this.log(`IDE server listening on port ${port}`);
fs.writeFile(this.portFile, JSON.stringify({ port })).catch((err) => {
this.log(`Failed to write port to file: ${err}`);
});
this.log(this.portFile);
}
});
}
@@ -219,6 +231,11 @@ export class IDEServer {
if (this.context) {
this.context.environmentVariableCollection.clear();
}
try {
await fs.unlink(this.portFile);
} catch (_err) {
// Ignore errors if the file doesn't exist.
}
}
}