VWED_server/test_lsp_demo.html

523 lines
18 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>VWED LSP 代码补全测试</title>
<style>
body {
font-family: 'Consolas', 'Monaco', monospace;
margin: 20px;
background-color: #1e1e1e;
color: #d4d4d4;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
background: #2d2d30;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px;
}
.editor-container {
display: flex;
gap: 20px;
}
.editor-panel {
flex: 1;
background: #2d2d30;
border-radius: 8px;
padding: 20px;
}
.editor {
width: 100%;
height: 400px;
background: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 4px;
padding: 10px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 14px;
color: #d4d4d4;
resize: vertical;
}
.completion-list {
background: #252526;
border: 1px solid #3e3e42;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
margin-top: 10px;
}
.completion-item {
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid #3e3e42;
}
.completion-item:hover {
background: #094771;
}
.completion-item.selected {
background: #0e639c;
}
.completion-label {
font-weight: bold;
color: #4ec9b0;
}
.completion-detail {
font-size: 12px;
color: #9cdcfe;
margin-left: 10px;
}
.completion-doc {
font-size: 11px;
color: #6a9955;
margin-top: 4px;
}
.controls {
margin: 20px 0;
}
.btn {
background: #0e639c;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin-right: 10px;
}
.btn:hover {
background: #1177bb;
}
.btn:disabled {
background: #666;
cursor: not-allowed;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-size: 14px;
}
.status.connected {
background: #1a7f2e;
color: white;
}
.status.disconnected {
background: #a74c47;
color: white;
}
.status.error {
background: #d73027;
color: white;
}
.log {
background: #1e1e1e;
border: 1px solid #3e3e42;
border-radius: 4px;
padding: 10px;
height: 200px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
white-space: pre-wrap;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>VWED LSP 代码补全测试</h1>
<p>测试基于WebSocket的Language Server Protocol代码补全功能</p>
</div>
<div class="controls">
<button id="connectBtn" class="btn">连接LSP服务器</button>
<button id="disconnectBtn" class="btn" disabled>断开连接</button>
<button id="triggerCompletionBtn" class="btn" disabled>手动触发补全</button>
<button id="clearLogBtn" class="btn">清除日志</button>
</div>
<div id="status" class="status disconnected">未连接</div>
<div class="editor-container">
<div class="editor-panel">
<h3>Python代码编辑器</h3>
<textarea id="codeEditor" class="editor" placeholder="在这里输入Python代码...试试输入 'VWED.' 来测试补全功能">def boot():
# 试试输入 VWED. 来测试补全
VWED.
# 测试变量补全
my_variable = "test"
my_
# 测试Python内置函数
pri</textarea>
<div id="completionList" class="completion-list" style="display: none;"></div>
</div>
<div class="editor-panel">
<h3>调试日志</h3>
<div id="logOutput" class="log"></div>
</div>
</div>
</div>
<script>
class VWEDLSPClient {
constructor() {
this.ws = null;
this.isConnected = false;
this.clientId = 'test-client-' + Date.now();
this.documentUri = 'file:///test.py';
this.documentVersion = 1;
this.requestId = 1;
this.pendingRequests = new Map();
this.editor = document.getElementById('codeEditor');
this.statusElement = document.getElementById('status');
this.logElement = document.getElementById('logOutput');
this.completionList = document.getElementById('completionList');
this.setupEventListeners();
}
setupEventListeners() {
document.getElementById('connectBtn').addEventListener('click', () => this.connect());
document.getElementById('disconnectBtn').addEventListener('click', () => this.disconnect());
document.getElementById('triggerCompletionBtn').addEventListener('click', () => this.triggerCompletion());
document.getElementById('clearLogBtn').addEventListener('click', () => this.clearLog());
this.editor.addEventListener('input', (e) => this.onTextChange(e));
this.editor.addEventListener('keydown', (e) => this.onKeyDown(e));
}
connect() {
if (this.isConnected) return;
this.log('正在连接LSP服务器...');
const wsUrl = `ws://localhost:8000/lsp/code-completion/${this.clientId}`;
this.ws = new WebSocket(wsUrl);
this.ws.onopen = () => {
this.isConnected = true;
this.updateStatus('connected', '已连接');
this.log('LSP服务器连接成功');
// 发送初始化请求
this.sendInitialize();
// 打开文档
this.sendDidOpen();
document.getElementById('connectBtn').disabled = true;
document.getElementById('disconnectBtn').disabled = false;
};
this.ws.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
this.handleMessage(message);
} catch (e) {
this.log(`解析消息失败: ${e.message}`);
}
};
this.ws.onclose = () => {
this.isConnected = false;
this.updateStatus('disconnected', '连接已断开');
this.log('LSP服务器连接已断开');
document.getElementById('connectBtn').disabled = false;
document.getElementById('disconnectBtn').disabled = true;
};
this.ws.onerror = (error) => {
this.updateStatus('error', '连接错误');
this.log(`连接错误: ${error}`);
};
}
disconnect() {
if (this.ws) {
this.ws.close();
}
}
sendMessage(message) {
if (this.ws && this.isConnected) {
const jsonMessage = JSON.stringify(message);
this.ws.send(jsonMessage);
this.log(`发送: ${message.method || 'response'}`);
}
}
sendInitialize() {
const message = {
jsonrpc: "2.0",
id: this.requestId++,
method: "initialize",
params: {
clientInfo: {
name: "VWED Test Client",
version: "1.0.0"
},
capabilities: {
textDocument: {
completion: {
dynamicRegistration: false
},
signatureHelp: {
dynamicRegistration: false
},
hover: {
dynamicRegistration: false
}
}
}
}
};
this.sendMessage(message);
}
sendDidOpen() {
const message = {
jsonrpc: "2.0",
method: "textDocument/didOpen",
params: {
textDocument: {
uri: this.documentUri,
languageId: "python",
version: this.documentVersion,
text: this.editor.value
}
}
};
this.sendMessage(message);
}
sendDidChange() {
this.documentVersion++;
const message = {
jsonrpc: "2.0",
method: "textDocument/didChange",
params: {
textDocument: {
uri: this.documentUri,
version: this.documentVersion
},
contentChanges: [
{
text: this.editor.value
}
]
}
};
this.sendMessage(message);
}
requestCompletion(position) {
const requestId = this.requestId++;
const message = {
jsonrpc: "2.0",
id: requestId,
method: "textDocument/completion",
params: {
textDocument: {
uri: this.documentUri
},
position: position,
context: {
documentText: this.editor.value
}
}
};
this.pendingRequests.set(requestId, 'completion');
this.sendMessage(message);
return requestId;
}
onTextChange(event) {
if (!this.isConnected) return;
// 发送文档变更通知
this.sendDidChange();
// 检查是否需要触发补全
const cursorPosition = this.getCursorPosition();
const char = event.inputType === 'insertText' ? event.data : null;
this.log(`输入字符: "${char}", 输入类型: ${event.inputType}`);
// 触发补全的条件
if (char === '.' || char === ' ' || event.inputType === 'insertText') {
setTimeout(() => {
this.triggerCompletion();
}, 100);
}
// 也在输入VWED时触发
const currentText = this.editor.value;
const beforeCursor = currentText.substring(0, this.editor.selectionStart);
if (beforeCursor.endsWith('VWED.') || beforeCursor.endsWith('VWED')) {
setTimeout(() => {
this.triggerCompletion();
}, 100);
}
}
onKeyDown(event) {
// 处理快捷键
if (event.ctrlKey && event.key === ' ') {
event.preventDefault();
this.triggerCompletion();
}
}
triggerCompletion() {
if (!this.isConnected) return;
const position = this.getCursorPosition();
this.requestCompletion(position);
}
getCursorPosition() {
const cursorPos = this.editor.selectionStart;
const textBeforeCursor = this.editor.value.substring(0, cursorPos);
const lines = textBeforeCursor.split('\n');
return {
line: lines.length - 1,
character: lines[lines.length - 1].length
};
}
handleMessage(message) {
this.log(`接收: ${JSON.stringify(message, null, 2)}`);
if (message.id && this.pendingRequests.has(message.id)) {
const requestType = this.pendingRequests.get(message.id);
this.pendingRequests.delete(message.id);
if (requestType === 'completion' && message.result) {
this.showCompletions(message.result.items || []);
}
}
}
showCompletions(items) {
this.completionList.innerHTML = '';
if (items.length === 0) {
this.completionList.style.display = 'none';
return;
}
items.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'completion-item';
if (index === 0) div.classList.add('selected');
div.innerHTML = `
<span class="completion-label">${item.label}</span>
<span class="completion-detail">${item.detail || ''}</span>
${item.documentation ? `<div class="completion-doc">${item.documentation}</div>` : ''}
`;
div.addEventListener('click', () => {
this.insertCompletion(item);
});
this.completionList.appendChild(div);
});
this.completionList.style.display = 'block';
}
insertCompletion(item) {
// 简单的补全插入逻辑
const cursorPos = this.editor.selectionStart;
const textBefore = this.editor.value.substring(0, cursorPos);
const textAfter = this.editor.value.substring(cursorPos);
// 查找需要替换的前缀
const match = textBefore.match(/([a-zA-Z_][a-zA-Z0-9_.]*)$/);
let replaceStart = cursorPos;
if (match) {
replaceStart = cursorPos - match[1].length;
}
const newText = textBefore.substring(0, replaceStart) + item.insertText + textAfter;
this.editor.value = newText;
// 设置光标位置
const newCursorPos = replaceStart + item.insertText.length;
this.editor.setSelectionRange(newCursorPos, newCursorPos);
this.completionList.style.display = 'none';
this.editor.focus();
// 发送文档变更
this.sendDidChange();
}
updateStatus(type, message) {
this.statusElement.className = `status ${type}`;
this.statusElement.textContent = message;
}
log(message) {
const timestamp = new Date().toLocaleTimeString();
this.logElement.textContent += `[${timestamp}] ${message}\n`;
this.logElement.scrollTop = this.logElement.scrollHeight;
}
clearLog() {
this.logElement.textContent = '';
}
}
// 初始化LSP客户端
const lspClient = new VWEDLSPClient();
// 页面加载完成后自动连接
window.addEventListener('load', () => {
setTimeout(() => {
lspClient.connect();
}, 1000);
});
</script>
</body>
</html>