mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-05-01 21:20:44 +00:00
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:
parent
8420386d14
commit
05b56487ca
9 changed files with 350 additions and 596 deletions
|
|
@ -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()` 方法监控服务器运行状态
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* LSP Language Detector
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* LSP Response Normalizer
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
259
packages/cli/src/services/lsp/NativeLspClient.ts
Normal file
259
packages/cli/src/services/lsp/NativeLspClient.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue