refactor(cli/lsp): extract NativeLspClient and simplify LSP service architecture

- Extract NativeLspClient from NativeLspService for better separation of concerns
- Simplify LspConfigLoader by removing built-in configuration options
- Remove LSP_DEBUGGING_GUIDE.md as it's no longer needed
- Clean up unused LSP constants
- Remove deprecated LSP configuration from config.ts
This commit is contained in:
yiliang114 2026-01-25 23:05:56 +08:00
parent 8420386d14
commit 05b56487ca
9 changed files with 350 additions and 596 deletions

View file

@ -1,156 +0,0 @@
# LSP 调试指南
本指南介绍如何调试 packages/cli 中的 LSP (Language Server Protocol) 功能。
## 1. 启用调试模式
CLI 支持调试模式,可以提供额外的日志信息:
```bash
# 使用 debug 标志运行
qwen --debug [你的命令]
# 或设置环境变量
DEBUG=true qwen [你的命令]
DEBUG_MODE=true qwen [你的命令]
```
## 2. LSP 配置选项
LSP 功能通过 `--experimental-lsp` 命令行参数启用。服务器配置通过以下方式定义:
- `.lsp.json` 文件:在项目根目录创建配置文件
- `lsp.languageServers`:在 `settings.json` 中内联配置
### 在 settings.json 中的示例配置
```json
{
"lsp": {
"languageServers": {
"typescript-language-server": {
"languages": ["typescript", "javascript"],
"command": "typescript-language-server",
"args": ["--stdio"]
}
}
}
}
```
### 在 .lsp.json 中的示例配置
```json
{
"typescript": {
"command": "typescript-language-server",
"args": ["--stdio"],
"extensionToLanguage": {
".ts": "typescript",
".tsx": "typescriptreact"
}
}
}
```
## 3. NativeLspService 调试功能
`NativeLspService` 类包含几个调试功能:
### 3.1 控制台日志
服务向控制台输出状态消息:
- `LSP 服务器 ${name} 启动成功` - 服务器成功启动
- `LSP 服务器 ${name} 启动失败` - 服务器启动失败
- `工作区不受信任,跳过 LSP 服务器发现` - 工作区不受信任,跳过发现
### 3.2 错误处理
服务具有全面的错误处理和详细的错误消息
### 3.3 状态跟踪
您可以通过 `getStatus()` 方法检查所有 LSP 服务器的状态
## 4. 调试命令
```bash
# 启用调试运行
qwen --debug --prompt "调试 LSP 功能"
# 检查在您的项目中检测到哪些 LSP 服务器
# 系统会自动检测语言和相应的 LSP 服务器
```
## 5. 手动 LSP 服务器配置
您还可以在项目根目录使用 `.lsp.json` 文件手动配置 LSP 服务器。
推荐使用新格式(以服务器名称为键),旧格式仍然兼容但会提示迁移:
```json
{
"languageServers": {
"pylsp": {
"command": "pylsp",
"args": [],
"languages": ["python"],
"transport": "stdio",
"settings": {},
"workspaceFolder": null,
"startupTimeout": 10000,
"shutdownTimeout": 3000,
"restartOnCrash": true,
"maxRestarts": 3,
"trustRequired": true
}
}
}
```
旧格式示例:
```json
{
"python": {
"command": "pylsp",
"args": [],
"transport": "stdio",
"trustRequired": true
}
}
```
## 6. LSP 问题排查
### 6.1 检查 LSP 服务器是否已安装
- 对于 TypeScript/JavaScript: `typescript-language-server`
- 对于 Python: `pylsp`
- 对于 Go: `gopls`
### 6.2 验证工作区信任
- LSP 服务器可能需要受信任的工作区才能启动
- 检查 `security.folderTrust.enabled` 设置
### 6.3 查看日志
- 查找以 `LSP 服务器` 开头的控制台消息
- 检查命令存在性和路径安全性问题
## 7. LSP 服务启动流程
LSP 服务的启动遵循以下流程:
1. **发现和准备**: `discoverAndPrepare()` 方法检测工作区中的编程语言
2. **创建服务器句柄**: 根据检测到的语言创建对应的服务器句柄
3. **启动服务器**: `start()` 方法启动所有服务器句柄
4. **状态管理**: 服务器状态在 `NOT_STARTED`, `IN_PROGRESS`, `READY`, `FAILED` 之间转换
## 8. 调试技巧
- 使用 `--debug` 标志查看详细的启动过程
- 检查工作区是否受信任(影响 LSP 服务器启动)
- 确认 LSP 服务器命令在系统 PATH 中可用
- 使用 `getStatus()` 方法监控服务器运行状态

