feat: 统一LSP工具并扩展操作支持

- 创建统一的LSP工具,整合了之前的多个分散LSP工具
- 增加对更多LSP操作的支持,包括hover、documentSymbol、goToImplementation等
- 扩展LSP类型定义,支持Call Hierarchy等高级功能
- 更新配置和测试文件以适配新的LSP工具架构
- 保持向后兼容性,同时引入新工具名称映射

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

此更改是LSP工具重构计划的一部分,旨在提供更统一和功能完备的LSP集成体验。
This commit is contained in:
yiliang114 2026-01-18 19:34:17 +08:00
parent a14d1e27bb
commit d9328fa478
13 changed files with 4092 additions and 141 deletions

View file

@ -20,22 +20,24 @@ import { ExtensionStorage, type Extension } from './extension.js';
import * as ServerConfig from '@qwen-code/qwen-code-core';
import { isWorkspaceTrusted } from './trustedFolders.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { NativeLspService } from '../services/lsp/NativeLspService.js';
const mockDiscoverAndPrepare = vi.fn();
const mockStartLsp = vi.fn();
const mockDefinitions = vi.fn().mockResolvedValue([]);
const mockReferences = vi.fn().mockResolvedValue([]);
const mockWorkspaceSymbols = vi.fn().mockResolvedValue([]);
const nativeLspServiceMock = vi.fn().mockImplementation(() => ({
discoverAndPrepare: mockDiscoverAndPrepare,
start: mockStartLsp,
definitions: mockDefinitions,
references: mockReferences,
workspaceSymbols: mockWorkspaceSymbols,
}));
const createNativeLspServiceInstance = () => ({
discoverAndPrepare: vi.fn(),
start: vi.fn(),
definitions: vi.fn().mockResolvedValue([]),
references: vi.fn().mockResolvedValue([]),
workspaceSymbols: vi.fn().mockResolvedValue([]),
});
vi.mock('../services/lsp/NativeLspService.js', () => ({
NativeLspService: nativeLspServiceMock,
NativeLspService: vi.fn().mockImplementation(() => ({
discoverAndPrepare: vi.fn(),
start: vi.fn(),
definitions: vi.fn().mockResolvedValue([]),
references: vi.fn().mockResolvedValue([]),
workspaceSymbols: vi.fn().mockResolvedValue([]),
})),
}));
vi.mock('./trustedFolders.js', () => ({
@ -44,6 +46,17 @@ vi.mock('./trustedFolders.js', () => ({
.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');
@ -533,16 +546,10 @@ describe('loadCliConfig', () => {
beforeEach(() => {
vi.resetAllMocks();
mockDiscoverAndPrepare.mockReset();
mockStartLsp.mockReset();
mockWorkspaceSymbols.mockReset();
mockWorkspaceSymbols.mockResolvedValue([]);
nativeLspServiceMock.mockReset();
nativeLspServiceMock.mockImplementation(() => ({
discoverAndPrepare: mockDiscoverAndPrepare,
start: mockStartLsp,
workspaceSymbols: mockWorkspaceSymbols,
}));
nativeLspServiceMock.mockImplementation(() =>
createNativeLspServiceInstance(),
);
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
});
@ -637,8 +644,10 @@ describe('loadCliConfig', () => {
expect(config.getLspAllowed()).toEqual(['typescript-language-server']);
expect(config.getLspExcluded()).toEqual(['pylsp']);
expect(nativeLspServiceMock).toHaveBeenCalledTimes(1);
expect(mockDiscoverAndPrepare).toHaveBeenCalledTimes(1);
expect(mockStartLsp).toHaveBeenCalledTimes(1);
const lspInstance = getLastLspInstance();
expect(lspInstance).toBeDefined();
expect(lspInstance?.discoverAndPrepare).toHaveBeenCalledTimes(1);
expect(lspInstance?.start).toHaveBeenCalledTimes(1);
const options = nativeLspServiceMock.mock.calls[0][5];
expect(options?.allowedServers).toEqual(['typescript-language-server']);
@ -664,7 +673,7 @@ describe('loadCliConfig', () => {
expect(config.isLspEnabled()).toBe(true);
expect(nativeLspServiceMock).not.toHaveBeenCalled();
expect(mockDiscoverAndPrepare).not.toHaveBeenCalled();
expect(getLastLspInstance()).toBeUndefined();
});
it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => {

View file

@ -193,6 +193,67 @@ class NativeLspClient implements LspClient {
limit,
);
}
/**
* Get hover information (documentation, type info) for a symbol.
*/
hover(
location: Parameters<NativeLspService['hover']>[0],
serverName?: string,
) {
return this.service.hover(location, serverName);
}
/**
* Get all symbols in a document.
*/
documentSymbols(uri: string, serverName?: string, limit?: number) {
return this.service.documentSymbols(uri, serverName, limit);
}
/**
* Find implementations of an interface or abstract method.
*/
implementations(
location: Parameters<NativeLspService['implementations']>[0],
serverName?: string,
limit?: number,
) {
return this.service.implementations(location, serverName, limit);
}
/**
* Prepare call hierarchy item at a position (functions/methods).
*/
prepareCallHierarchy(
location: Parameters<NativeLspService['prepareCallHierarchy']>[0],
serverName?: string,
limit?: number,
) {
return this.service.prepareCallHierarchy(location, serverName, limit);
}
/**
* Find all functions/methods that call the given function.
*/
incomingCalls(
item: Parameters<NativeLspService['incomingCalls']>[0],
serverName?: string,
limit?: number,
) {
return this.service.incomingCalls(item, serverName, limit);
}
/**
* Find all functions/methods called by the given function.
*/
outgoingCalls(
item: Parameters<NativeLspService['outgoingCalls']>[0],
serverName?: string,
limit?: number,
) {
return this.service.outgoingCalls(item, serverName, limit);
}
}
function normalizeOutputFormat(
@ -812,6 +873,7 @@ export async function loadCliConfig(
const lspEnabled = settings.lsp?.enabled ?? false;
const lspAllowed = settings.lsp?.allowed ?? settings.mcp?.allowed;
const lspExcluded = settings.lsp?.excluded ?? settings.mcp?.excluded;
const lspLanguageServers = settings.lsp?.languageServers;
let lspClient: LspClient | undefined;
const question = argv.promptInteractive || argv.prompt || '';
const inputFormat: InputFormat =
@ -1149,6 +1211,7 @@ export async function loadCliConfig(
allowedServers: lspAllowed,
excludedServers: lspExcluded,
requireTrustedWorkspace: folderTrust,
inlineServerConfigs: lspLanguageServers,
},
);

View file

@ -1083,6 +1083,17 @@ const SETTINGS_SCHEMA = {
'Optional blocklist of LSP server names that should not start.',
showInDialog: false,
},
languageServers: {
type: 'object',
label: 'LSP Language Servers',
category: 'LSP',
requiresRestart: true,
default: {} as Record<string, unknown>,
description:
'Inline LSP server configuration (same format as .lsp.json).',
showInDialog: false,
mergeStrategy: MergeStrategy.SHALLOW_MERGE,
},
},
},
useSmartEdit: {