2025-09-30 13:52:36 +08:00
|
|
|
|
#!/usr/bin/env python
|
|
|
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
LSP WebSocket API路由
|
|
|
|
|
|
为脚本编辑器提供基于WebSocket的Language Server Protocol支持
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import asyncio
|
|
|
|
|
|
from typing import Dict, Any, Optional
|
|
|
|
|
|
from fastapi import WebSocket, WebSocketDisconnect, APIRouter
|
|
|
|
|
|
from utils.logger import get_logger
|
|
|
|
|
|
from services.online_script.lsp_completion_service import get_lsp_completion_service
|
|
|
|
|
|
|
|
|
|
|
|
logger = get_logger("routes.lsp_websocket")
|
|
|
|
|
|
router = APIRouter(prefix="/lsp", tags=["LSP WebSocket"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class LSPConnectionManager:
|
|
|
|
|
|
"""LSP WebSocket连接管理器"""
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
|
|
|
|
self.active_connections: Dict[str, Dict[str, Any]] = {}
|
|
|
|
|
|
self.lsp_service = get_lsp_completion_service()
|
|
|
|
|
|
|
|
|
|
|
|
async def connect(self, websocket: WebSocket, client_id: str):
|
|
|
|
|
|
"""建立WebSocket连接"""
|
|
|
|
|
|
await websocket.accept()
|
|
|
|
|
|
self.active_connections[client_id] = {
|
|
|
|
|
|
'websocket': websocket,
|
|
|
|
|
|
'documents': {}, # 存储文档状态
|
|
|
|
|
|
'initialized': False
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.info(f"LSP客户端连接: {client_id}")
|
|
|
|
|
|
|
|
|
|
|
|
def disconnect(self, client_id: str):
|
|
|
|
|
|
"""断开WebSocket连接"""
|
|
|
|
|
|
if client_id in self.active_connections:
|
|
|
|
|
|
del self.active_connections[client_id]
|
|
|
|
|
|
logger.info(f"LSP客户端断开: {client_id}")
|
|
|
|
|
|
|
|
|
|
|
|
async def send_message(self, client_id: str, message: Dict[str, Any]):
|
|
|
|
|
|
"""发送消息给指定客户端"""
|
|
|
|
|
|
if client_id in self.active_connections:
|
|
|
|
|
|
websocket = self.active_connections[client_id]['websocket']
|
|
|
|
|
|
try:
|
|
|
|
|
|
await websocket.send_text(json.dumps(message, ensure_ascii=False))
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"发送LSP消息失败 {client_id}: {e}")
|
|
|
|
|
|
self.disconnect(client_id)
|
|
|
|
|
|
|
|
|
|
|
|
async def handle_message(self, client_id: str, message: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
|
|
|
|
"""处理LSP消息"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
method = message.get('method')
|
|
|
|
|
|
params = message.get('params', {})
|
|
|
|
|
|
|
|
|
|
|
|
# 更新文档状态
|
|
|
|
|
|
if method == 'textDocument/didOpen':
|
|
|
|
|
|
await self._handle_did_open(client_id, params)
|
2025-10-14 15:34:44 +08:00
|
|
|
|
logger.info(f"客户端 {client_id} 打开文档")
|
2025-09-30 13:52:36 +08:00
|
|
|
|
elif method == 'textDocument/didChange':
|
|
|
|
|
|
await self._handle_did_change(client_id, params)
|
|
|
|
|
|
elif method == 'textDocument/didClose':
|
|
|
|
|
|
await self._handle_did_close(client_id, params)
|
2025-10-14 15:34:44 +08:00
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
# 为需要文档内容的请求添加文档文本
|
|
|
|
|
|
if method in ['textDocument/completion', 'textDocument/signatureHelp', 'textDocument/hover']:
|
|
|
|
|
|
text_document = params.get('textDocument', {})
|
|
|
|
|
|
uri = text_document.get('uri')
|
2025-10-14 15:34:44 +08:00
|
|
|
|
|
|
|
|
|
|
logger.debug(f"处理 {method} 请求, URI: {uri}, 客户端: {client_id}")
|
|
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
if uri and client_id in self.active_connections:
|
|
|
|
|
|
documents = self.active_connections[client_id]['documents']
|
2025-10-14 15:34:44 +08:00
|
|
|
|
logger.debug(f"当前文档列表: {list(documents.keys())}")
|
|
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
if uri in documents:
|
|
|
|
|
|
# 将文档内容添加到context中
|
|
|
|
|
|
if 'context' not in params:
|
|
|
|
|
|
params['context'] = {}
|
|
|
|
|
|
params['context']['documentText'] = documents[uri]['text']
|
|
|
|
|
|
message['params'] = params
|
2025-10-14 15:34:44 +08:00
|
|
|
|
logger.debug(f"已添加文档内容,长度: {len(documents[uri]['text'])}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"未找到文档 {uri},当前已打开文档: {list(documents.keys())}")
|
|
|
|
|
|
# 提供空文档内容,避免补全完全失败
|
|
|
|
|
|
if 'context' not in params:
|
|
|
|
|
|
params['context'] = {}
|
|
|
|
|
|
params['context']['documentText'] = ''
|
|
|
|
|
|
message['params'] = params
|
|
|
|
|
|
else:
|
|
|
|
|
|
logger.warning(f"客户端 {client_id} 不存在或URI为空")
|
2025-09-30 13:52:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 使用LSP服务处理消息
|
|
|
|
|
|
response = self.lsp_service.handle_lsp_message(message)
|
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"处理LSP消息失败 {client_id}: {e}", exc_info=True)
|
|
|
|
|
|
return {
|
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
|
"id": message.get('id'),
|
|
|
|
|
|
"error": {
|
|
|
|
|
|
"code": -32603,
|
|
|
|
|
|
"message": f"Internal error: {str(e)}"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async def _handle_did_open(self, client_id: str, params: Dict[str, Any]):
|
|
|
|
|
|
"""处理文档打开"""
|
|
|
|
|
|
text_document = params.get('textDocument', {})
|
|
|
|
|
|
uri = text_document.get('uri')
|
|
|
|
|
|
text = text_document.get('text', '')
|
|
|
|
|
|
version = text_document.get('version', 1)
|
|
|
|
|
|
|
|
|
|
|
|
if uri and client_id in self.active_connections:
|
|
|
|
|
|
self.active_connections[client_id]['documents'][uri] = {
|
|
|
|
|
|
'text': text,
|
|
|
|
|
|
'version': version
|
|
|
|
|
|
}
|
|
|
|
|
|
logger.debug(f"文档已打开: {uri}")
|
|
|
|
|
|
|
|
|
|
|
|
async def _handle_did_change(self, client_id: str, params: Dict[str, Any]):
|
|
|
|
|
|
"""处理文档变更"""
|
|
|
|
|
|
text_document = params.get('textDocument', {})
|
|
|
|
|
|
uri = text_document.get('uri')
|
|
|
|
|
|
version = text_document.get('version')
|
|
|
|
|
|
content_changes = params.get('contentChanges', [])
|
2025-10-14 15:34:44 +08:00
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
if uri and client_id in self.active_connections:
|
|
|
|
|
|
documents = self.active_connections[client_id]['documents']
|
2025-10-14 15:34:44 +08:00
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
if uri in documents:
|
|
|
|
|
|
# 应用文档变更
|
|
|
|
|
|
for change in content_changes:
|
|
|
|
|
|
if 'range' not in change:
|
|
|
|
|
|
# 全文更新
|
|
|
|
|
|
documents[uri]['text'] = change.get('text', '')
|
2025-10-14 15:34:44 +08:00
|
|
|
|
logger.debug(f"全文更新: {uri}")
|
2025-09-30 13:52:36 +08:00
|
|
|
|
else:
|
2025-10-14 15:34:44 +08:00
|
|
|
|
# 增量更新 - 正确实现
|
|
|
|
|
|
documents[uri]['text'] = self._apply_text_edit(
|
|
|
|
|
|
documents[uri]['text'],
|
|
|
|
|
|
change.get('range'),
|
|
|
|
|
|
change.get('text', '')
|
|
|
|
|
|
)
|
|
|
|
|
|
logger.debug(f"增量更新: {uri}, range: {change.get('range')}, text: {change.get('text', '')}")
|
|
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
documents[uri]['version'] = version
|
2025-10-14 15:34:44 +08:00
|
|
|
|
logger.debug(f"文档已更新: {uri}, 版本: {version}, 内容预览: {documents[uri]['text'][:100]}")
|
2025-09-30 13:52:36 +08:00
|
|
|
|
|
|
|
|
|
|
async def _handle_did_close(self, client_id: str, params: Dict[str, Any]):
|
|
|
|
|
|
"""处理文档关闭"""
|
|
|
|
|
|
text_document = params.get('textDocument', {})
|
|
|
|
|
|
uri = text_document.get('uri')
|
2025-10-14 15:34:44 +08:00
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
if uri and client_id in self.active_connections:
|
|
|
|
|
|
documents = self.active_connections[client_id]['documents']
|
|
|
|
|
|
if uri in documents:
|
|
|
|
|
|
del documents[uri]
|
|
|
|
|
|
logger.debug(f"文档已关闭: {uri}")
|
|
|
|
|
|
|
2025-10-14 15:34:44 +08:00
|
|
|
|
def _apply_text_edit(self, text: str, range_info: Dict[str, Any], new_text: str) -> str:
|
|
|
|
|
|
"""
|
|
|
|
|
|
应用文本编辑到文档
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
text: 原始文档文本
|
|
|
|
|
|
range_info: LSP Range对象,包含start和end位置
|
|
|
|
|
|
new_text: 要插入的新文本
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
更新后的文档文本
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not range_info:
|
|
|
|
|
|
return new_text
|
|
|
|
|
|
|
|
|
|
|
|
start = range_info.get('start', {})
|
|
|
|
|
|
end = range_info.get('end', {})
|
|
|
|
|
|
start_line = start.get('line', 0)
|
|
|
|
|
|
start_char = start.get('character', 0)
|
|
|
|
|
|
end_line = end.get('line', 0)
|
|
|
|
|
|
end_char = end.get('character', 0)
|
|
|
|
|
|
|
|
|
|
|
|
# 将文档分割成行
|
|
|
|
|
|
lines = text.split('\n')
|
|
|
|
|
|
|
|
|
|
|
|
# 确保行数足够
|
|
|
|
|
|
while len(lines) <= max(start_line, end_line):
|
|
|
|
|
|
lines.append('')
|
|
|
|
|
|
|
|
|
|
|
|
# 获取起始行和结束行
|
|
|
|
|
|
start_line_text = lines[start_line] if start_line < len(lines) else ''
|
|
|
|
|
|
end_line_text = lines[end_line] if end_line < len(lines) else ''
|
|
|
|
|
|
|
|
|
|
|
|
# 构建新的文本
|
|
|
|
|
|
# 保留起始行的前半部分
|
|
|
|
|
|
before = start_line_text[:start_char]
|
|
|
|
|
|
# 保留结束行的后半部分
|
|
|
|
|
|
after = end_line_text[end_char:]
|
|
|
|
|
|
# 新文本
|
|
|
|
|
|
new_content = before + new_text + after
|
|
|
|
|
|
|
|
|
|
|
|
# 构建新的行数组
|
|
|
|
|
|
if start_line == end_line:
|
|
|
|
|
|
# 单行编辑
|
|
|
|
|
|
lines[start_line] = new_content
|
|
|
|
|
|
else:
|
|
|
|
|
|
# 多行编辑
|
|
|
|
|
|
new_lines = new_content.split('\n')
|
|
|
|
|
|
lines = lines[:start_line] + new_lines + lines[end_line + 1:]
|
|
|
|
|
|
|
|
|
|
|
|
return '\n'.join(lines)
|
|
|
|
|
|
|
2025-09-30 13:52:36 +08:00
|
|
|
|
|
|
|
|
|
|
# 全局连接管理器
|
|
|
|
|
|
lsp_manager = LSPConnectionManager()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.websocket("/code-completion/{client_id}")
|
|
|
|
|
|
async def lsp_websocket_endpoint(websocket: WebSocket, client_id: str):
|
|
|
|
|
|
"""LSP WebSocket端点"""
|
|
|
|
|
|
await lsp_manager.connect(websocket, client_id)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
while True:
|
|
|
|
|
|
# 接收消息
|
|
|
|
|
|
data = await websocket.receive_text()
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
message = json.loads(data)
|
|
|
|
|
|
logger.debug(f"接收到LSP消息 {client_id}: {message.get('method', 'unknown')}")
|
|
|
|
|
|
|
|
|
|
|
|
# 处理消息
|
|
|
|
|
|
response = await lsp_manager.handle_message(client_id, message)
|
|
|
|
|
|
|
|
|
|
|
|
# 发送响应
|
|
|
|
|
|
if response:
|
|
|
|
|
|
await lsp_manager.send_message(client_id, response)
|
|
|
|
|
|
|
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
|
|
|
|
logger.error(f"JSON解析失败 {client_id}: {e}")
|
|
|
|
|
|
error_response = {
|
|
|
|
|
|
"jsonrpc": "2.0",
|
|
|
|
|
|
"id": None,
|
|
|
|
|
|
"error": {
|
|
|
|
|
|
"code": -32700,
|
|
|
|
|
|
"message": "Parse error"
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
await lsp_manager.send_message(client_id, error_response)
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"处理消息失败 {client_id}: {e}", exc_info=True)
|
|
|
|
|
|
|
|
|
|
|
|
except WebSocketDisconnect:
|
|
|
|
|
|
logger.info(f"LSP客户端断开连接: {client_id}")
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"WebSocket连接异常 {client_id}: {e}")
|
|
|
|
|
|
finally:
|
|
|
|
|
|
lsp_manager.disconnect(client_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/health")
|
|
|
|
|
|
async def lsp_health_check():
|
|
|
|
|
|
"""LSP服务健康检查"""
|
|
|
|
|
|
return {
|
|
|
|
|
|
"status": "healthy",
|
|
|
|
|
|
"service": "VWED LSP Code Completion",
|
|
|
|
|
|
"active_connections": len(lsp_manager.active_connections),
|
|
|
|
|
|
"capabilities": [
|
|
|
|
|
|
"textDocument/completion",
|
|
|
|
|
|
"textDocument/signatureHelp",
|
|
|
|
|
|
"textDocument/hover",
|
|
|
|
|
|
"textDocument/didOpen",
|
|
|
|
|
|
"textDocument/didChange",
|
|
|
|
|
|
"textDocument/didClose"
|
|
|
|
|
|
]
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.get("/clients")
|
|
|
|
|
|
async def get_active_clients():
|
|
|
|
|
|
"""获取活跃的LSP客户端"""
|
|
|
|
|
|
clients = []
|
|
|
|
|
|
for client_id, connection in lsp_manager.active_connections.items():
|
|
|
|
|
|
clients.append({
|
|
|
|
|
|
"client_id": client_id,
|
|
|
|
|
|
"initialized": connection.get('initialized', False),
|
|
|
|
|
|
"document_count": len(connection.get('documents', {})),
|
|
|
|
|
|
"documents": list(connection.get('documents', {}).keys())
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"active_clients": clients,
|
|
|
|
|
|
"total_count": len(clients)
|
|
|
|
|
|
}
|