mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-12 02:37:45 +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();
|
const { executions } = await response.json();
|
||||||
if (!executions || executions.length === 0) return;
|
if (!executions || executions.length === 0) return;
|
||||||
|
|
||||||
executions.forEach(exec => {
|
let needsUiUpdate = false;
|
||||||
// Skip if already tracked (avoid overwriting live data)
|
|
||||||
if (cliStreamExecutions[exec.id]) return;
|
|
||||||
|
|
||||||
// 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] = {
|
cliStreamExecutions[exec.id] = {
|
||||||
tool: exec.tool || 'cli',
|
tool: exec.tool || 'cli',
|
||||||
mode: exec.mode || 'analysis',
|
mode: exec.mode || 'analysis',
|
||||||
@@ -55,24 +100,12 @@ async function syncActiveExecutions() {
|
|||||||
timestamp: exec.startTime
|
timestamp: exec.startTime
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fill historical output (limit to last MAX_OUTPUT_LINES)
|
// Add historical output
|
||||||
if (exec.output) {
|
cliStreamExecutions[exec.id].output.push(...historicalLines);
|
||||||
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()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update UI if we recovered any executions
|
// Update UI if we recovered or merged any executions
|
||||||
if (executions.length > 0) {
|
if (needsUiUpdate) {
|
||||||
// Set active tab to first running execution
|
// Set active tab to first running execution
|
||||||
const runningExec = executions.find(e => e.status === 'running');
|
const runningExec = executions.find(e => e.status === 'running');
|
||||||
if (runningExec && !activeStreamTab) {
|
if (runningExec && !activeStreamTab) {
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ export class CliHistoryStore {
|
|||||||
this.db = new Database(this.dbPath);
|
this.db = new Database(this.dbPath);
|
||||||
this.db.pragma('journal_mode = WAL');
|
this.db.pragma('journal_mode = WAL');
|
||||||
this.db.pragma('synchronous = NORMAL');
|
this.db.pragma('synchronous = NORMAL');
|
||||||
|
this.db.pragma('busy_timeout = 5000'); // Wait up to 5 seconds for locks
|
||||||
|
|
||||||
this.initSchema();
|
this.initSchema();
|
||||||
this.migrateFromJson(historyDir);
|
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
|
* 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 } {
|
deleteConversation(id: string): { success: boolean; error?: string } {
|
||||||
try {
|
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 };
|
return { success: result.changes > 0 };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { success: false, error: (err as Error).message };
|
return { success: false, error: (err as Error).message };
|
||||||
@@ -821,7 +859,7 @@ export class CliHistoryStore {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
transaction();
|
this.withRetry(() => transaction());
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -896,14 +934,14 @@ export class CliHistoryStore {
|
|||||||
project_hash = @project_hash
|
project_hash = @project_hash
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run({
|
this.withRetry(() => stmt.run({
|
||||||
ccw_id: mapping.ccw_id,
|
ccw_id: mapping.ccw_id,
|
||||||
tool: mapping.tool,
|
tool: mapping.tool,
|
||||||
native_session_id: mapping.native_session_id,
|
native_session_id: mapping.native_session_id,
|
||||||
native_session_path: mapping.native_session_path || null,
|
native_session_path: mapping.native_session_path || null,
|
||||||
project_hash: mapping.project_hash || null,
|
project_hash: mapping.project_hash || null,
|
||||||
created_at: mapping.created_at || new Date().toISOString()
|
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)
|
VALUES (@id, @created_at, @tool, @prompt_count, @patterns, @suggestions, @raw_output, @execution_id, @lang)
|
||||||
`);
|
`);
|
||||||
|
|
||||||
stmt.run({
|
this.withRetry(() => stmt.run({
|
||||||
id: insight.id,
|
id: insight.id,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
tool: insight.tool,
|
tool: insight.tool,
|
||||||
@@ -1157,7 +1195,7 @@ export class CliHistoryStore {
|
|||||||
raw_output: insight.rawOutput || null,
|
raw_output: insight.rawOutput || null,
|
||||||
execution_id: insight.executionId || null,
|
execution_id: insight.executionId || null,
|
||||||
lang: insight.lang || 'en'
|
lang: insight.lang || 'en'
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1249,7 +1287,7 @@ export class CliHistoryStore {
|
|||||||
updated_at = @updated_at
|
updated_at = @updated_at
|
||||||
`);
|
`);
|
||||||
|
|
||||||
const result = stmt.run({
|
const result = this.withRetry(() => stmt.run({
|
||||||
execution_id: review.execution_id,
|
execution_id: review.execution_id,
|
||||||
status: review.status,
|
status: review.status,
|
||||||
rating: review.rating ?? null,
|
rating: review.rating ?? null,
|
||||||
@@ -1257,7 +1295,7 @@ export class CliHistoryStore {
|
|||||||
reviewer: review.reviewer ?? null,
|
reviewer: review.reviewer ?? null,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at
|
updated_at
|
||||||
});
|
}));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: result.lastInsertRowid as number,
|
id: result.lastInsertRowid as number,
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ class ServerState:
|
|||||||
capabilities: Dict[str, Any] = field(default_factory=dict)
|
capabilities: Dict[str, Any] = field(default_factory=dict)
|
||||||
pending_requests: Dict[int, asyncio.Future] = field(default_factory=dict)
|
pending_requests: Dict[int, asyncio.Future] = field(default_factory=dict)
|
||||||
restart_count: int = 0
|
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:
|
class StandaloneLspManager:
|
||||||
@@ -253,18 +255,22 @@ class StandaloneLspManager:
|
|||||||
|
|
||||||
self._servers[language_id] = state
|
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)
|
# Start reading stderr in background (prevents pipe buffer from filling up)
|
||||||
if process.stderr:
|
if process.stderr:
|
||||||
self._stderr_tasks[language_id] = asyncio.create_task(
|
self._stderr_tasks[language_id] = asyncio.create_task(
|
||||||
self._read_stderr(language_id, process.stderr)
|
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)
|
await self._initialize_server(state)
|
||||||
|
|
||||||
logger.info(f"{config.display_name} started and initialized")
|
logger.info(f"{config.display_name} started and initialized")
|
||||||
@@ -353,49 +359,25 @@ class StandaloneLspManager:
|
|||||||
return await self._start_server(language_id)
|
return await self._start_server(language_id)
|
||||||
|
|
||||||
async def _initialize_server(self, state: ServerState) -> None:
|
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()
|
root_uri = self.workspace_root.as_uri()
|
||||||
|
|
||||||
|
# Simplified params matching direct test that works
|
||||||
params = {
|
params = {
|
||||||
"processId": os.getpid(),
|
"processId": None, # Use None like direct test
|
||||||
"rootUri": root_uri,
|
"rootUri": root_uri,
|
||||||
"rootPath": str(self.workspace_root),
|
"rootPath": str(self.workspace_root),
|
||||||
"capabilities": {
|
"capabilities": {
|
||||||
"textDocument": {
|
"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": {
|
"documentSymbol": {
|
||||||
"dynamicRegistration": False,
|
|
||||||
"hierarchicalDocumentSymbolSupport": True,
|
"hierarchicalDocumentSymbolSupport": True,
|
||||||
},
|
},
|
||||||
"callHierarchy": {
|
|
||||||
"dynamicRegistration": False,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"workspace": {
|
"workspace": {
|
||||||
"workspaceFolders": True,
|
|
||||||
"configuration": True,
|
"configuration": True,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -405,17 +387,49 @@ class StandaloneLspManager:
|
|||||||
"name": self.workspace_root.name,
|
"name": self.workspace_root.name,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"initializationOptions": state.config.initialization_options,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result = await self._send_request(state, "initialize", params)
|
# Send initialize request and wait for response via queue
|
||||||
|
state.request_id += 1
|
||||||
|
init_request_id = state.request_id
|
||||||
|
|
||||||
if result:
|
# Create future for the response
|
||||||
state.capabilities = result.get("capabilities", {})
|
future: asyncio.Future = asyncio.get_event_loop().create_future()
|
||||||
state.initialized = True
|
state.pending_requests[init_request_id] = future
|
||||||
|
|
||||||
# Send initialized notification
|
# Send the request
|
||||||
await self._send_notification(state, "initialized", {})
|
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:
|
def _encode_message(self, content: Dict[str, Any]) -> bytes:
|
||||||
"""Encode a JSON-RPC message with LSP headers."""
|
"""Encode a JSON-RPC message with LSP headers."""
|
||||||
@@ -466,61 +480,119 @@ class StandaloneLspManager:
|
|||||||
logger.error(f"Error reading message: {e}")
|
logger.error(f"Error reading message: {e}")
|
||||||
return None, True
|
return None, True
|
||||||
|
|
||||||
async def _read_responses(self, language_id: str) -> None:
|
async def _continuous_reader(self, language_id: str) -> None:
|
||||||
"""Background task to read responses from a language server."""
|
"""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)
|
state = self._servers.get(language_id)
|
||||||
if not state:
|
if not state:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
logger.debug(f"Continuous reader started for {language_id}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
# Yield to allow other tasks to run
|
try:
|
||||||
await asyncio.sleep(0)
|
# 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:
|
line_str = line.decode("ascii").strip()
|
||||||
logger.debug(f"Read loop for {language_id}: stream closed")
|
if not line_str:
|
||||||
break
|
break # End of headers
|
||||||
|
|
||||||
if message is None:
|
if line_str.lower().startswith("content-length:"):
|
||||||
# Just a timeout, continue waiting
|
content_length = int(line_str.split(":")[1].strip())
|
||||||
logger.debug(f"Read loop for {language_id}: timeout, continuing...")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Log all incoming messages for debugging
|
if content_length == 0:
|
||||||
msg_id = message.get("id", "none")
|
continue
|
||||||
msg_method = message.get("method", "none")
|
|
||||||
logger.debug(f"Received message: id={msg_id}, method={msg_method}")
|
|
||||||
|
|
||||||
# Handle response (has id but no method)
|
# Read body
|
||||||
if "id" in message and "method" not in message:
|
body = await state.reader.readexactly(content_length)
|
||||||
request_id = message["id"]
|
message = json.loads(body.decode("utf-8"))
|
||||||
logger.debug(f"Received response id={request_id}, pending={list(state.pending_requests.keys())}")
|
|
||||||
if request_id in state.pending_requests:
|
# Put message in queue for processing
|
||||||
future = state.pending_requests.pop(request_id)
|
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:
|
if "error" in message:
|
||||||
future.set_exception(
|
future.set_exception(
|
||||||
Exception(message["error"].get("message", "Unknown error"))
|
Exception(message["error"].get("message", "Unknown error"))
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
future.set_result(message.get("result"))
|
future.set_result(message.get("result"))
|
||||||
|
logger.debug(f"Response routed to pending request id={msg_id}")
|
||||||
else:
|
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
|
# Server request (has both id and method) - needs response
|
||||||
elif "id" in message and "method" in message:
|
elif msg_id is not None and method:
|
||||||
logger.info(f"Server request received: {message.get('method')} with id={message.get('id')}")
|
logger.info(f"Server request: {method} (id={msg_id})")
|
||||||
await self._handle_server_request(state, message)
|
await self._handle_server_request(state, message)
|
||||||
|
|
||||||
# Handle notification from server (has method but no id)
|
# Notification (has method but no id)
|
||||||
elif "method" in message:
|
elif method:
|
||||||
self._handle_server_message(language_id, message)
|
self._handle_server_message(language_id, message)
|
||||||
|
|
||||||
|
state.message_queue.task_done()
|
||||||
|
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
logger.debug(f"Message processor cancelled for {language_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error in read loop for {language_id}: {e}")
|
logger.error(f"Error in message processor for {language_id}: {e}")
|
||||||
|
|
||||||
async def _read_stderr(self, language_id: str, stderr: asyncio.StreamReader) -> None:
|
async def _read_stderr(self, language_id: str, stderr: asyncio.StreamReader) -> None:
|
||||||
"""Background task to read stderr from a language server.
|
"""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
|
# Give the language server a brief moment to process the file
|
||||||
# The read loop running in background will handle workspace/configuration requests
|
# The message queue handles any server requests automatically
|
||||||
await asyncio.sleep(2.0)
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# ========== Public LSP Methods ==========
|
# ========== 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