mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
# 🚀 Sync Gemini CLI v0.2.1 - Major Feature Update (#483)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
||||
30
packages/vscode-ide-companion/development.md
Normal file
30
packages/vscode-ide-companion/development.md
Normal 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
|
||||
```
|
||||
@@ -47,6 +47,7 @@ async function main() {
|
||||
/* add to the end of plugins array */
|
||||
esbuildProblemMatcherPlugin,
|
||||
],
|
||||
loader: { '.node': 'file' },
|
||||
});
|
||||
if (watch) {
|
||||
await ctx.watch();
|
||||
|
||||
@@ -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),
|
||||
|
||||
211
packages/vscode-ide-companion/src/extension-multi-folder.test.ts
Normal file
211
packages/vscode-ide-companion/src/extension-multi-folder.test.ts
Normal 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',
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user