实现一个简单的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>

截图 2023-10-05 10-01-50.png
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 协议》,转载必须注明作者和本文链接
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!