Merge branch 'main' into pr-1539

This commit is contained in:
tanzhenxin 2026-01-29 19:18:23 +08:00
commit 1c5b74ebd9
210 changed files with 22365 additions and 2747 deletions

View file

@ -168,7 +168,7 @@ describe('validateAuthMethod', () => {
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
});
it('should use config.modelsConfig.getModel() when Config is provided', () => {
it('should use config.getModelsConfig().getModel() when Config is provided', () => {
// Settings has a different model
vi.mocked(settings.loadSettings).mockReturnValue({
merged: {
@ -184,18 +184,18 @@ describe('validateAuthMethod', () => {
// Mock Config object that returns a different model (e.g., from CLI args)
const mockConfig = {
modelsConfig: {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('cli-model'),
},
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
// Set the env key for the CLI model, not the settings model
process.env['CLI_API_KEY'] = 'cli-key';
// Should use 'cli-model' from config.modelsConfig.getModel(), not 'settings-model'
// Should use 'cli-model' from config.getModelsConfig().getModel(), not 'settings-model'
const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);
expect(result).toBeNull();
expect(mockConfig.modelsConfig.getModel).toHaveBeenCalled();
expect(mockConfig.getModelsConfig).toHaveBeenCalled();
});
it('should fail validation when Config provides different model without matching env key', () => {
@ -217,9 +217,9 @@ describe('validateAuthMethod', () => {
} as unknown as ReturnType<typeof settings.loadSettings>);
const mockConfig = {
modelsConfig: {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('cli-model'),
},
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
// Don't set CLI_API_KEY - validation should fail

View file

@ -60,9 +60,9 @@ function hasApiKeyForAuth(
| ModelProvidersConfig
| undefined;
// Use config.modelsConfig.getModel() if available for accurate model ID resolution
// Use config.getModelsConfig().getModel() if available for accurate model ID resolution
// that accounts for CLI args, env vars, and settings. Fall back to settings.model.name.
const modelId = config?.modelsConfig.getModel() ?? settings.model?.name;
const modelId = config?.getModelsConfig().getModel() ?? settings.model?.name;
// Try to find model-specific envKey from modelProviders
const modelConfig = findModelConfig(modelProviders, authType, modelId);
@ -184,9 +184,9 @@ export function validateAuthMethod(
const modelProviders = settings.merged.modelProviders as
| ModelProvidersConfig
| undefined;
// Use config.modelsConfig.getModel() if available for accurate model ID
// Use config.getModelsConfig().getModel() if available for accurate model ID
const modelId =
config?.modelsConfig.getModel() ?? settings.merged.model?.name;
config?.getModelsConfig().getModel() ?? settings.merged.model?.name;
const modelConfig = findModelConfig(modelProviders, authMethod, modelId);
if (modelConfig && !modelConfig.baseUrl) {

View file

@ -13,18 +13,48 @@ import {
WriteFileTool,
DEFAULT_QWEN_MODEL,
OutputFormat,
NativeLspService,
} from '@qwen-code/qwen-code-core';
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
import type { Settings } from './settings.js';
import * as ServerConfig from '@qwen-code/qwen-code-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
const createNativeLspServiceInstance = () => ({
discoverAndPrepare: vi.fn(),
start: vi.fn(),
definitions: vi.fn().mockResolvedValue([]),
references: vi.fn().mockResolvedValue([]),
workspaceSymbols: vi.fn().mockResolvedValue([]),
hover: vi.fn().mockResolvedValue(null),
documentSymbols: vi.fn().mockResolvedValue([]),
implementations: vi.fn().mockResolvedValue([]),
prepareCallHierarchy: vi.fn().mockResolvedValue([]),
incomingCalls: vi.fn().mockResolvedValue([]),
outgoingCalls: vi.fn().mockResolvedValue([]),
diagnostics: vi.fn().mockResolvedValue([]),
workspaceDiagnostics: vi.fn().mockResolvedValue([]),
codeActions: vi.fn().mockResolvedValue([]),
applyWorkspaceEdit: vi.fn().mockResolvedValue(false),
});
vi.mock('./trustedFolders.js', () => ({
isWorkspaceTrusted: vi
.fn()
.mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted
}));
const nativeLspServiceMock = vi.mocked(NativeLspService);
const getLastLspInstance = () => {
const results = nativeLspServiceMock.mock.results;
if (results.length === 0) {
return undefined;
}
return results[results.length - 1]?.value as ReturnType<
typeof createNativeLspServiceInstance
>;
};
vi.mock('fs', async (importOriginal) => {
const actualFs = await importOriginal<typeof import('fs')>();
const pathMod = await import('node:path');
@ -79,6 +109,9 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
const actualServer = await importOriginal<typeof ServerConfig>();
return {
...actualServer,
NativeLspService: vi
.fn()
.mockImplementation(() => createNativeLspServiceInstance()),
IdeClient: {
getInstance: vi.fn().mockResolvedValue({
getConnectionStatus: vi.fn(),
@ -514,6 +547,10 @@ describe('loadCliConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
nativeLspServiceMock.mockReset();
nativeLspServiceMock.mockImplementation(
() => createNativeLspServiceInstance() as unknown as NativeLspService,
);
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});
@ -543,6 +580,22 @@ describe('loadCliConfig', () => {
expect(config.getIncludePartialMessages()).toBe(true);
});
it('should initialize native LSP service when enabled', async () => {
process.argv = ['node', 'script.js', '--experimental-lsp'];
const argv = await parseArguments({} as Settings);
const settings: Settings = {};
const config = await loadCliConfig(settings, argv);
// LSP is enabled via --experimental-lsp flag
expect(config.isLspEnabled()).toBe(true);
expect(nativeLspServiceMock).toHaveBeenCalledTimes(1);
const lspInstance = getLastLspInstance();
expect(lspInstance).toBeDefined();
expect(lspInstance?.discoverAndPrepare).toHaveBeenCalledTimes(1);
expect(lspInstance?.start).toHaveBeenCalledTimes(1);
});
describe('Proxy configuration', () => {
const originalProxyEnv: { [key: string]: string | undefined } = {};
const proxyEnvVars = [

View file

@ -20,11 +20,15 @@ import {
OutputFormat,
isToolEnabled,
SessionService,
ideContextStore,
type ResumedSessionData,
type LspClient,
type ToolName,
EditTool,
ShellTool,
WriteFileTool,
NativeLspClient,
NativeLspService,
} from '@qwen-code/qwen-code-core';
import { extensionsCommand } from '../commands/extensions.js';
import type { Settings } from './settings.js';
@ -113,6 +117,7 @@ export interface CliArgs {
acp: boolean | undefined;
experimentalAcp: boolean | undefined;
experimentalSkills: boolean | undefined;
experimentalLsp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
openaiLogging: boolean | undefined;
@ -331,6 +336,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
return settings.experimental?.skills ?? legacySkills ?? false;
})(),
})
.option('experimental-lsp', {
type: 'boolean',
description:
'Enable experimental LSP (Language Server Protocol) feature for code intelligence',
default: false,
})
.option('channel', {
type: 'string',
choices: ['VSCode', 'ACP', 'SDK', 'CI'],
@ -713,6 +724,9 @@ export async function loadCliConfig(
.map(resolvePath)
.concat((argv.includeDirectories || []).map(resolvePath));
// LSP configuration: enabled only via --experimental-lsp flag
const lspEnabled = argv.experimentalLsp === true;
let lspClient: LspClient | undefined;
const question = argv.promptInteractive || argv.prompt || '';
const inputFormat: InputFormat =
(argv.inputFormat as InputFormat | undefined) ?? InputFormat.TEXT;
@ -924,7 +938,7 @@ export async function loadCliConfig(
const modelProvidersConfig = settings.modelProviders;
return new Config({
const config = new Config({
sessionId,
sessionData,
embeddingModel: DEFAULT_QWEN_EMBEDDING_MODEL,
@ -932,7 +946,7 @@ export async function loadCliConfig(
targetDir: cwd,
includeDirectories,
loadMemoryFromIncludeDirectories:
settings.context?.loadMemoryFromIncludeDirectories || false,
settings.context?.loadFromIncludeDirectories || false,
importFormat: settings.context?.importFormat || 'tree',
debugMode,
question,
@ -1016,7 +1030,34 @@ export async function loadCliConfig(
// always be true and the settings file can never disable recording.
chatRecording:
argv.chatRecording ?? settings.general?.chatRecording ?? true,
lsp: {
enabled: lspEnabled,
},
});
if (lspEnabled) {
try {
const lspService = new NativeLspService(
config,
config.getWorkspaceContext(),
appEvents,
fileService,
ideContextStore,
{
requireTrustedWorkspace: folderTrust,
},
);
await lspService.discoverAndPrepare();
await lspService.start();
lspClient = new NativeLspClient(lspService);
config.setLspClient(lspClient);
} catch (err) {
logger.warn('Failed to initialize native LSP service:', err);
}
}
return config;
}
function mergeExcludeTools(

View file

@ -218,14 +218,14 @@ describe('SettingsSchema', () => {
},
context: {
includeDirectories: ['/path/to/dir'],
loadMemoryFromIncludeDirectories: true,
loadFromIncludeDirectories: true,
},
};
// TypeScript should not complain about these properties
expect(settings.ui?.theme).toBe('dark');
expect(settings.context?.includeDirectories).toEqual(['/path/to/dir']);
expect(settings.context?.loadMemoryFromIncludeDirectories).toBe(true);
expect(settings.context?.loadFromIncludeDirectories).toBe(true);
});
it('should have includeDirectories setting in schema', () => {
@ -243,21 +243,19 @@ describe('SettingsSchema', () => {
).toEqual([]);
});
it('should have loadMemoryFromIncludeDirectories setting in schema', () => {
it('should have loadFromIncludeDirectories setting in schema', () => {
expect(
getSettingsSchema().context?.properties
.loadMemoryFromIncludeDirectories,
getSettingsSchema().context?.properties.loadFromIncludeDirectories,
).toBeDefined();
expect(
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
.type,
getSettingsSchema().context?.properties.loadFromIncludeDirectories.type,
).toBe('boolean');
expect(
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
getSettingsSchema().context?.properties.loadFromIncludeDirectories
.category,
).toBe('Context');
expect(
getSettingsSchema().context?.properties.loadMemoryFromIncludeDirectories
getSettingsSchema().context?.properties.loadFromIncludeDirectories
.default,
).toBe(false);
});

View file

@ -18,6 +18,7 @@ import {
DEFAULT_TRUNCATE_TOOL_OUTPUT_THRESHOLD,
} from '@qwen-code/qwen-code-core';
import type { CustomTheme } from '../ui/themes/theme.js';
import { getLanguageSettingsOptions } from '../i18n/languages.js';
export type SettingsType =
| 'boolean'
@ -210,13 +211,7 @@ const SETTINGS_SCHEMA = {
'You can also use custom language codes (e.g., "es", "fr") by placing JS language files ' +
'in ~/.qwen/locales/ (e.g., ~/.qwen/locales/es.js).',
showInDialog: true,
options: [
{ value: 'auto', label: 'Auto (detect from system)' },
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文 (Chinese)' },
{ value: 'ru', label: 'Русский (Russian)' },
{ value: 'de', label: 'Deutsch (German)' },
],
options: [] as readonly SettingEnumOption[],
},
outputLanguage: {
type: 'string',
@ -226,7 +221,7 @@ const SETTINGS_SCHEMA = {
default: 'auto',
description:
'The language for LLM output. Use "auto" to detect from system settings, ' +
'or set a specific language (e.g., "English", "中文", "日本語").',
'or set a specific language.',
showInDialog: true,
},
terminalBell: {
@ -693,7 +688,7 @@ const SETTINGS_SCHEMA = {
showInDialog: false,
mergeStrategy: MergeStrategy.CONCAT,
},
loadMemoryFromIncludeDirectories: {
loadFromIncludeDirectories: {
type: 'boolean',
label: 'Load Memory From Include Directories',
category: 'Context',
@ -1195,6 +1190,15 @@ const SETTINGS_SCHEMA = {
export type SettingsSchemaType = typeof SETTINGS_SCHEMA;
export function getSettingsSchema(): SettingsSchemaType {
// Inject dynamic language options
const schema = SETTINGS_SCHEMA as unknown as SettingsSchema;
if (schema['general']?.properties?.['language']) {
(
schema['general'].properties['language'] as {
options?: SettingEnumOption[];
}
).options = getLanguageSettingsOptions();
}
return SETTINGS_SCHEMA;
}