mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-05 01:50:27 +08:00
Add TypeScript LSP setup guide and enhance debugging tests
- Created a comprehensive guide for setting up TypeScript LSP in Claude Code, detailing installation methods, configuration, and troubleshooting. - Added multiple debugging test scripts to validate LSP communication with pyright, including direct communication tests, configuration checks, and document symbol retrieval. - Implemented error handling and logging for better visibility during LSP interactions.
This commit is contained in:
366
.claude/TYPESCRIPT_LSP_SETUP.md
Normal file
366
.claude/TYPESCRIPT_LSP_SETUP.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# Claude Code TypeScript LSP 配置指南
|
||||
|
||||
> 更新日期: 2026-01-20
|
||||
> 适用版本: Claude Code v2.0.74+
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [方式一:插件市场(推荐)](#方式一插件市场推荐)
|
||||
2. [方式二:MCP Server (cclsp)](#方式二mcp-server-cclsp)
|
||||
3. [方式三:内置LSP工具](#方式三内置lsp工具)
|
||||
4. [配置验证](#配置验证)
|
||||
5. [故障排查](#故障排查)
|
||||
|
||||
---
|
||||
|
||||
## 方式一:插件市场(推荐)
|
||||
|
||||
### 步骤 1: 添加插件市场
|
||||
|
||||
在Claude Code中执行:
|
||||
|
||||
```bash
|
||||
/plugin marketplace add boostvolt/claude-code-lsps
|
||||
```
|
||||
|
||||
### 步骤 2: 安装TypeScript LSP插件
|
||||
|
||||
```bash
|
||||
# TypeScript/JavaScript支持(推荐vtsls)
|
||||
/plugin install vtsls@claude-code-lsps
|
||||
```
|
||||
|
||||
### 步骤 3: 验证安装
|
||||
|
||||
```bash
|
||||
/plugin list
|
||||
```
|
||||
|
||||
应该看到:
|
||||
```
|
||||
✓ vtsls@claude-code-lsps (enabled)
|
||||
✓ pyright-lsp@claude-plugins-official (enabled)
|
||||
```
|
||||
|
||||
### 配置文件自动更新
|
||||
|
||||
安装后,`~/.claude/settings.json` 会自动添加:
|
||||
|
||||
```json
|
||||
{
|
||||
"enabledPlugins": {
|
||||
"pyright-lsp@claude-plugins-official": true,
|
||||
"vtsls@claude-code-lsps": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 支持的操作
|
||||
|
||||
- `goToDefinition` - 跳转到定义
|
||||
- `findReferences` - 查找引用
|
||||
- `hover` - 显示类型信息
|
||||
- `documentSymbol` - 文档符号
|
||||
- `getDiagnostics` - 诊断信息
|
||||
|
||||
---
|
||||
|
||||
## 方式二:MCP Server (cclsp)
|
||||
|
||||
### 优势
|
||||
|
||||
- **位置容错**:自动修正AI生成的不精确行号
|
||||
- **更多功能**:支持重命名、完整诊断
|
||||
- **灵活配置**:完全自定义LSP服务器
|
||||
|
||||
### 安装步骤
|
||||
|
||||
#### 1. 安装TypeScript Language Server
|
||||
|
||||
```bash
|
||||
npm install -g typescript-language-server typescript
|
||||
```
|
||||
|
||||
验证安装:
|
||||
```bash
|
||||
typescript-language-server --version
|
||||
```
|
||||
|
||||
#### 2. 配置cclsp
|
||||
|
||||
运行自动配置:
|
||||
```bash
|
||||
npx cclsp@latest setup --user
|
||||
```
|
||||
|
||||
或手动创建配置文件:
|
||||
|
||||
**文件位置**: `~/.claude/cclsp.json` 或 `~/.config/claude/cclsp.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"servers": [
|
||||
{
|
||||
"extensions": ["ts", "tsx", "js", "jsx"],
|
||||
"command": ["typescript-language-server", "--stdio"],
|
||||
"rootDir": ".",
|
||||
"restartInterval": 5,
|
||||
"initializationOptions": {
|
||||
"preferences": {
|
||||
"includeInlayParameterNameHints": "all",
|
||||
"includeInlayPropertyDeclarationTypeHints": true,
|
||||
"includeInlayFunctionParameterTypeHints": true,
|
||||
"includeInlayVariableTypeHints": true
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"extensions": ["py", "pyi"],
|
||||
"command": ["pylsp"],
|
||||
"rootDir": ".",
|
||||
"restartInterval": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 在Claude Code中启用MCP Server
|
||||
|
||||
添加到Claude Code配置:
|
||||
|
||||
```bash
|
||||
# 查看当前MCP配置
|
||||
cat ~/.claude/.mcp.json
|
||||
|
||||
# 如果没有,创建新的
|
||||
```
|
||||
|
||||
**文件**: `~/.claude/.mcp.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"cclsp": {
|
||||
"command": "npx",
|
||||
"args": ["cclsp@latest"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### cclsp可用的MCP工具
|
||||
|
||||
使用时,Claude Code会自动调用这些工具:
|
||||
|
||||
- `find_definition` - 按名称查找定义(支持模糊匹配)
|
||||
- `find_references` - 查找所有引用
|
||||
- `rename_symbol` - 重命名符号(带备份)
|
||||
- `get_diagnostics` - 获取诊断信息
|
||||
- `restart_server` - 重启LSP服务器
|
||||
|
||||
---
|
||||
|
||||
## 方式三:内置LSP工具
|
||||
|
||||
### 启用方式
|
||||
|
||||
设置环境变量:
|
||||
|
||||
**Linux/Mac**:
|
||||
```bash
|
||||
export ENABLE_LSP_TOOL=1
|
||||
claude
|
||||
```
|
||||
|
||||
**Windows (PowerShell)**:
|
||||
```powershell
|
||||
$env:ENABLE_LSP_TOOL=1
|
||||
claude
|
||||
```
|
||||
|
||||
**永久启用** (添加到shell配置):
|
||||
```bash
|
||||
# Linux/Mac
|
||||
echo 'export ENABLE_LSP_TOOL=1' >> ~/.bashrc
|
||||
source ~/.bashrc
|
||||
|
||||
# Windows (PowerShell Profile)
|
||||
Add-Content $PROFILE '$env:ENABLE_LSP_TOOL=1'
|
||||
```
|
||||
|
||||
### 限制
|
||||
|
||||
- 需要先安装语言服务器插件(见方式一)
|
||||
- 不支持重命名等高级操作
|
||||
- 无位置容错功能
|
||||
|
||||
---
|
||||
|
||||
## 配置验证
|
||||
|
||||
### 1. 检查LSP服务器是否可用
|
||||
|
||||
```bash
|
||||
# 检查TypeScript Language Server
|
||||
which typescript-language-server # Linux/Mac
|
||||
where typescript-language-server # Windows
|
||||
|
||||
# 测试运行
|
||||
typescript-language-server --stdio
|
||||
```
|
||||
|
||||
### 2. 在Claude Code中测试
|
||||
|
||||
打开任意TypeScript文件,让Claude执行:
|
||||
|
||||
```typescript
|
||||
// 测试LSP功能
|
||||
LSP({
|
||||
operation: "hover",
|
||||
filePath: "path/to/your/file.ts",
|
||||
line: 10,
|
||||
character: 5
|
||||
})
|
||||
```
|
||||
|
||||
### 3. 检查插件状态
|
||||
|
||||
```bash
|
||||
/plugin list
|
||||
```
|
||||
|
||||
查看启用的插件:
|
||||
```bash
|
||||
cat ~/.claude/settings.json | grep enabledPlugins
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 问题 1: "No LSP server available"
|
||||
|
||||
**原因**:TypeScript LSP插件未安装或未启用
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 重新安装插件
|
||||
/plugin install vtsls@claude-code-lsps
|
||||
|
||||
# 检查settings.json
|
||||
cat ~/.claude/settings.json
|
||||
```
|
||||
|
||||
### 问题 2: "typescript-language-server: command not found"
|
||||
|
||||
**原因**:未安装TypeScript Language Server
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
npm install -g typescript-language-server typescript
|
||||
|
||||
# 验证
|
||||
typescript-language-server --version
|
||||
```
|
||||
|
||||
### 问题 3: LSP响应慢或超时
|
||||
|
||||
**原因**:项目太大或配置不当
|
||||
|
||||
**解决**:
|
||||
```json
|
||||
// 在tsconfig.json中优化
|
||||
{
|
||||
"compilerOptions": {
|
||||
"incremental": true,
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
```
|
||||
|
||||
### 问题 4: 插件安装失败
|
||||
|
||||
**原因**:网络问题或插件市场未添加
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 确认插件市场已添加
|
||||
/plugin marketplace list
|
||||
|
||||
# 如果没有,重新添加
|
||||
/plugin marketplace add boostvolt/claude-code-lsps
|
||||
|
||||
# 重试安装
|
||||
/plugin install vtsls@claude-code-lsps
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三种方式对比
|
||||
|
||||
| 特性 | 插件市场 | cclsp (MCP) | 内置LSP |
|
||||
|------|----------|-------------|---------|
|
||||
| 安装复杂度 | ⭐ 低 | ⭐⭐ 中 | ⭐ 低 |
|
||||
| 功能完整性 | ⭐⭐⭐ 完整 | ⭐⭐⭐ 完整+ | ⭐⭐ 基础 |
|
||||
| 位置容错 | ❌ 无 | ✅ 有 | ❌ 无 |
|
||||
| 重命名支持 | ✅ 有 | ✅ 有 | ❌ 无 |
|
||||
| 自定义配置 | ⚙️ 有限 | ⚙️ 完整 | ❌ 无 |
|
||||
| 生产稳定性 | ⭐⭐⭐ 高 | ⭐⭐ 中 | ⭐⭐⭐ 高 |
|
||||
|
||||
---
|
||||
|
||||
## 推荐配置
|
||||
|
||||
### 新手用户
|
||||
**推荐**: 方式一(插件市场)
|
||||
- 一条命令安装
|
||||
- 官方维护,稳定可靠
|
||||
- 满足日常使用需求
|
||||
|
||||
### 高级用户
|
||||
**推荐**: 方式二(cclsp)
|
||||
- 完整功能支持
|
||||
- 位置容错(AI友好)
|
||||
- 灵活配置
|
||||
- 支持重命名等高级操作
|
||||
|
||||
### 快速测试
|
||||
**推荐**: 方式三(内置LSP)+ 方式一(插件)
|
||||
- 设置环境变量
|
||||
- 安装插件
|
||||
- 立即可用
|
||||
|
||||
---
|
||||
|
||||
## 附录:支持的语言
|
||||
|
||||
通过插件市场可用的LSP:
|
||||
|
||||
| 语言 | 插件名 | 安装命令 |
|
||||
|------|--------|----------|
|
||||
| TypeScript/JavaScript | vtsls | `/plugin install vtsls@claude-code-lsps` |
|
||||
| Python | pyright | `/plugin install pyright@claude-code-lsps` |
|
||||
| Go | gopls | `/plugin install gopls@claude-code-lsps` |
|
||||
| Rust | rust-analyzer | `/plugin install rust-analyzer@claude-code-lsps` |
|
||||
| Java | jdtls | `/plugin install jdtls@claude-code-lsps` |
|
||||
| C/C++ | clangd | `/plugin install clangd@claude-code-lsps` |
|
||||
| C# | omnisharp | `/plugin install omnisharp@claude-code-lsps` |
|
||||
| PHP | intelephense | `/plugin install intelephense@claude-code-lsps` |
|
||||
| Kotlin | kotlin-ls | `/plugin install kotlin-language-server@claude-code-lsps` |
|
||||
| Ruby | solargraph | `/plugin install solargraph@claude-code-lsps` |
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [Claude Code LSP 文档](https://docs.anthropic.com/claude-code/lsp)
|
||||
- [cclsp GitHub](https://github.com/ktnyt/cclsp)
|
||||
- [TypeScript Language Server](https://github.com/typescript-language-server/typescript-language-server)
|
||||
- [Plugin Marketplace](https://github.com/boostvolt/claude-code-lsps)
|
||||
|
||||
---
|
||||
|
||||
**配置完成后,重启Claude Code以应用更改**
|
||||
@@ -34,11 +34,56 @@ async function syncActiveExecutions() {
|
||||
const { executions } = await response.json();
|
||||
if (!executions || executions.length === 0) return;
|
||||
|
||||
executions.forEach(exec => {
|
||||
// Skip if already tracked (avoid overwriting live data)
|
||||
if (cliStreamExecutions[exec.id]) return;
|
||||
let needsUiUpdate = false;
|
||||
|
||||
// Rebuild execution state
|
||||
executions.forEach(exec => {
|
||||
const existing = cliStreamExecutions[exec.id];
|
||||
|
||||
// Parse historical output from server
|
||||
const historicalLines = [];
|
||||
if (exec.output) {
|
||||
const lines = exec.output.split('\n');
|
||||
const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1);
|
||||
lines.slice(startIndex).forEach(line => {
|
||||
if (line.trim()) {
|
||||
historicalLines.push({
|
||||
type: 'stdout',
|
||||
content: line,
|
||||
timestamp: exec.startTime || Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (existing) {
|
||||
// Already tracked by WebSocket events - merge historical output
|
||||
// Only prepend historical lines that are not already in the output
|
||||
// (WebSocket events only add NEW output, so historical output should come before)
|
||||
const existingContentSet = new Set(existing.output.map(o => o.content));
|
||||
const missingLines = historicalLines.filter(h => !existingContentSet.has(h.content));
|
||||
|
||||
if (missingLines.length > 0) {
|
||||
// Find the system start message index (skip it when prepending)
|
||||
const systemMsgIndex = existing.output.findIndex(o => o.type === 'system');
|
||||
const insertIndex = systemMsgIndex >= 0 ? systemMsgIndex + 1 : 0;
|
||||
|
||||
// Prepend missing historical lines after system message
|
||||
existing.output.splice(insertIndex, 0, ...missingLines);
|
||||
|
||||
// Trim if too long
|
||||
if (existing.output.length > MAX_OUTPUT_LINES) {
|
||||
existing.output = existing.output.slice(-MAX_OUTPUT_LINES);
|
||||
}
|
||||
|
||||
needsUiUpdate = true;
|
||||
console.log(`[CLI Stream] Merged ${missingLines.length} historical lines for ${exec.id}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
needsUiUpdate = true;
|
||||
|
||||
// New execution - rebuild full state
|
||||
cliStreamExecutions[exec.id] = {
|
||||
tool: exec.tool || 'cli',
|
||||
mode: exec.mode || 'analysis',
|
||||
@@ -55,24 +100,12 @@ async function syncActiveExecutions() {
|
||||
timestamp: exec.startTime
|
||||
});
|
||||
|
||||
// Fill historical output (limit to last MAX_OUTPUT_LINES)
|
||||
if (exec.output) {
|
||||
const lines = exec.output.split('\n');
|
||||
const startIndex = Math.max(0, lines.length - MAX_OUTPUT_LINES + 1);
|
||||
lines.slice(startIndex).forEach(line => {
|
||||
if (line.trim()) {
|
||||
cliStreamExecutions[exec.id].output.push({
|
||||
type: 'stdout',
|
||||
content: line,
|
||||
timestamp: Date.now()
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
// Add historical output
|
||||
cliStreamExecutions[exec.id].output.push(...historicalLines);
|
||||
});
|
||||
|
||||
// Update UI if we recovered any executions
|
||||
if (executions.length > 0) {
|
||||
// Update UI if we recovered or merged any executions
|
||||
if (needsUiUpdate) {
|
||||
// Set active tab to first running execution
|
||||
const runningExec = executions.find(e => e.status === 'running');
|
||||
if (runningExec && !activeStreamTab) {
|
||||
|
||||
@@ -119,6 +119,7 @@ export class CliHistoryStore {
|
||||
this.db = new Database(this.dbPath);
|
||||
this.db.pragma('journal_mode = WAL');
|
||||
this.db.pragma('synchronous = NORMAL');
|
||||
this.db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds for locks
|
||||
|
||||
this.initSchema();
|
||||
this.migrateFromJson(historyDir);
|
||||
@@ -365,6 +366,41 @@ export class CliHistoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a database operation with retry logic for SQLITE_BUSY errors
|
||||
* @param operation - Function to execute
|
||||
* @param maxRetries - Maximum retry attempts (default: 3)
|
||||
* @param baseDelay - Base delay in ms for exponential backoff (default: 100)
|
||||
*/
|
||||
private withRetry<T>(operation: () => T, maxRetries = 3, baseDelay = 100): T {
|
||||
let lastError: Error | null = null;
|
||||
|
||||
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
return operation();
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
// Check if it's a SQLITE_BUSY error
|
||||
if (error.message?.includes('SQLITE_BUSY') || error.message?.includes('database is locked')) {
|
||||
lastError = error;
|
||||
if (attempt < maxRetries) {
|
||||
// Exponential backoff: 100ms, 200ms, 400ms
|
||||
const delay = baseDelay * Math.pow(2, attempt);
|
||||
// Sync sleep using Atomics (works in Node.js)
|
||||
const sharedBuffer = new SharedArrayBuffer(4);
|
||||
const sharedArray = new Int32Array(sharedBuffer);
|
||||
Atomics.wait(sharedArray, 0, 0, delay);
|
||||
}
|
||||
} else {
|
||||
// Non-BUSY error, throw immediately
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError || new Error('Operation failed after retries');
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate existing JSON files to SQLite
|
||||
*/
|
||||
@@ -522,7 +558,7 @@ export class CliHistoryStore {
|
||||
}
|
||||
});
|
||||
|
||||
transaction();
|
||||
this.withRetry(() => transaction());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -795,7 +831,9 @@ export class CliHistoryStore {
|
||||
*/
|
||||
deleteConversation(id: string): { success: boolean; error?: string } {
|
||||
try {
|
||||
const result = this.db.prepare('DELETE FROM conversations WHERE id = ?').run(id);
|
||||
const result = this.withRetry(() =>
|
||||
this.db.prepare('DELETE FROM conversations WHERE id = ?').run(id)
|
||||
);
|
||||
return { success: result.changes > 0 };
|
||||
} catch (err) {
|
||||
return { success: false, error: (err as Error).message };
|
||||
@@ -821,7 +859,7 @@ export class CliHistoryStore {
|
||||
}
|
||||
});
|
||||
|
||||
transaction();
|
||||
this.withRetry(() => transaction());
|
||||
|
||||
return {
|
||||
success: true,
|
||||
@@ -896,14 +934,14 @@ export class CliHistoryStore {
|
||||
project_hash = @project_hash
|
||||
`);
|
||||
|
||||
stmt.run({
|
||||
this.withRetry(() => stmt.run({
|
||||
ccw_id: mapping.ccw_id,
|
||||
tool: mapping.tool,
|
||||
native_session_id: mapping.native_session_id,
|
||||
native_session_path: mapping.native_session_path || null,
|
||||
project_hash: mapping.project_hash || null,
|
||||
created_at: mapping.created_at || new Date().toISOString()
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1147,7 +1185,7 @@ export class CliHistoryStore {
|
||||
VALUES (@id, @created_at, @tool, @prompt_count, @patterns, @suggestions, @raw_output, @execution_id, @lang)
|
||||
`);
|
||||
|
||||
stmt.run({
|
||||
this.withRetry(() => stmt.run({
|
||||
id: insight.id,
|
||||
created_at: new Date().toISOString(),
|
||||
tool: insight.tool,
|
||||
@@ -1157,7 +1195,7 @@ export class CliHistoryStore {
|
||||
raw_output: insight.rawOutput || null,
|
||||
execution_id: insight.executionId || null,
|
||||
lang: insight.lang || 'en'
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1249,7 +1287,7 @@ export class CliHistoryStore {
|
||||
updated_at = @updated_at
|
||||
`);
|
||||
|
||||
const result = stmt.run({
|
||||
const result = this.withRetry(() => stmt.run({
|
||||
execution_id: review.execution_id,
|
||||
status: review.status,
|
||||
rating: review.rating ?? null,
|
||||
@@ -1257,7 +1295,7 @@ export class CliHistoryStore {
|
||||
reviewer: review.reviewer ?? null,
|
||||
created_at,
|
||||
updated_at
|
||||
});
|
||||
}));
|
||||
|
||||
return {
|
||||
id: result.lastInsertRowid as number,
|
||||
|
||||
@@ -42,10 +42,10 @@ class ServerConfig:
|
||||
max_restarts: int = 3
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass
|
||||
class ServerState:
|
||||
"""State of a running language server."""
|
||||
|
||||
|
||||
config: ServerConfig
|
||||
process: asyncio.subprocess.Process
|
||||
reader: asyncio.StreamReader
|
||||
@@ -55,6 +55,8 @@ class ServerState:
|
||||
capabilities: Dict[str, Any] = field(default_factory=dict)
|
||||
pending_requests: Dict[int, asyncio.Future] = field(default_factory=dict)
|
||||
restart_count: int = 0
|
||||
# Queue for producer-consumer pattern - continuous reading puts messages here
|
||||
message_queue: asyncio.Queue = field(default_factory=asyncio.Queue)
|
||||
|
||||
|
||||
class StandaloneLspManager:
|
||||
@@ -253,20 +255,24 @@ class StandaloneLspManager:
|
||||
|
||||
self._servers[language_id] = state
|
||||
|
||||
# Start reading responses in background
|
||||
self._read_tasks[language_id] = asyncio.create_task(
|
||||
self._read_responses(language_id)
|
||||
)
|
||||
|
||||
# Start reading stderr in background (prevents pipe buffer from filling up)
|
||||
if process.stderr:
|
||||
self._stderr_tasks[language_id] = asyncio.create_task(
|
||||
self._read_stderr(language_id, process.stderr)
|
||||
)
|
||||
|
||||
# Initialize the server
|
||||
# CRITICAL: Start the continuous reader task IMMEDIATELY before any communication
|
||||
# This ensures no messages are lost during initialization handshake
|
||||
self._read_tasks[language_id] = asyncio.create_task(
|
||||
self._continuous_reader(language_id)
|
||||
)
|
||||
|
||||
# Start the message processor task to handle queued messages
|
||||
asyncio.create_task(self._process_messages(language_id))
|
||||
|
||||
# Initialize the server - now uses queue for reading responses
|
||||
await self._initialize_server(state)
|
||||
|
||||
|
||||
logger.info(f"{config.display_name} started and initialized")
|
||||
return state
|
||||
|
||||
@@ -353,49 +359,25 @@ class StandaloneLspManager:
|
||||
return await self._start_server(language_id)
|
||||
|
||||
async def _initialize_server(self, state: ServerState) -> None:
|
||||
"""Send initialize request to language server."""
|
||||
"""Send initialize request and wait for response via the message queue.
|
||||
|
||||
The continuous reader and message processor are already running, so we just
|
||||
send the request and wait for the response via pending_requests.
|
||||
"""
|
||||
root_uri = self.workspace_root.as_uri()
|
||||
|
||||
|
||||
# Simplified params matching direct test that works
|
||||
params = {
|
||||
"processId": os.getpid(),
|
||||
"processId": None, # Use None like direct test
|
||||
"rootUri": root_uri,
|
||||
"rootPath": str(self.workspace_root),
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"synchronization": {
|
||||
"dynamicRegistration": False,
|
||||
"willSave": False,
|
||||
"willSaveWaitUntil": False,
|
||||
"didSave": True,
|
||||
},
|
||||
"completion": {
|
||||
"dynamicRegistration": False,
|
||||
"completionItem": {
|
||||
"snippetSupport": False,
|
||||
"documentationFormat": ["plaintext", "markdown"],
|
||||
},
|
||||
},
|
||||
"hover": {
|
||||
"dynamicRegistration": False,
|
||||
"contentFormat": ["plaintext", "markdown"],
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": False,
|
||||
"linkSupport": False,
|
||||
},
|
||||
"references": {
|
||||
"dynamicRegistration": False,
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": False,
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
"callHierarchy": {
|
||||
"dynamicRegistration": False,
|
||||
},
|
||||
},
|
||||
"workspace": {
|
||||
"workspaceFolders": True,
|
||||
"configuration": True,
|
||||
},
|
||||
},
|
||||
@@ -405,17 +387,49 @@ class StandaloneLspManager:
|
||||
"name": self.workspace_root.name,
|
||||
}
|
||||
],
|
||||
"initializationOptions": state.config.initialization_options,
|
||||
}
|
||||
|
||||
result = await self._send_request(state, "initialize", params)
|
||||
|
||||
if result:
|
||||
state.capabilities = result.get("capabilities", {})
|
||||
state.initialized = True
|
||||
# Send initialize request and wait for response via queue
|
||||
state.request_id += 1
|
||||
init_request_id = state.request_id
|
||||
|
||||
# Send initialized notification
|
||||
await self._send_notification(state, "initialized", {})
|
||||
# Create future for the response
|
||||
future: asyncio.Future = asyncio.get_event_loop().create_future()
|
||||
state.pending_requests[init_request_id] = future
|
||||
|
||||
# Send the request
|
||||
init_message = {
|
||||
"jsonrpc": "2.0",
|
||||
"id": init_request_id,
|
||||
"method": "initialize",
|
||||
"params": params,
|
||||
}
|
||||
encoded = self._encode_message(init_message)
|
||||
logger.debug(f"Sending initialize request id={init_request_id}")
|
||||
state.writer.write(encoded)
|
||||
await state.writer.drain()
|
||||
|
||||
# Wait for response (will be routed by _process_messages)
|
||||
try:
|
||||
init_result = await asyncio.wait_for(future, timeout=30.0)
|
||||
except asyncio.TimeoutError:
|
||||
state.pending_requests.pop(init_request_id, None)
|
||||
raise RuntimeError("Initialize request timed out")
|
||||
|
||||
if init_result is None:
|
||||
init_result = {}
|
||||
|
||||
# Store capabilities
|
||||
state.capabilities = init_result.get("capabilities", {})
|
||||
state.initialized = True
|
||||
logger.debug(f"Initialize response received, capabilities: {len(state.capabilities)} keys")
|
||||
|
||||
# Send initialized notification
|
||||
await self._send_notification(state, "initialized", {})
|
||||
|
||||
# Give time for server to process initialized and send any requests
|
||||
# The message processor will handle workspace/configuration automatically
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
def _encode_message(self, content: Dict[str, Any]) -> bytes:
|
||||
"""Encode a JSON-RPC message with LSP headers."""
|
||||
@@ -465,63 +479,121 @@ class StandaloneLspManager:
|
||||
except Exception as e:
|
||||
logger.error(f"Error reading message: {e}")
|
||||
return None, True
|
||||
|
||||
async def _read_responses(self, language_id: str) -> None:
|
||||
"""Background task to read responses from a language server."""
|
||||
|
||||
async def _continuous_reader(self, language_id: str) -> None:
|
||||
"""Continuously read messages from language server and put them in the queue.
|
||||
|
||||
This is the PRODUCER in the producer-consumer pattern. It starts IMMEDIATELY
|
||||
after subprocess creation and runs continuously until shutdown. This ensures
|
||||
no messages are ever lost, even during initialization handshake.
|
||||
"""
|
||||
state = self._servers.get(language_id)
|
||||
if not state:
|
||||
return
|
||||
|
||||
logger.debug(f"Continuous reader started for {language_id}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Yield to allow other tasks to run
|
||||
await asyncio.sleep(0)
|
||||
try:
|
||||
# Read headers with timeout
|
||||
content_length = 0
|
||||
while True:
|
||||
try:
|
||||
line = await asyncio.wait_for(state.reader.readline(), timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
continue # Keep waiting for data
|
||||
|
||||
message, stream_closed = await self._read_message(state.reader)
|
||||
if not line:
|
||||
logger.debug(f"Continuous reader for {language_id}: EOF")
|
||||
return
|
||||
|
||||
if stream_closed:
|
||||
logger.debug(f"Read loop for {language_id}: stream closed")
|
||||
break
|
||||
line_str = line.decode("ascii").strip()
|
||||
if not line_str:
|
||||
break # End of headers
|
||||
|
||||
if message is None:
|
||||
# Just a timeout, continue waiting
|
||||
logger.debug(f"Read loop for {language_id}: timeout, continuing...")
|
||||
continue
|
||||
if line_str.lower().startswith("content-length:"):
|
||||
content_length = int(line_str.split(":")[1].strip())
|
||||
|
||||
# Log all incoming messages for debugging
|
||||
msg_id = message.get("id", "none")
|
||||
msg_method = message.get("method", "none")
|
||||
logger.debug(f"Received message: id={msg_id}, method={msg_method}")
|
||||
if content_length == 0:
|
||||
continue
|
||||
|
||||
# Handle response (has id but no method)
|
||||
if "id" in message and "method" not in message:
|
||||
request_id = message["id"]
|
||||
logger.debug(f"Received response id={request_id}, pending={list(state.pending_requests.keys())}")
|
||||
if request_id in state.pending_requests:
|
||||
future = state.pending_requests.pop(request_id)
|
||||
# Read body
|
||||
body = await state.reader.readexactly(content_length)
|
||||
message = json.loads(body.decode("utf-8"))
|
||||
|
||||
# Put message in queue for processing
|
||||
await state.message_queue.put(message)
|
||||
|
||||
msg_id = message.get("id", "none")
|
||||
msg_method = message.get("method", "none")
|
||||
logger.debug(f"Queued message: id={msg_id}, method={msg_method}")
|
||||
|
||||
except asyncio.IncompleteReadError:
|
||||
logger.debug(f"Continuous reader for {language_id}: IncompleteReadError")
|
||||
return
|
||||
except Exception as e:
|
||||
logger.error(f"Error in continuous reader for {language_id}: {e}")
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"Continuous reader cancelled for {language_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Fatal error in continuous reader for {language_id}: {e}")
|
||||
|
||||
async def _process_messages(self, language_id: str) -> None:
|
||||
"""Process messages from the queue and route them appropriately.
|
||||
|
||||
This is the CONSUMER in the producer-consumer pattern. It handles:
|
||||
- Server requests (workspace/configuration, etc.) - responds immediately
|
||||
- Notifications (window/logMessage, etc.) - logs them
|
||||
- Responses to our requests are NOT handled here - they're consumed by _wait_for_response
|
||||
"""
|
||||
state = self._servers.get(language_id)
|
||||
if not state:
|
||||
return
|
||||
|
||||
logger.debug(f"Message processor started for {language_id}")
|
||||
|
||||
try:
|
||||
while True:
|
||||
# Get message from queue (blocks until available)
|
||||
message = await state.message_queue.get()
|
||||
|
||||
msg_id = message.get("id")
|
||||
method = message.get("method", "")
|
||||
|
||||
# Response (has id but no method) - put back for _wait_for_response to consume
|
||||
if msg_id is not None and not method:
|
||||
# This is a response to one of our requests
|
||||
if msg_id in state.pending_requests:
|
||||
future = state.pending_requests.pop(msg_id)
|
||||
if "error" in message:
|
||||
future.set_exception(
|
||||
Exception(message["error"].get("message", "Unknown error"))
|
||||
)
|
||||
else:
|
||||
future.set_result(message.get("result"))
|
||||
logger.debug(f"Response routed to pending request id={msg_id}")
|
||||
else:
|
||||
logger.debug(f"No pending request for id={request_id}")
|
||||
logger.debug(f"No pending request for response id={msg_id}")
|
||||
|
||||
# Handle server request (has both id and method) - needs response
|
||||
elif "id" in message and "method" in message:
|
||||
logger.info(f"Server request received: {message.get('method')} with id={message.get('id')}")
|
||||
# Server request (has both id and method) - needs response
|
||||
elif msg_id is not None and method:
|
||||
logger.info(f"Server request: {method} (id={msg_id})")
|
||||
await self._handle_server_request(state, message)
|
||||
|
||||
# Handle notification from server (has method but no id)
|
||||
elif "method" in message:
|
||||
# Notification (has method but no id)
|
||||
elif method:
|
||||
self._handle_server_message(language_id, message)
|
||||
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error in read loop for {language_id}: {e}")
|
||||
|
||||
state.message_queue.task_done()
|
||||
|
||||
except asyncio.CancelledError:
|
||||
logger.debug(f"Message processor cancelled for {language_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in message processor for {language_id}: {e}")
|
||||
|
||||
async def _read_stderr(self, language_id: str, stderr: asyncio.StreamReader) -> None:
|
||||
"""Background task to read stderr from a language server.
|
||||
|
||||
@@ -732,9 +804,9 @@ class StandaloneLspManager:
|
||||
}
|
||||
})
|
||||
|
||||
# Give the language server time to process the file and send any requests
|
||||
# The read loop running in background will handle workspace/configuration requests
|
||||
await asyncio.sleep(2.0)
|
||||
# Give the language server a brief moment to process the file
|
||||
# The message queue handles any server requests automatically
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# ========== Public LSP Methods ==========
|
||||
|
||||
|
||||
149
codex-lens/tests/real/debug_compare.py
Normal file
149
codex-lens/tests/real/debug_compare.py
Normal file
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
"""Compare manager read behavior vs direct read."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||
|
||||
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||
|
||||
|
||||
async def direct_test():
|
||||
"""Direct communication - this works."""
|
||||
workspace = Path(__file__).parent.parent.parent
|
||||
print("\n=== DIRECT TEST ===")
|
||||
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"pyright-langserver", "--stdio",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(workspace),
|
||||
)
|
||||
|
||||
def encode_message(content):
|
||||
body = json.dumps(content).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n"
|
||||
return header.encode("ascii") + body
|
||||
|
||||
async def send(message):
|
||||
encoded = encode_message(message)
|
||||
process.stdin.write(encoded)
|
||||
await process.stdin.drain()
|
||||
msg_desc = message.get('method') or f"response id={message.get('id')}"
|
||||
print(f" SENT: {msg_desc}")
|
||||
|
||||
async def read_one():
|
||||
content_length = 0
|
||||
while True:
|
||||
line = await asyncio.wait_for(process.stdout.readline(), timeout=3.0)
|
||||
if not line:
|
||||
return None
|
||||
line_str = line.decode("ascii").strip()
|
||||
if not line_str:
|
||||
break
|
||||
if line_str.lower().startswith("content-length:"):
|
||||
content_length = int(line_str.split(":")[1].strip())
|
||||
if content_length == 0:
|
||||
return None
|
||||
body = await process.stdout.readexactly(content_length)
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
# Initialize
|
||||
print(" Sending initialize...")
|
||||
await send({
|
||||
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||
"params": {
|
||||
"processId": None,
|
||||
"rootUri": workspace.as_uri(),
|
||||
"capabilities": {"workspace": {"configuration": True}},
|
||||
"workspaceFolders": [{"uri": workspace.as_uri(), "name": workspace.name}],
|
||||
},
|
||||
})
|
||||
|
||||
# Read until response
|
||||
while True:
|
||||
msg = await read_one()
|
||||
if msg and msg.get("id") == 1:
|
||||
print(f" Initialize response OK")
|
||||
break
|
||||
elif msg:
|
||||
print(f" Notification: {msg.get('method')}")
|
||||
|
||||
# Send initialized
|
||||
print(" Sending initialized...")
|
||||
await send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
|
||||
|
||||
# Check for workspace/configuration
|
||||
print(" Checking for workspace/configuration (3s timeout)...")
|
||||
try:
|
||||
for i in range(10):
|
||||
msg = await read_one()
|
||||
if msg:
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
print(f" RECV: {method or 'response'} (id={msg_id})")
|
||||
if method == "workspace/configuration":
|
||||
print(" SUCCESS: workspace/configuration received!")
|
||||
break
|
||||
except asyncio.TimeoutError:
|
||||
print(" TIMEOUT: No more messages")
|
||||
|
||||
process.terminate()
|
||||
await process.wait()
|
||||
|
||||
|
||||
async def manager_test():
|
||||
"""Manager communication - investigating why this doesn't work."""
|
||||
workspace = Path(__file__).parent.parent.parent
|
||||
print("\n=== MANAGER TEST ===")
|
||||
|
||||
manager = StandaloneLspManager(
|
||||
workspace_root=str(workspace),
|
||||
timeout=60.0
|
||||
)
|
||||
await manager.start()
|
||||
|
||||
# Just check if server initialized
|
||||
state = manager._servers.get("python")
|
||||
if state:
|
||||
print(f" Server initialized: {state.initialized}")
|
||||
print(f" Capabilities: {len(state.capabilities)} keys")
|
||||
else:
|
||||
# Force initialization by getting server for a Python file
|
||||
print(" Getting server for Python file...")
|
||||
test_file = workspace / "tests" / "real" / "debug_compare.py"
|
||||
state = await manager._get_server(str(test_file))
|
||||
if state:
|
||||
print(f" Server initialized: {state.initialized}")
|
||||
|
||||
# Try to read directly from state.reader
|
||||
if state:
|
||||
print("\n Direct read test from state.reader:")
|
||||
print(f" state.reader is: {type(state.reader)}")
|
||||
print(f" state.reader at_eof: {state.reader.at_eof()}")
|
||||
|
||||
# Check if there's data available
|
||||
try:
|
||||
line = await asyncio.wait_for(state.reader.readline(), timeout=1.0)
|
||||
if line:
|
||||
print(f" Got line: {line[:50]}...")
|
||||
else:
|
||||
print(f" readline returned empty (EOF)")
|
||||
except asyncio.TimeoutError:
|
||||
print(f" readline timed out (no data)")
|
||||
|
||||
await manager.stop()
|
||||
|
||||
|
||||
async def main():
|
||||
await direct_test()
|
||||
await manager_test()
|
||||
print("\n=== DONE ===")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
216
codex-lens/tests/real/debug_config.py
Normal file
216
codex-lens/tests/real/debug_config.py
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python
|
||||
"""Test if pyright sends workspace/configuration after initialized."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add source to path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent / "src"))
|
||||
|
||||
|
||||
async def read_message_direct(reader):
|
||||
"""Read a JSON-RPC message - direct blocking read, no timeout."""
|
||||
content_length = 0
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
return None
|
||||
line_str = line.decode("ascii").strip()
|
||||
if not line_str:
|
||||
break
|
||||
if line_str.lower().startswith("content-length:"):
|
||||
content_length = int(line_str.split(":")[1].strip())
|
||||
|
||||
if content_length == 0:
|
||||
return None
|
||||
|
||||
body = await reader.readexactly(content_length)
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
|
||||
async def main():
|
||||
workspace = Path(__file__).parent.parent.parent
|
||||
print(f"Workspace: {workspace}")
|
||||
|
||||
# Start pyright - exactly like in direct test
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"pyright-langserver", "--stdio",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(workspace),
|
||||
)
|
||||
|
||||
def encode_message(content):
|
||||
body = json.dumps(content).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n"
|
||||
return header.encode("ascii") + body
|
||||
|
||||
async def send(message):
|
||||
encoded = encode_message(message)
|
||||
process.stdin.write(encoded)
|
||||
await process.stdin.drain()
|
||||
method_or_resp = message.get('method') or f"response id={message.get('id')}"
|
||||
print(f"SENT: {method_or_resp} ({len(encoded)} bytes)")
|
||||
|
||||
# Start stderr reader
|
||||
async def read_stderr():
|
||||
while True:
|
||||
line = await process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
print(f"[stderr] {line.decode('utf-8', errors='replace').rstrip()}")
|
||||
asyncio.create_task(read_stderr())
|
||||
|
||||
print("\n=== INITIALIZE ===")
|
||||
await send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"processId": None,
|
||||
"rootUri": workspace.as_uri(),
|
||||
"rootPath": str(workspace),
|
||||
"capabilities": {
|
||||
"workspace": {"configuration": True},
|
||||
},
|
||||
"workspaceFolders": [{"uri": workspace.as_uri(), "name": workspace.name}],
|
||||
},
|
||||
})
|
||||
|
||||
# Read until we get initialize response
|
||||
print("Reading initialize response...")
|
||||
while True:
|
||||
msg = await asyncio.wait_for(read_message_direct(process.stdout), timeout=10)
|
||||
if msg is None:
|
||||
break
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
if method:
|
||||
print(f"RECV: {method} (notification)")
|
||||
else:
|
||||
print(f"RECV: response id={msg_id}")
|
||||
if msg_id == 1:
|
||||
print("Initialize OK!")
|
||||
break
|
||||
|
||||
print("\n=== SEND INITIALIZED ===")
|
||||
await send({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialized",
|
||||
"params": {},
|
||||
})
|
||||
|
||||
# Now, here's the key test - will we receive workspace/configuration?
|
||||
print("\n=== WAIT FOR workspace/configuration ===")
|
||||
print("Reading with 5 second timeout...")
|
||||
|
||||
try:
|
||||
for i in range(10):
|
||||
msg = await asyncio.wait_for(read_message_direct(process.stdout), timeout=2)
|
||||
if msg is None:
|
||||
print("EOF")
|
||||
break
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
print(f"RECV: method={method}, id={msg_id}")
|
||||
|
||||
# Respond to server requests
|
||||
if msg_id is not None and method:
|
||||
if method == "workspace/configuration":
|
||||
print(" -> Got workspace/configuration! Responding...")
|
||||
await send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": [{} for _ in msg.get("params", {}).get("items", [])],
|
||||
})
|
||||
else:
|
||||
print(f" -> Responding to {method}")
|
||||
await send({"jsonrpc": "2.0", "id": msg_id, "result": None})
|
||||
except asyncio.TimeoutError:
|
||||
print("No more messages (timeout)")
|
||||
|
||||
print("\n=== Now start background read task like manager does ===")
|
||||
|
||||
# Store references like manager does
|
||||
reader = process.stdout # This is how manager does it
|
||||
writer = process.stdin
|
||||
|
||||
# Start background read task
|
||||
async def bg_read_loop():
|
||||
print("[BG] Read loop started")
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(0)
|
||||
try:
|
||||
msg = await asyncio.wait_for(read_message_direct(reader), timeout=1.0)
|
||||
if msg is None:
|
||||
print("[BG] Stream closed")
|
||||
break
|
||||
bg_method = msg.get('method') or f"response id={msg.get('id')}"
|
||||
print(f"[BG] RECV: {bg_method}")
|
||||
|
||||
# Handle server requests
|
||||
method = msg.get("method")
|
||||
msg_id = msg.get("id")
|
||||
if msg_id is not None and method:
|
||||
print(f"[BG] Responding to {method}")
|
||||
await send({"jsonrpc": "2.0", "id": msg_id, "result": None})
|
||||
except asyncio.TimeoutError:
|
||||
print("[BG] timeout")
|
||||
except asyncio.CancelledError:
|
||||
print("[BG] Cancelled")
|
||||
|
||||
read_task = asyncio.create_task(bg_read_loop())
|
||||
|
||||
# Wait a moment
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# Now send didOpen and documentSymbol like manager does
|
||||
print("\n=== SEND didOpen ===")
|
||||
test_file = workspace / "tests" / "real" / "debug_config.py"
|
||||
await send({
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didOpen",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": test_file.as_uri(),
|
||||
"languageId": "python",
|
||||
"version": 1,
|
||||
"text": test_file.read_text(),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
# Wait for processing
|
||||
await asyncio.sleep(2)
|
||||
|
||||
print("\n=== SEND documentSymbol ===")
|
||||
await send({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "textDocument/documentSymbol",
|
||||
"params": {"textDocument": {"uri": test_file.as_uri()}},
|
||||
})
|
||||
|
||||
# Wait for response
|
||||
print("Waiting for documentSymbol response (max 30s)...")
|
||||
deadline = asyncio.get_event_loop().time() + 30
|
||||
while asyncio.get_event_loop().time() < deadline:
|
||||
await asyncio.sleep(0.5)
|
||||
# The background task will print when it receives the response
|
||||
|
||||
read_task.cancel()
|
||||
try:
|
||||
await read_task
|
||||
except asyncio.CancelledError:
|
||||
pass
|
||||
|
||||
process.terminate()
|
||||
print("\nDone!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
320
codex-lens/tests/real/debug_direct.py
Normal file
320
codex-lens/tests/real/debug_direct.py
Normal file
@@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python
|
||||
"""Minimal direct test of pyright LSP communication."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
async def send_message(writer, message):
|
||||
"""Send a JSON-RPC message."""
|
||||
body = json.dumps(message).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n".encode("ascii")
|
||||
writer.write(header + body)
|
||||
await writer.drain()
|
||||
print(f"SENT: {message.get('method', 'response')} (id={message.get('id', 'N/A')})")
|
||||
|
||||
|
||||
async def read_message(reader):
|
||||
"""Read a JSON-RPC message."""
|
||||
# Read headers
|
||||
content_length = 0
|
||||
while True:
|
||||
line = await reader.readline()
|
||||
if not line:
|
||||
return None
|
||||
line_str = line.decode("ascii").strip()
|
||||
if not line_str:
|
||||
break
|
||||
if line_str.lower().startswith("content-length:"):
|
||||
content_length = int(line_str.split(":")[1].strip())
|
||||
|
||||
if content_length == 0:
|
||||
return None
|
||||
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
|
||||
async def main():
|
||||
workspace = Path(__file__).parent.parent.parent
|
||||
test_file = workspace / "tests" / "real" / "debug_direct.py"
|
||||
|
||||
print(f"Workspace: {workspace}")
|
||||
print(f"Test file: {test_file}")
|
||||
print()
|
||||
|
||||
# Start pyright
|
||||
print("Starting pyright-langserver...")
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"pyright-langserver", "--stdio",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(workspace),
|
||||
)
|
||||
|
||||
# Start stderr reader
|
||||
async def read_stderr():
|
||||
while True:
|
||||
line = await process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
print(f"[stderr] {line.decode('utf-8', errors='replace').rstrip()}")
|
||||
|
||||
stderr_task = asyncio.create_task(read_stderr())
|
||||
|
||||
try:
|
||||
# 1. Send initialize
|
||||
print("\n=== INITIALIZE ===")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"processId": None,
|
||||
"rootUri": workspace.as_uri(),
|
||||
"rootPath": str(workspace),
|
||||
"capabilities": {
|
||||
"textDocument": {
|
||||
"documentSymbol": {
|
||||
"hierarchicalDocumentSymbolSupport": True,
|
||||
},
|
||||
},
|
||||
"workspace": {
|
||||
"configuration": True,
|
||||
},
|
||||
},
|
||||
"workspaceFolders": [{"uri": workspace.as_uri(), "name": workspace.name}],
|
||||
},
|
||||
})
|
||||
|
||||
# Read all messages until we get initialize response
|
||||
print("\n=== READING RESPONSES ===")
|
||||
init_done = False
|
||||
for i in range(20):
|
||||
try:
|
||||
msg = await asyncio.wait_for(read_message(process.stdout), timeout=5.0)
|
||||
if msg is None:
|
||||
print("EOF")
|
||||
break
|
||||
|
||||
method = msg.get("method", "")
|
||||
msg_id = msg.get("id", "N/A")
|
||||
|
||||
if method:
|
||||
print(f"RECV: {method} (id={msg_id})")
|
||||
|
||||
# Handle server requests
|
||||
if msg_id != "N/A":
|
||||
if method == "workspace/configuration":
|
||||
print(" -> Responding to workspace/configuration")
|
||||
items = msg.get("params", {}).get("items", [])
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": [{"pythonPath": "python"} for _ in items],
|
||||
})
|
||||
elif method == "client/registerCapability":
|
||||
print(" -> Responding to client/registerCapability")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
elif method == "window/workDoneProgress/create":
|
||||
print(" -> Responding to window/workDoneProgress/create")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
else:
|
||||
print(f"RECV: response (id={msg_id})")
|
||||
if msg_id == 1:
|
||||
print(" -> Initialize response received!")
|
||||
caps = list(msg.get("result", {}).get("capabilities", {}).keys())
|
||||
print(f" -> Capabilities: {caps[:5]}...")
|
||||
init_done = True
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f" Timeout waiting for message {i+1}")
|
||||
break
|
||||
|
||||
if not init_done:
|
||||
print("ERROR: Initialize failed")
|
||||
return
|
||||
|
||||
# 2. Send initialized notification
|
||||
print("\n=== INITIALIZED ===")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "initialized",
|
||||
"params": {},
|
||||
})
|
||||
|
||||
# Read any messages pyright sends after initialized
|
||||
print("\n=== READING POST-INITIALIZED MESSAGES ===")
|
||||
for i in range(10):
|
||||
try:
|
||||
msg = await asyncio.wait_for(read_message(process.stdout), timeout=2.0)
|
||||
if msg is None:
|
||||
break
|
||||
|
||||
method = msg.get("method", "")
|
||||
msg_id = msg.get("id", "N/A")
|
||||
|
||||
print(f"RECV: {method or 'response'} (id={msg_id})")
|
||||
|
||||
# Handle server requests
|
||||
if msg_id != "N/A" and method:
|
||||
if method == "workspace/configuration":
|
||||
print(" -> Responding to workspace/configuration")
|
||||
items = msg.get("params", {}).get("items", [])
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": [{"pythonPath": "python"} for _ in items],
|
||||
})
|
||||
elif method == "client/registerCapability":
|
||||
print(" -> Responding to client/registerCapability")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
elif method == "window/workDoneProgress/create":
|
||||
print(" -> Responding to window/workDoneProgress/create")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f" No more messages (timeout)")
|
||||
break
|
||||
|
||||
# 3. Send didOpen
|
||||
print("\n=== DIDOPEN ===")
|
||||
content = test_file.read_text(encoding="utf-8")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "textDocument/didOpen",
|
||||
"params": {
|
||||
"textDocument": {
|
||||
"uri": test_file.as_uri(),
|
||||
"languageId": "python",
|
||||
"version": 1,
|
||||
"text": content,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
# Read any messages
|
||||
print("\n=== READING POST-DIDOPEN MESSAGES ===")
|
||||
for i in range(10):
|
||||
try:
|
||||
msg = await asyncio.wait_for(read_message(process.stdout), timeout=2.0)
|
||||
if msg is None:
|
||||
break
|
||||
|
||||
method = msg.get("method", "")
|
||||
msg_id = msg.get("id", "N/A")
|
||||
|
||||
print(f"RECV: {method or 'response'} (id={msg_id})")
|
||||
|
||||
# Handle server requests
|
||||
if msg_id != "N/A" and method:
|
||||
if method == "workspace/configuration":
|
||||
print(" -> Responding to workspace/configuration")
|
||||
items = msg.get("params", {}).get("items", [])
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": [{"pythonPath": "python"} for _ in items],
|
||||
})
|
||||
else:
|
||||
print(f" -> Responding with null to {method}")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f" No more messages (timeout)")
|
||||
break
|
||||
|
||||
# 4. Send documentSymbol request
|
||||
print("\n=== DOCUMENTSYMBOL ===")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"method": "textDocument/documentSymbol",
|
||||
"params": {
|
||||
"textDocument": {"uri": test_file.as_uri()},
|
||||
},
|
||||
})
|
||||
|
||||
# Wait for response
|
||||
print("\n=== READING DOCUMENTSYMBOL RESPONSE ===")
|
||||
for i in range(20):
|
||||
try:
|
||||
msg = await asyncio.wait_for(read_message(process.stdout), timeout=5.0)
|
||||
if msg is None:
|
||||
break
|
||||
|
||||
method = msg.get("method", "")
|
||||
msg_id = msg.get("id", "N/A")
|
||||
|
||||
if method:
|
||||
print(f"RECV: {method} (id={msg_id})")
|
||||
|
||||
# Handle server requests
|
||||
if msg_id != "N/A":
|
||||
if method == "workspace/configuration":
|
||||
print(" -> Responding to workspace/configuration")
|
||||
items = msg.get("params", {}).get("items", [])
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": [{"pythonPath": "python"} for _ in items],
|
||||
})
|
||||
else:
|
||||
print(f" -> Responding with null to {method}")
|
||||
await send_message(process.stdin, {
|
||||
"jsonrpc": "2.0",
|
||||
"id": msg_id,
|
||||
"result": None,
|
||||
})
|
||||
else:
|
||||
print(f"RECV: response (id={msg_id})")
|
||||
if msg_id == 2:
|
||||
result = msg.get("result", [])
|
||||
print(f" -> DocumentSymbol response: {len(result)} symbols")
|
||||
for sym in result[:5]:
|
||||
print(f" - {sym.get('name')} ({sym.get('kind')})")
|
||||
break
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
print(f" Timeout {i+1}")
|
||||
if i >= 5:
|
||||
break
|
||||
|
||||
print("\n=== DONE ===")
|
||||
|
||||
finally:
|
||||
stderr_task.cancel()
|
||||
process.terminate()
|
||||
try:
|
||||
await asyncio.wait_for(process.wait(), timeout=5.0)
|
||||
except asyncio.TimeoutError:
|
||||
process.kill()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
123
codex-lens/tests/real/debug_reads.py
Normal file
123
codex-lens/tests/real/debug_reads.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python
|
||||
"""Debug exactly what's happening with reads after initialized."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
async def main():
|
||||
workspace = Path(__file__).parent.parent.parent
|
||||
print(f"Workspace: {workspace}")
|
||||
|
||||
# Start pyright
|
||||
process = await asyncio.create_subprocess_exec(
|
||||
"pyright-langserver", "--stdio",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
cwd=str(workspace),
|
||||
)
|
||||
|
||||
# Helper to encode message
|
||||
def encode(content):
|
||||
body = json.dumps(content).encode("utf-8")
|
||||
header = f"Content-Length: {len(body)}\r\n\r\n"
|
||||
return header.encode("ascii") + body
|
||||
|
||||
# Helper to send
|
||||
async def send(msg):
|
||||
encoded = encode(msg)
|
||||
process.stdin.write(encoded)
|
||||
await process.stdin.drain()
|
||||
method = msg.get("method") or f"response-{msg.get('id')}"
|
||||
print(f"SENT: {method}")
|
||||
|
||||
# Helper to read one message
|
||||
async def read_one(timeout=3.0):
|
||||
content_length = 0
|
||||
while True:
|
||||
try:
|
||||
print(f" readline(timeout={timeout})...")
|
||||
line = await asyncio.wait_for(process.stdout.readline(), timeout=timeout)
|
||||
print(f" got line: {repr(line[:50] if len(line) > 50 else line)}")
|
||||
except asyncio.TimeoutError:
|
||||
print(f" TIMEOUT on readline")
|
||||
return None
|
||||
|
||||
if not line:
|
||||
print(f" EOF")
|
||||
return None
|
||||
|
||||
line_str = line.decode("ascii").strip()
|
||||
if not line_str:
|
||||
break # End of headers
|
||||
|
||||
if line_str.lower().startswith("content-length:"):
|
||||
content_length = int(line_str.split(":")[1].strip())
|
||||
|
||||
if content_length == 0:
|
||||
return None
|
||||
|
||||
body = await process.stdout.readexactly(content_length)
|
||||
return json.loads(body.decode("utf-8"))
|
||||
|
||||
# Start stderr reader
|
||||
async def read_stderr():
|
||||
while True:
|
||||
line = await process.stderr.readline()
|
||||
if not line:
|
||||
break
|
||||
print(f"[stderr] {line.decode('utf-8', errors='replace').rstrip()}")
|
||||
asyncio.create_task(read_stderr())
|
||||
|
||||
print("\n=== INITIALIZE ===")
|
||||
await send({
|
||||
"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
||||
"params": {
|
||||
"processId": None,
|
||||
"rootUri": workspace.as_uri(),
|
||||
"capabilities": {"workspace": {"configuration": True}},
|
||||
"workspaceFolders": [{"uri": workspace.as_uri(), "name": workspace.name}],
|
||||
},
|
||||
})
|
||||
|
||||
# Read until initialize response
|
||||
print("\n=== READING UNTIL INITIALIZE RESPONSE ===")
|
||||
while True:
|
||||
msg = await read_one()
|
||||
if msg and msg.get("id") == 1 and "method" not in msg:
|
||||
print(f"Got initialize response")
|
||||
break
|
||||
elif msg:
|
||||
print(f"Got notification: {msg.get('method')}")
|
||||
|
||||
print("\n=== SEND INITIALIZED ===")
|
||||
await send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
|
||||
|
||||
print("\n=== NOW TRY TO READ WORKSPACE/CONFIGURATION ===")
|
||||
print("Attempting reads with 2s timeout each...")
|
||||
|
||||
for i in range(3):
|
||||
print(f"\n--- Read attempt {i+1} ---")
|
||||
msg = await read_one(timeout=2.0)
|
||||
if msg:
|
||||
method = msg.get("method", "")
|
||||
msg_id = msg.get("id")
|
||||
print(f"SUCCESS: method={method}, id={msg_id}")
|
||||
if method and msg_id is not None:
|
||||
# Respond to server request
|
||||
print(f"Responding to {method}")
|
||||
await send({"jsonrpc": "2.0", "id": msg_id, "result": [{}]})
|
||||
else:
|
||||
print(f"No message (timeout or EOF)")
|
||||
break
|
||||
|
||||
print("\n=== CLEANUP ===")
|
||||
process.terminate()
|
||||
await process.wait()
|
||||
print("Done")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
Reference in New Issue
Block a user