523 lines
18 KiB
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> |