View file

@ -45,6 +45,7 @@ import { getCliVersion } from '../utils/version.js';
import { loadSandboxConfig } from './sandboxConfig.js';
import { appEvents } from '../utils/events.js';
import { mcpCommand } from '../commands/mcp.js';
import { NativeLspClient } from '../services/lsp/NativeLspClient.js';
import { NativeLspService } from '../services/lsp/NativeLspService.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
@ -151,134 +152,6 @@ export interface CliArgs {
channel: string | undefined;
}
class NativeLspClient implements LspClient {
constructor(private readonly service: NativeLspService) {}
workspaceSymbols(query: string, limit?: number) {
return this.service.workspaceSymbols(query, limit);
}
definitions(
location: Parameters<NativeLspService['definitions']>[0],
serverName?: string,
limit?: number,
) {
return this.service.definitions(location, serverName, limit);
}
references(
location: Parameters<NativeLspService['references']>[0],
serverName?: string,
includeDeclaration?: boolean,
limit?: number,
) {
return this.service.references(
location,
serverName,
includeDeclaration,
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);
}
/**
* Get diagnostics for a specific document.
*/
diagnostics(uri: string, serverName?: string) {
return this.service.diagnostics(uri, serverName);
}
/**
* Get diagnostics for all open documents in the workspace.
*/
workspaceDiagnostics(serverName?: string, limit?: number) {
return this.service.workspaceDiagnostics(serverName, limit);
}
/**
* Get code actions available at a specific location.
*/
codeActions(
uri: string,
range: Parameters<NativeLspService['codeActions']>[1],
context: Parameters<NativeLspService['codeActions']>[2],
serverName?: string,
limit?: number,
) {
return this.service.codeActions(uri, range, context, serverName, limit);
}
/**
* Apply a workspace edit (from code action or other sources).
*/
applyWorkspaceEdit(
edit: Parameters<NativeLspService['applyWorkspaceEdit']>[0],
serverName?: string,
) {
return this.service.applyWorkspaceEdit(edit, serverName);
}
}
function normalizeOutputFormat(
format: string | OutputFormat | undefined,
): OutputFormat | undefined {

View file

@ -14,39 +14,28 @@ import type {
} from './LspTypes.js';
export class LspConfigLoader {
private warnedLegacyConfig = false;
constructor(private readonly workspaceRoot: string) {}
/**
* Load user .lsp.json configuration
* Load user .lsp.json configuration.
* Supports two official formats:
* 1. Basic format: { "language": { "command": "...", "extensionToLanguage": {...} } }
* 2. LanguageServers format: { "languageServers": { "server-name": { "languages": [...], ... } } }
*/
async loadUserConfigs(): Promise<LspServerConfig[]> {
const configs: LspServerConfig[] = [];
const sources: Array<{ origin: string; data: unknown }> = [];
const lspConfigPath = path.join(this.workspaceRoot, '.lsp.json');
if (fs.existsSync(lspConfigPath)) {
try {
const configContent = fs.readFileSync(lspConfigPath, 'utf-8');
sources.push({
origin: lspConfigPath,
data: JSON.parse(configContent),
});
} catch (error) {
console.warn('Failed to load user .lsp.json config:', error);
}
if (!fs.existsSync(lspConfigPath)) {
return [];
}
for (const source of sources) {
const parsed = this.parseConfigSource(source.data, source.origin);
if (parsed.usedLegacyFormat && parsed.configs.length > 0) {
this.warnLegacyConfig(source.origin);
}
configs.push(...parsed.configs);
try {
const configContent = fs.readFileSync(lspConfigPath, 'utf-8');
const data = JSON.parse(configContent);
return this.parseConfigSource(data, lspConfigPath);
} catch (error) {
console.warn('Failed to load user .lsp.json config:', error);
return [];
}
return configs;
}
/**
@ -164,40 +153,43 @@ export class LspConfigLoader {
return presets;
}
/**
* Parse configuration source and extract server configs.
* Detects format based on presence of 'languageServers' key.
*/
private parseConfigSource(
source: unknown,
origin: string,
): { configs: LspServerConfig[]; usedLegacyFormat: boolean } {
): LspServerConfig[] {
if (!this.isRecord(source)) {
return { configs: [], usedLegacyFormat: false };
return [];
}
const configs: LspServerConfig[] = [];
let serverMap: Record<string, unknown> = source;
let usedLegacyFormat = false;
if (this.isRecord(source['languageServers'])) {
serverMap = source['languageServers'] as Record<string, unknown>;
} else if (this.isNewFormatServerMap(source)) {
serverMap = source;
} else {
usedLegacyFormat = true;
}
// Determine format: languageServers wrapper vs basic format
const hasLanguageServersWrapper = this.isRecord(source['languageServers']);
const serverMap = hasLanguageServersWrapper
? (source['languageServers'] as Record<string, unknown>)
: source;
for (const [key, spec] of Object.entries(serverMap)) {
if (!this.isRecord(spec)) {
continue;
}
const languagesValue = spec['languages'];
const languages = usedLegacyFormat
? [key]
: (this.normalizeStringArray(languagesValue) ??
(typeof languagesValue === 'string' ? [languagesValue] : []));
// In basic format: key is language name, server name comes from command
// In languageServers format: key is server name, languages come from 'languages' array
const isBasicFormat = !hasLanguageServersWrapper && !spec['languages'];
const name = usedLegacyFormat
const languages = isBasicFormat
? [key]
: (this.normalizeStringArray(spec['languages']) ??
(typeof spec['languages'] === 'string' ? [spec['languages']] : []));
const name = isBasicFormat
? typeof spec['command'] === 'string'
? (spec['command'] as string)
? spec['command']
: key
: key;
@ -207,7 +199,7 @@ export class LspConfigLoader {
}
}
return { configs, usedLegacyFormat };
return configs;
}
private buildServerConfig(
@ -282,37 +274,6 @@ export class LspConfigLoader {
};
}
private isNewFormatServerMap(value: Record<string, unknown>): boolean {
return Object.values(value).some(
(entry) => this.isRecord(entry) && this.isNewFormatServerSpec(entry),
);
}
private isNewFormatServerSpec(value: Record<string, unknown>): boolean {
return (
Array.isArray(value['languages']) ||
this.isRecord(value['extensionToLanguage']) ||
this.isRecord(value['settings']) ||
value['workspaceFolder'] !== undefined ||
value['startupTimeout'] !== undefined ||
value['shutdownTimeout'] !== undefined ||
value['restartOnCrash'] !== undefined ||
value['maxRestarts'] !== undefined ||
this.isRecord(value['env']) ||
value['socket'] !== undefined
);
}
private warnLegacyConfig(origin: string): void {
if (this.warnedLegacyConfig) {
return;
}
console.warn(
`Legacy LSP config detected in ${origin}. Please migrate to the languageServers format.`,
);
this.warnedLegacyConfig = true;
}
private isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* LSP Language Detector
*

View file

@ -1,3 +1,9 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* LSP Response Normalizer
*

View file

@ -143,7 +143,13 @@ export class LspServerManager {
}
}
private isTypescriptServer(handle: LspServerHandle): boolean {
/**
* Check if the given handle is a TypeScript language server.
*
* @param handle - The LSP server handle
* @returns true if it's a TypeScript server
*/
isTypescriptServer(handle: LspServerHandle): boolean {
return (
handle.config.name.includes('typescript') ||
(handle.config.command?.includes('typescript') ?? false)

View file

@ -0,0 +1,259 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
/**
* NativeLspClient is an adapter that implements the LspClient interface
* by delegating all calls to NativeLspService.
*
* This class bridges the gap between the generic LspClient interface (defined in core)
* and the CLI-specific NativeLspService implementation.
*/
import type {
LspCallHierarchyIncomingCall,
LspCallHierarchyItem,
LspCallHierarchyOutgoingCall,
LspClient,
LspCodeAction,
LspCodeActionContext,
LspDefinition,
LspDiagnostic,
LspFileDiagnostics,
LspHoverResult,
LspLocation,
LspRange,
LspReference,
LspSymbolInformation,
LspWorkspaceEdit,
} from '@qwen-code/qwen-code-core';
import type { NativeLspService } from './NativeLspService.js';
/**
* Adapter class that implements LspClient by delegating to NativeLspService.
*
* @example
* ```typescript
* const lspService = new NativeLspService(config, workspaceContext, ...);
* await lspService.start();
* const lspClient = new NativeLspClient(lspService);
* config.setLspClient(lspClient);
* ```
*/
export class NativeLspClient implements LspClient {
/**
* Creates a new NativeLspClient instance.
*
* @param service - The NativeLspService instance to delegate calls to
*/
constructor(private readonly service: NativeLspService) {}
/**
* Search for symbols across the workspace.
*
* @param query - The search query string
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of symbol information
*/
workspaceSymbols(
query: string,
limit?: number,
): Promise<LspSymbolInformation[]> {
return this.service.workspaceSymbols(query, limit);
}
/**
* Find where a symbol is defined.
*
* @param location - The source location to find definitions for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of definition locations
*/
definitions(
location: LspLocation,
serverName?: string,
limit?: number,
): Promise<LspDefinition[]> {
return this.service.definitions(location, serverName, limit);
}
/**
* Find all references to a symbol.
*
* @param location - The source location to find references for
* @param serverName - Optional specific LSP server to query
* @param includeDeclaration - Whether to include the declaration in results
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of reference locations
*/
references(
location: LspLocation,
serverName?: string,
includeDeclaration?: boolean,
limit?: number,
): Promise<LspReference[]> {
return this.service.references(
location,
serverName,
includeDeclaration,
limit,
);
}
/**
* Get hover information (documentation, type info) for a symbol.
*
* @param location - The source location to get hover info for
* @param serverName - Optional specific LSP server to query
* @returns Promise resolving to hover result or null if not available
*/
hover(
location: LspLocation,
serverName?: string,
): Promise<LspHoverResult | null> {
return this.service.hover(location, serverName);
}
/**
* Get all symbols in a document.
*
* @param uri - The document URI to get symbols for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of symbol information
*/
documentSymbols(
uri: string,
serverName?: string,
limit?: number,
): Promise<LspSymbolInformation[]> {
return this.service.documentSymbols(uri, serverName, limit);
}
/**
* Find implementations of an interface or abstract method.
*
* @param location - The source location to find implementations for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of implementation locations
*/
implementations(
location: LspLocation,
serverName?: string,
limit?: number,
): Promise<LspDefinition[]> {
return this.service.implementations(location, serverName, limit);
}
/**
* Prepare call hierarchy item at a position (functions/methods).
*
* @param location - The source location to prepare call hierarchy for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of call hierarchy items
*/
prepareCallHierarchy(
location: LspLocation,
serverName?: string,
limit?: number,
): Promise<LspCallHierarchyItem[]> {
return this.service.prepareCallHierarchy(location, serverName, limit);
}
/**
* Find all functions/methods that call the given function.
*
* @param item - The call hierarchy item to find callers for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of incoming calls
*/
incomingCalls(
item: LspCallHierarchyItem,
serverName?: string,
limit?: number,
): Promise<LspCallHierarchyIncomingCall[]> {
return this.service.incomingCalls(item, serverName, limit);
}
/**
* Find all functions/methods called by the given function.
*
* @param item - The call hierarchy item to find callees for
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of results to return
* @returns Promise resolving to array of outgoing calls
*/
outgoingCalls(
item: LspCallHierarchyItem,
serverName?: string,
limit?: number,
): Promise<LspCallHierarchyOutgoingCall[]> {
return this.service.outgoingCalls(item, serverName, limit);
}
/**
* Get diagnostics for a specific document.
*
* @param uri - The document URI to get diagnostics for
* @param serverName - Optional specific LSP server to query
* @returns Promise resolving to array of diagnostics
*/
diagnostics(uri: string, serverName?: string): Promise<LspDiagnostic[]> {
return this.service.diagnostics(uri, serverName);
}
/**
* Get diagnostics for all open documents in the workspace.
*
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of file diagnostics to return
* @returns Promise resolving to array of file diagnostics
*/
workspaceDiagnostics(
serverName?: string,
limit?: number,
): Promise<LspFileDiagnostics[]> {
return this.service.workspaceDiagnostics(serverName, limit);
}
/**
* Get code actions available at a specific location.
*
* @param uri - The document URI
* @param range - The range to get code actions for
* @param context - The code action context including diagnostics
* @param serverName - Optional specific LSP server to query
* @param limit - Maximum number of code actions to return
* @returns Promise resolving to array of code actions
*/
codeActions(
uri: string,
range: LspRange,
context: LspCodeActionContext,
serverName?: string,
limit?: number,
): Promise<LspCodeAction[]> {
return this.service.codeActions(uri, range, context, serverName, limit);
}
/**
* Apply a workspace edit (from code action or other sources).
*
* @param edit - The workspace edit to apply
* @param serverName - Optional specific LSP server context
* @returns Promise resolving to true if edit was applied successfully
*/
applyWorkspaceEdit(
edit: LspWorkspaceEdit,
serverName?: string,
): Promise<boolean> {
return this.service.applyWorkspaceEdit(edit, serverName);
}
}

View file

@ -34,6 +34,7 @@ import type {
LspServerHandle,
LspServerStatus,
NativeLspServiceOptions,
LspConnectionInterface,
} from './LspTypes.js';
import * as path from 'path';
import { fileURLToPath } from 'url';
@ -131,6 +132,29 @@ export class NativeLspService {
return this.serverManager.getStatus();
}
/**
* Get ready server handles filtered by optional server name.
* Each handle is guaranteed to have a valid connection.
*
* @param serverName - Optional server name to filter by
* @returns Array of [serverName, handle] tuples with active connections
*/
private getReadyHandles(
serverName?: string,
): Array<[string, LspServerHandle & { connection: LspConnectionInterface }]> {
return Array.from(this.serverManager.getHandles().entries()).filter(
(
entry,
): entry is [
string,
LspServerHandle & { connection: LspConnectionInterface },
] =>
entry[1].status === 'READY' &&
entry[1].connection !== undefined &&
(!serverName || entry[0] === serverName),
);
}
/**
* Workspace symbol search across all ready LSP servers.
*/
@ -152,7 +176,7 @@ export class NativeLspService {
query,
});
if (
this.isTypescriptServer(handle) &&
this.serverManager.isTypescriptServer(handle) &&
this.isNoProjectErrorResponse(response)
) {
await this.serverManager.warmupTypescriptServer(handle, true);
@ -191,19 +215,9 @@ export class NativeLspService {
serverName?: string,
limit = 50,
): Promise<LspDefinition[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -248,19 +262,9 @@ export class NativeLspService {
includeDeclaration = false,
limit = 200,
): Promise<LspReference[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -302,19 +306,9 @@ export class NativeLspService {
location: LspLocation,
serverName?: string,
): Promise<LspHoverResult | null> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request('textDocument/hover', {
@ -341,19 +335,9 @@ export class NativeLspService {
serverName?: string,
limit = 200,
): Promise<LspSymbolInformation[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -414,19 +398,9 @@ export class NativeLspService {
serverName?: string,
limit = 50,
): Promise<LspDefinition[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -476,19 +450,9 @@ export class NativeLspService {
serverName?: string,
limit = 50,
): Promise<LspCallHierarchyItem[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -539,19 +503,9 @@ export class NativeLspService {
limit = 50,
): Promise<LspCallHierarchyIncomingCall[]> {
const targetServer = serverName ?? item.serverName;
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!targetServer || name === targetServer),
);
const handles = this.getReadyHandles(targetServer);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -596,19 +550,9 @@ export class NativeLspService {
limit = 50,
): Promise<LspCallHierarchyOutgoingCall[]> {
const targetServer = serverName ?? item.serverName;
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!targetServer || name === targetServer),
);
const handles = this.getReadyHandles(targetServer);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
const response = await handle.connection.request(
@ -651,21 +595,10 @@ export class NativeLspService {
uri: string,
serverName?: string,
): Promise<LspDiagnostic[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
const allDiagnostics: LspDiagnostic[] = [];
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
@ -709,21 +642,10 @@ export class NativeLspService {
serverName?: string,
limit = 100,
): Promise<LspFileDiagnostics[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
const results: LspFileDiagnostics[] = [];
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
@ -775,19 +697,9 @@ export class NativeLspService {
serverName?: string,
limit = 20,
): Promise<LspCodeAction[]> {
const handles = Array.from(
this.serverManager.getHandles().entries(),
).filter(
([name, handle]) =>
handle.status === 'READY' &&
handle.connection &&
(!serverName || name === serverName),
);
const handles = this.getReadyHandles(serverName);
for (const [name, handle] of handles) {
if (!handle.connection) {
continue;
}
try {
await this.serverManager.warmupTypescriptServer(handle);
@ -930,13 +842,6 @@ export class NativeLspService {
fs.writeFileSync(filePath, lines.join('\n'), 'utf-8');
}
private isTypescriptServer(handle: LspServerHandle): boolean {
return (
handle.config.name.includes('typescript') ||
(handle.config.command?.includes('typescript') ?? false)
);
}
private isNoProjectErrorResponse(response: unknown): boolean {
if (!response) {
return false;

View file

@ -25,9 +25,6 @@ export const DEFAULT_LSP_WARMUP_DELAY_MS = 150;
/** Default timeout for command existence check in milliseconds */
export const DEFAULT_LSP_COMMAND_CHECK_TIMEOUT_MS = 2000;
/** Default timeout for LSP server shutdown in milliseconds */
export const DEFAULT_LSP_SHUTDOWN_TIMEOUT_MS = 5000;
// ============================================================================
// Retry Constants
// ============================================================================
@ -105,106 +102,3 @@ export const CODE_ACTION_KIND_LABELS: Record<string, LspCodeActionKind> = {
'source.organizeImports': 'source.organizeImports',
'source.fixAll': 'source.fixAll',
};
// ============================================================================
// Language Detection
// ============================================================================
/**
* Common root marker files that indicate project type/language.
*/
export const COMMON_ROOT_MARKERS = [
'package.json',
'tsconfig.json',
'pyproject.toml',
'go.mod',
'Cargo.toml',
'pom.xml',
'build.gradle',
'composer.json',
'Gemfile',
'mix.exs',
'deno.json',
] as const;
/**
* Mapping from root marker files to programming languages.
*/
export const MARKER_TO_LANGUAGE: Record<string, string> = {
'package.json': 'javascript',
'tsconfig.json': 'typescript',
'pyproject.toml': 'python',
'go.mod': 'go',
'Cargo.toml': 'rust',
'pom.xml': 'java',
'build.gradle': 'java',
'composer.json': 'php',
Gemfile: 'ruby',
'*.sln': 'csharp',
'mix.exs': 'elixir',
'deno.json': 'deno',
};
/**
* Default mapping from file extensions to language identifiers.
*/
export const DEFAULT_EXTENSION_TO_LANGUAGE: Record<string, string> = {
js: 'javascript',
ts: 'typescript',
jsx: 'javascriptreact',
tsx: 'typescriptreact',
py: 'python',
go: 'go',
rs: 'rust',
java: 'java',
cpp: 'cpp',
c: 'c',
php: 'php',
rb: 'ruby',
cs: 'csharp',
vue: 'vue',
svelte: 'svelte',
html: 'html',
css: 'css',
json: 'json',
yaml: 'yaml',
yml: 'yaml',
};
/**
* Glob patterns to exclude when detecting languages.
*/
export const LANGUAGE_DETECTION_EXCLUDE_PATTERNS = [
'**/node_modules/**',
'**/.git/**',
'**/dist/**',
'**/build/**',
] as const;
// ============================================================================
// Default Limits for LSP Operations
// ============================================================================
/** Default limit for workspace symbol search results */
export const DEFAULT_LSP_WORKSPACE_SYMBOL_LIMIT = 50;
/** Default limit for definition/implementation results */
export const DEFAULT_LSP_DEFINITION_LIMIT = 50;
/** Default limit for reference results */
export const DEFAULT_LSP_REFERENCE_LIMIT = 200;
/** Default limit for document symbol results */
export const DEFAULT_LSP_DOCUMENT_SYMBOL_LIMIT = 200;
/** Default limit for call hierarchy results */
export const DEFAULT_LSP_CALL_HIERARCHY_LIMIT = 50;
/** Default limit for diagnostics results */
export const DEFAULT_LSP_DIAGNOSTICS_LIMIT = 100;
/** Default limit for code action results */
export const DEFAULT_LSP_CODE_ACTION_LIMIT = 20;
/** Maximum number of files to scan during language detection */
export const DEFAULT_LSP_LANGUAGE_DETECTION_FILE_LIMIT = 1000;