实现一个简单的ide-demo
因为新公司是搞ide的,所以想从0-1实现一个ide。平时主要是做web开发,所以这里就实现一个web版的吧。
ide主要分两部分:
1、前端: 获取用户输入展现到ide容器中,高亮显示语法。
2、后端: 语法/语义分析,比如代码错误提示、代码提示。(在编译器开发中该部分属于前端)
以前的ide比如eclipse、idea好像不会区分前后端,好像是微软在开发vscode时提出的lsp协议,所以大部分ide开始接入lsp了。
前端实现:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Web-Ide</title>
<link rel="stylesheet" href="./index.css">
</head>
<body>
<div class="container">
<div id="content">
<span id="cursor"></span>
</div>
</div>
<script src="./index.js"></script>
</body>
</html>
id为content的div为显示开发者的输入容器,cursor为光标组件。
/**
* 用户输入结果生成对应的内容
*/
function inputContent() {
document.addEventListener('keydown', function(event) {
// 获取用户输入,根据用户按下不同键将字符显示到content容器中
createNode(event.key, type)
});
}
let newNode = null //是否需要新建节点
/**
* 创建节点,比如用户输入时主动创建元素插入到ide容器中
* key为letter时是输入普通字符比如1-9 a-zA-z
* @param {*} key
* @param {*} value
*/
function createNode (key, value) {
if (key === 'letter') { // 可显示字符
newNode = newNode === null ? document.createElement('span') : newNode
newNode.innerText = newNode.innerText += value
newNode.className = 'letter'
ContentDom.insertBefore(newNode, CursorDom)
} else if (key === 'BlankSpace') { // 空格
newNode = document.createElement('span')
newNode.innerText = value
newNode.className = 'letter'
ContentDom.insertBefore(newNode, CursorDom)
highlightNode(newNode.previousSibling)
newNode = null
} else if (key === 'Enter') { // 回车
newNode = document.createElement('div')
newNode.className = 'enter'
ContentDom.insertBefore(newNode, CursorDom)
newNode = null
}
}
/**
* 节点是否高亮
* @param {*} node
*/
function highlightNode (node) {
const key = node.innerText
if (key && node && ['def', 'class'].includes(key)) {
node.className = node.className += ' language-keyword'
}
}
/**
* 删除节点
* type为Backspace: 点击Backspace,删除代码
* @param {*} type
*/
function deleteNode (type) {
if (type === 'Backspace') {
const node = CursorDom.previousSibling
if (node && node.classList && node.classList.contains('letter')) {
const txt = node.innerText
node.innerText = txt.slice(0, -1)
if (node.innerText === '') {
node.remove()
}
} else if (node.className === 'enter') {
node.remove()
}
}
}
用户连续的输入都将使用一个span标签包裹,如果出现换行或者空格代表此次输入结束之后的输入由新的span标签包裹。每次空格时都需要判断前一个span中的内容是否需要高亮显示。
按下Backspace时代表删除,删除光标前一个元素最后的一个字符,如果元素无字符就删除该元素。
全部代码: github.com/schizobulia/ide-study/b...
后端实现
import { IWebSocket, WebSocketMessageReader, WebSocketMessageWriter } from 'vscode-ws-jsonrpc';
import { createConnection, createServerProcess, forward } from 'vscode-ws-jsonrpc/server';
import { Message, InitializeRequest, InitializeParams } from 'vscode-languageserver';
import { WebSocketServer } from 'ws';
const launchLanguageServer = (socket: IWebSocket) => {
const serverName = 'PYRIGHT'
const a = '/xxx/monaco-languageclient/node_modules/pyright/dist/pyright-langserver.js'
const serverConnection = createServerProcess(serverName, 'node', [a, '--stdio'])
const reader = new WebSocketMessageReader(socket)
const writer = new WebSocketMessageWriter(socket)
const socketConnection = createConnection(reader, writer, () => socket.dispose());
if (serverConnection) {
forward(socketConnection, serverConnection, message => {
if (Message.isRequest(message)) {
console.log(`${serverName} Server received:`);
console.dir(message);
if (message.method === InitializeRequest.type.method) {
const initializeParams = message.params as InitializeParams;
initializeParams.processId = process.pid;
}
}
if (Message.isResponse(message)) {
console.log(`${serverName} Server sent:`);
console.log(message);
}
return message;
});
}
}
const runServer = () => {
process.on('uncaughtException', function (err: any) {
console.error('Uncaught Exception: ', err.toString());
if (err.stack) {
console.error(err.stack);
}
});
const wss = new WebSocketServer({ port: 6000 });
wss.on('connection', function (ws) {
const socket: IWebSocket = {
send: content => ws.send(content, error => {
if (error) {
throw error;
}
}),
onMessage: cb => ws.on('message', (data) => {
cb(data);
}),
onError: cb => ws.on('error', cb),
onClose: cb => ws.on('close', cb),
dispose: () => ws.close()
};
launchLanguageServer(socket)
});
wss.on('error', function (params) {
console.error(params)
})
}
runServer()
目前大部分语言都已经有了相关的lsp-sdk,我们可以找一个合适的即可,这里我们参考monaco中使用的pyright-langserver。有了lsp-sdk之后我们的实现其实就很简单了,只要将lsp-server和前端部分打通就可以了。参考上述代码通过rpc打通既可。
为了更好的理解通信方式此处我先通过node开启一个webscoket客户端与lsp-server通信,这样更好的理解每一个协议是做什么的。 github.com/schizobulia/ide-study/b...
lsp-server全部代码: github.com/schizobulia/ide-study/b... ,此处代码基本来源于monaco-languageclient项目。
备注:本来想单独运行index.ts文件,奈何各种环境配置好麻烦,此处给出简单的运行方式:
# 运行前端
$ 浏览器直接打开index.html可看到前端效果
# 运行后端
$ git clone https://github.com/TypeFox/monaco-languageclient.git
$ cd monaco-languageclient
$ npm i
$ npm run build
$ 将index.ts复制到monaco-languageclient文件夹下
$ node --loader ts-node/esm index.ts # 成功启动lsp-server
$ node client.js # 回到ide-study目录执行该命令,控制台打印全部日志。
前端和后端的连接就比较简单,可以自己搞一个socket连接起来进行。这里只是一个学习使用的demo就不实现了。当然也有更好的方式可以参考monaco-languageclient项目,这里只是为了更好的理解ide的实现流程,所以就尽可能拆分的细致一点。
参考链接
如果你喜欢我的作品,请考虑赞助我,以保持它们的可持续性。
新工作还没有入职,如果有远程工作可以联系我。
本作品采用《CC 协议》,转载必须注明作者和本文链接