monaco代码编辑器


前置知识

需求: 实现一个代码编辑器, 支持js、python、ruby语法提示和在线调试功能,
且支持内置函数(自己定义的一些函数)语法提示和在线debug

第1讲: 代码编辑器

实现方式: monaco官网

1.1 monaco创建实例

mounted生命周期 开始阅读代码, 会顺序执行下面逻辑:
定义主题->添加内置函数->创建model->创建monaco实例->绑定事件

MonacoEditor.vue

<template>
  <div class="monaco-container">
    <div ref="container" class="monaco-editor"></div>
  </div>
</template>

<script>
  // import * as monaco from 'monaco-editor'
  import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'
  import {debounce} from '@/utils/util'
  import {listen} from "vscode-ws-jsonrpc";
  import {
    MonacoLanguageClient,
    CloseAction,
    ErrorAction,
    MonacoServices,
    createConnection,
  } from "monaco-languageclient";
  import ReconnectingWebSocket from "reconnecting-websocket"
  import buildInFn_js from "./MonacoHelper/buildInFn_js"
  import buildInFnList from "./MonacoHelper/buildInFn_other"

  // 插件有bug, 别删撒
  window.monaco = monaco
  window.setImmediate = setTimeout

  export default {
    name: 'MonacoEditor',
    data: function () {
      return {
        monacoEditor: '',       // monaco实例
        codeChangeEmitter: '',  // monaco修改内容回调
        model: null,
        lsp: {}                 // 连接过的语法提示后台
      }
    },
    props: {
      // manaco内容
      codes: {
        type: String,
        default: ''
      },
      // 语法提示后台URL
      socketUrl: {
        type: String,
        default: ''
      }
    },
    methods: {
      // 初始化
      init() {
        this.defineTheme()              // 定义主题
        this.createBuildInFnTip()       // 内置函数提示
        this.createModel()              // 创建model
        this.createMonacoInstance('javascript', this.codes)     // 创建monaco实例
      },

      // 定义主题
      defineTheme() {
        monaco.editor.defineTheme('SlushTheme', {
          "base": "vs",
          "inherit": true,
          "rules": [
            {
              "background": "ebecf0",
              "token": ""
            },
            {
              "foreground": "406040",
              "token": "comment"
            },
            {
              "foreground": "c03030",
              "token": "string"
            },
            {
              "foreground": "0080a0",
              "token": "constant.numeric"
            },
            {
              "fontStyle": "underline",
              "token": "source.ocaml constant.numeric.floating-point"
            },
            {
              "foreground": "800000",
              "token": "constant.character"
            },
            {
              "foreground": "2060a0",
              "token": "keyword"
            },
            {
              "foreground": "2060a0",
              "token": "keyword.operator"
            },
            {
              "fontStyle": "underline",
              "token": "source.ocaml keyword.operator.symbol.prefix.floating-point"
            },
            {
              "fontStyle": "underline",
              "token": "source.ocaml keyword.operator.symbol.infix.floating-point"
            },
            {
              "foreground": "0080ff",
              "token": "entity.name.module"
            },
            {
              "foreground": "0080ff",
              "token": "support.other.module"
            },
            {
              "foreground": "a08000",
              "token": "storage.type"
            },
            {
              "foreground": "008080",
              "token": "storage"
            },
            {
              "foreground": "c08060",
              "token": "entity.name.class.variant"
            },
            {
              "fontStyle": "bold",
              "token": "keyword.other.directive"
            },
            {
              "foreground": "800000",
              "token": "entity.name.function"
            },
            {
              "foreground": "800080",
              "token": "storage.type.user-defined"
            },
            {
              "foreground": "8000c0",
              "token": "entity.name.type.class.type"
            }
          ],
          "colors": {
            "editor.foreground": "#000000",
            "editor.background": "#ebecf0",
            "editor.selectionBackground": "#b0b0ff",
            "editor.lineHighlightBackground": "#00000026",
            "editorCursor.foreground": "#000000",
            "editorWhitespace.foreground": "#bfbfbf"
          }
        })
        monaco.editor.setTheme('SlushTheme')
      },
      // 创建内置函数语法
      createBuildInFnTip() {
        // 添加智能合约内置函数 [js]
        monaco.languages.typescript.javascriptDefaults.addExtraLib(buildInFn_js);

        // 添加智能合约内置函数 [python]
        monaco.languages.registerCompletionItemProvider('python', {
          provideCompletionItems: this.provideCompletion
        })

        // 添加智能合约内置函数 [ruby]
        monaco.languages.registerCompletionItemProvider('ruby', {
          provideCompletionItems: this.provideCompletion
        })
      },
      provideCompletion(model, position) {
        let word = model.getWordUntilPosition(position);
        let range = {
          startLineNumber: position.lineNumber,
          endLineNumber: position.lineNumber,
          startColumn: word.startColumn,
          endColumn: word.endColumn
        };

        let suggestions = [];
        for (let i in buildInFnList) {
          suggestions.push({
            label: buildInFnList[i],
            kind: monaco.languages.CompletionItemKind['Function'],
            insertText: buildInFnList[i],
            detail: '',
            range: range
          });
        }

        return {suggestions};
      },

      // 创建model
      createModel() {
        let model = monaco.editor.getModel(monaco.Uri.parse("inmemory://model.json"))
        if (!model) {
          model = monaco.editor.createModel(
            this.codes,
            "javascript",
            monaco.Uri.parse("inmemory://model.json")
          )
        }
        this.model = model
      },
      // 创建model值
      setModelValue(v = '') {
        this.model.setValue(v)
      },
      // 创建model语言
      setModelLanguage(languages = 'javascript') {
        monaco.editor.setModelLanguage(this.model, languages)
      },

      // 创建monaco实例
      createMonacoInstance(language = 'javascript', v = '') {
        this.setModelLanguage(language)
        this.setModelValue(v)

        this.monacoEditor = monaco.editor.create(this.$refs.container, {
          model: this.model,
          language: language,
          theme: 'SlushTheme', // 默认主题 vs, hc-black, vs-dark
          automaticLayout: true,
          glyphMargin: true,
          lightbulb: {
            enabled: true,
          },
        })

        this.createMonacoEvent()
        this.$emit('codeMounted', this.monacoEditor);
      },
      // 创建Monaco事件
      createMonacoEvent() {
        // 监听IDE内容
        this.monacoEditor.onDidChangeModelContent(() => {
          this.codeChangeHandler(this.monacoEditor)

          this.$nextTick(() => {
            //获取当前的鼠标位置
            let pos = this.monacoEditor.getPosition()
            if (pos) {
              let line = pos.lineNumber //获取当前的行
              if (this.monacoEditor.getModel().getLineContent(line).trim() === '') { // 空行
                this.removeBreakPoint(line)
              } else {
                if (this.hasBreakPoint(line)) { //如果当前行存在断点
                  this.removeBreakPoint(line)
                  this.addBreakPoint(line)
                }
              }
            }
          })
        })

        // 监听鼠标点击
        this.monacoEditor.onMouseDown(e => {
          // 限制点击的位置
          if (e.target.detail
            && e.target.detail.offsetX
            && e.target.detail.offsetX >= 0
            && e.target.detail.offsetX <= 25
          ) {
            let line = e.target.position.lineNumber
            if (this.monacoEditor.getModel().getLineContent(line).trim() === '') {
              return
            }
            // 添加断点/删除断点
            if (!this.hasBreakPoint(line)) {
              this.addBreakPoint(line)
            } else {
              this.removeBreakPoint(line)
            }
            //如果存在上个位置,将鼠标移到上个位置,否则使editor失去焦点
            if (this.lastPosition) {
              this.monacoEditor.setPosition(this.lastPosition)
            } else {
              document.activeElement.blur()
            }
          }
          //更新lastPosition为当前鼠标的位置(只有点击编辑器里面的内容的时候)
          if (e.target.type === 6 || e.target.type === 7) {
            this.lastPosition = this.monacoEditor.getPosition()
          }
        })

        // 监听语言变化
        this.monacoEditor.onDidChangeModelLanguage((e) => {
          MonacoServices.install(this.monacoEditor);

          switch (e.newLanguage) {
            case 'javascript':
              // --snip--
              break;
            case 'python':
              this.$nextTick(this.linkWebSocket)
              break;
            case 'ruby':
              this.$nextTick(this.linkWebSocket)
              break;
          }
        })
      },
      // 文本修改回调
      codeChangeHandler(editor) {
        if (this.codeChangeEmitter) {
          this.codeChangeEmitter(editor);
        } else {
          this.codeChangeEmitter = debounce(
            function (editor) {
              this.$emit('codeChange', editor);
            }, 500
          );
          this.codeChangeEmitter(editor);
        }
      },
      // 连接websocket
      linkWebSocket() {
        let url = this.socketUrl
        if (this.lsp[url]) {
          return null
        } else {
          this.lsp[url] = true
        }

        const webSocket = this.createWebSocket(url);

        listen({
          webSocket,
          onConnection: (connection) => {
            const languageClient = this.createLanguageClient(connection);
            const disposable = languageClient.start();
            connection.onClose(() => disposable.dispose());
          }
        })
      },
      // 切换语言
      changeLanguage(language) {
        let languageMap = {
          js: 'javascript',
          python: 'python',
          ruby: 'ruby'
        }
        monaco.editor.setModelLanguage(this.monacoEditor.getModel(), languageMap[language])
      },
      // 创建语言客户端
      createLanguageClient(connection) {
        let language = this.getMonacoLanguage()

        return new MonacoLanguageClient({
          name: `Language Server Protocol ${language}`,
          clientOptions: {
            documentSelector: [language],
            errorHandler: {
              error: () => ErrorAction.Continue,
              closed: () => CloseAction.DoNotRestart,
            },
          },
          connectionProvider: {
            get: (errorHandler, closeHandler) => {
              return Promise.resolve(
                createConnection(connection, errorHandler, closeHandler)
              );
            },
          },
        })
      },
      // 连接websocket
      createWebSocket(url) {
        const socketOptions = {
          maxReconnectionDelay: 10000,
          minReconnectionDelay: 1000,
          reconnectionDelayGrowFactor: 1.3,
          connectionTimeout: 10000,
          maxRetries: Infinity,
          debug: false,
        };
        return new ReconnectingWebSocket(url, [], socketOptions);
      },

      // 添加断点
      async addBreakPoint(line) {
        let model = this.monacoEditor.getModel()
        if (!model) return
        let value = {
          range: new monaco.Range(line, 1, line, 1),
          options: {isWholeLine: true, linesDecorationsClassName: 'breakpoints'}
        }
        model.deltaDecorations([], [value])
        this.$emit('changeBreakpoint', this.getBreakPoint())
      },
      // 删除断点(如果指定了line,删除指定行的断点,否则删除当前model里面的所有断点)
      async removeBreakPoint(line) {
        let model = this.monacoEditor.getModel()
        if (!model) return
        let decorations
        let ids = []
        if (line !== undefined) {
          decorations = model.getLineDecorations(line)
        } else {
          decorations = model.getAllDecorations()
        }
        for (let decoration of decorations) {
          if (decoration.options.linesDecorationsClassName === 'breakpoints') {
            ids.push(decoration.id)
          }
        }
        if (ids && ids.length) {
          model.deltaDecorations(ids, [])
        }
        this.$emit('changeBreakpoint', this.getBreakPoint())
      },
      // 判断是否有断点
      hasBreakPoint(line) {
        let decorations = this.monacoEditor.getLineDecorations(line)
        for (let decoration of decorations) {
          if (decoration.options.linesDecorationsClassName === 'breakpoints') {
            return true
          }
        }
        return false
      },
      // 获取断点
      getBreakPoint() {
        let breakpoints = this.monacoEditor
          .getModel()
          .getAllDecorations()
          .filter(it => it.options.linesDecorationsClassName === 'breakpoints')
          .map(it => it.range.startLineNumber)
        return breakpoints
      },

      // 获取当前语言
      getMonacoLanguage() {
        return this.monacoEditor.getModel().getLanguageIdentifier().language
      },
    },
    mounted() {
      this.init()
    }
  }
</script>

<style scoped lang="scss">
  .monaco-container {
    width: 100%;
    height: 100%;

    .monaco-editor {
      width: 100%;
      height: 100%;
    }

    /deep/ .breakpoints {
      background-color: #c75450;
      width: 10px !important;
      height: 10px !important;
      left: 15% !important;
      top: 5px;
      border-radius: 5px;
    }

    /deep/ .debugLine {
      background-color: #2d609966;
    }
  }
</style>

1.2 monaco打包配置

vue.config.js

const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin')
const path = require('path');

function resolve(dir) {
  return path.join(__dirname, dir)
}

let webpackConfig = {
  chainWebpack: (config) => {
    config.resolve.alias
      .set('@', resolve('src'))
      .set('vscode', 'monaco-languageclient/lib/vscode-compatibility')
  },
  devServer: {
    port: '9010'
  },
  configureWebpack: {
    plugins: [
      new MonacoWebpackPlugin({
        languages: ['javascript', 'typescript', 'python', 'ruby'],
      })
    ]
  }
};

if (process.env.NODE_ENV !== 'dev') {
  webpackConfig.configureWebpack.devtool = false
}

module.exports = webpackConfig

第2讲: 完整语法提示

实现方式: language-server-protocol官网

在 第1讲 代码中, onDidChangeModelLanguage事件会在语言修改的时候连接语法提示的后台websocket,
node后台相应代码如下:

lspServer.js

#!/usr/bin/env node
"use strict";
Object.defineProperty(exports, "__esModule", {value: true});
const rpcServer = require("../../plugin/vscode-ws-jsonrpc/lib/server"); // vscode-ws-jsonrpc从github下载下来

/**
 * websocket - 后台的socket实例
 * request - socket连接的时候就有该参数 (如下)
 * 
 * wss.on('connection', (webSocket, request) => {
 *  handleLSPWebsocket(webSocket, request)
 * })
 */
module.exports = function handleLSPWebsocket(webSocket, request) {

  let languageServers = {
    python: ['python', '-m', 'pyls'],
    ruby: ['solargraph', 'stdio']
  }

  let langServer;
  Object.keys(languageServers).forEach((key) => {
    if (request.url === '/' + key) {
      langServer = languageServers[key];
    }
  });
  if (!langServer || !langServer.length) {
    console.log('[log] not language server', request.url);
    webSocket.close();
    return;
  }
  let localConnection = rpcServer.createServerProcess(`${request.url}Example`, langServer[0], langServer.slice(1));
  let socket = {
    send: content => webSocket.send(content),
    onMessage: cb => webSocket.onmessage = event => cb(event.data),
    onError: cb => webSocket.onerror = event => {
      if ('message' in event) {
        cb(event.message);
      }
    },
    onClose: cb => webSocket.onclose = event => cb(event.code, event.reason),
    dispose: () => webSocket.close()
  }
  let connection = rpcServer.createWebSocketConnection(socket);
  rpcServer.forward(connection, localConnection);
  console.log(`Forwarding new webSocket`);
  socket.onClose((code, reason) => {
    console.log('Client closed', code, reason);
    localConnection.dispose();
  });
}

第3讲: 在线调试代码

实现方式: debug-adapter-protocol官网

该部分代码其实和语法提示类似, 在语法提示中 monaco-languageclientvscode-ws-jsonrpc
会帮我们完成大部分websocket的接收和发送工作, 我们不需要做太多的判断。
但在调试的时候不行, 因为各个语言插件对 DAP 的实现有略微差异(但整体执行逻辑还是相似的),
所以要根据具体插件来写代码

不建议直接看下面代码, 可以先从 链接 入手

附上js调试前后端代码, 后台使用插件 microsoft/vscode-node-debug2, 其它语言实现方式类似

3.1 前端

MonacoDebugger.vue

<template>
  <div class="monaco-debugger">
    <div class="header">
      <p class="title">{{activeOperation}}</p>
      <i class="iconfont icon-suoxiao" @click="closeDebug"></i>
    </div>
    <!-- S 调试信息 -->
    <div class="content debugContent" v-show="activeOperation === '调试信息'">
      <div class="asider">
        <i class="iconfont icon-step-forward" title="继续" @click="handleContinue"></i>
        <i class="iconfont icon-debugstepover" title="下一步" @click="handleNextStep"></i>
        <i class="iconfont icon-debugstepinto" title="步入" @click="handleStepIn"></i>
        <i class="iconfont icon-debugstepout" title="步出" @click="handleStepOut"></i>
        <i class="iconfont icon-restart-line" title="重启" @click="handleRestart"></i>
        <i class="iconfont icon-stop" title="停止" @click="handleStop"></i>
      </div>
      <div class="infoContainer">
        <div class="tree">
          <el-tree
            ref="scopeTree"
            default-expand-all
            :props="scope.props"
            :data="scope.list"
            @node-click="handleScopeClick"
            node-key="uuid">
            <div class="custom-tree-node" slot-scope="{ node, data }">
              <span class="label" :title="JSON.stringify(data)">{{ data.name }}</span>&nbsp;
              <span v-show="!data.isScope">=</span>&nbsp;
              <span class="label"
                    v-show="!data.modify"
                    @click.stop.prevent="handleScopeValueClick"
                    @dblclick.prevent.stop="handleScopeValueDblclick(node, data)">
                {{ data.value }}
              </span>&nbsp;
              <el-input
                :ref="`input${data.uuid}`"
                @blur="handleScopeValueBlur(node, data)"
                v-show="data.modify"
                size="mini"
                v-model="data.value">
              </el-input>
            </div>
          </el-tree>
        </div>
      </div>
    </div>
    <!-- E 调试信息 -->

    <!-- S 断点信息 -->
    <div class="content bpContent" v-show="activeOperation === '断点信息'">
      <el-table
        :data="bp.list"
        height="100%">
        <el-table-column
          prop="displayLine"
          align="center"
          label="breakpoint">
          <template slot-scope="scope">
            <p @click="setIDEActiveLine(scope.row)">{{scope.row.displayLine}}</p>
          </template>
        </el-table-column>
        <el-table-column
          prop="condition"
          align="center"
          label="condition">
          <template slot-scope="scope">
            <el-input v-model="scope.row.condition" @click.stop="handleClick($event)"
                      @input="addBreakpointCondition"></el-input>
          </template>
        </el-table-column>
      </el-table>
    </div>
    <!-- E 断点信息 -->

    <!-- S 打印信息 -->
    <div class="content consoleContent" v-show="activeOperation === 'Console'">
      <div class="toolBar">
        <el-input v-model="output.search" placeholder="请输入搜索内容" size="mini" @input="searchOutPutList"></el-input>
        <i class="iconfont icon-delete" title="清除所有" @click="clearOutputList"></i>
      </div>
      <div class="consoleList">
        <div v-for="item in output.displayList" :key="item.id" class="consoleItem">
          <div class="label">{{item.label}}</div>
          <div>:{{item.line}}</div>
        </div>
      </div>
    </div>
    <!-- E 打印信息 -->

    <div class="footer">
      <div class="operation"
           v-for="item in operations"
           :key="item.id"
           @click="switchOperation(item)"
           :class="{active: activeOperation === item.text}"
      >
        <i class="iconfont" :class="item.iconName"></i>&nbsp;
        <span>{{item.text}}</span>
      </div>
    </div>
  </div>
</template>

<script>
  import ReconnectingWebSocket from "reconnecting-websocket";
  import Protocol from './MonacoHelper/Protocol'
  import jsDebugger from './MonacoHelper/jsDebugger'
  import pyDebugger from './MonacoHelper/pyDebugger'
  import rbDebugger from './MonacoHelper/rbDebugger'
  import {debounce} from '@/utils/util'

  export default {
    name: 'MonacoDebugger',
    data: function () {
      return {
        // 导航栏
        operations: [
          {iconName: 'icon-debug', text: "调试信息", id: '1'},
          {iconName: 'icon-point', text: "断点信息", id: '2'},
          {iconName: 'icon-console', text: "Console", id: '3'}
        ],
        activeOperation: '调试信息',
        // 调试信息导航
        scope: {
          uuid: 1,                                                        // key
          currentNode: '',                                                // 当前点击行
          props: {label: 'label', children: 'children'},  // 属性
          list: [],                                                       // 列表数据
          flag: {restart: false, isDebugging: false}                      // 标志
        },
        // 断点信息导航
        bp: {
          list: [],
          reservedLine: 1500  // 预留给后台添加代码的行数
        },
        // console导航
        output: {
          search: '',
          list: [],
          id: 0
        },
        // 协议实例 [用于websocket请求, 单独拆出来]
        protocolInstance: new Protocol(),
        language: ''
      }
    },
    props: {
      // 调试后台URL
      debugUrl: {
        type: String,
        default: ''
      }
    },
    mixins: [jsDebugger, pyDebugger, rbDebugger],
    methods: {
      // 连接websocket
      linkWebSocket(language) {
        this.language = language
        this.$nextTick(() => {
          this.webSocket = this.createWebSocket(this.debugUrl);
          this.protocolInstance.setWebsocketInstance(this.webSocket)
          this.createWebSocketEvent()
        })
      },
      // 创建websocket
      createWebSocket(url) {
        const socketOptions = {
          maxReconnectionDelay: 10000,
          minReconnectionDelay: 1000,
          reconnectionDelayGrowFactor: 1.3,
          connectionTimeout: 10000,
          maxRetries: 1,
          debug: false,
        };
        return new ReconnectingWebSocket(url, [], socketOptions);
      },
      // 创建websocket事件
      createWebSocketEvent() {
        // websocket开启
        this.webSocket.addEventListener('open', () => {
          // websocket通信
          this.webSocket.addEventListener('message', (message) => {
            switch (this.language) {
              case "js":
                this.handleMessage(message)
                break;
              case "python":
                this.handlePyMessage(message)
                break;
              case "ruby":
                this.handleRbMessage(message)
                break;
            }
          })
          this.launch()
        })

        // websocket错误
        this.webSocket.addEventListener('error', (error) => {
          console.error(`debug socket error: ${error}`)
        })

        // websocket关闭
        this.webSocket.addEventListener('close', (data) => {
          console.log(`debug socket close: ${data}`)
        })
      },

      // 格式化调试信息 [格式化成tree组件可用格式]
      formatScopeData(message) {
        let data = message.body.scopes.map((item) => {
          item.uuid = this.scope.uuid++
          item.children = []
          item.isScope = true
          return item
        })
        this.scope.list = data
      },
      // 格式化调试信息 [格式化成tree组件可用格式]
      formatVariablesData(message) {
        let data = message.body.variables.reduce((total, item) => {
          if (['Object'].includes(item.type)) {
            item.children = []
          }
          // 删除属性
          if (!['this', '__dirname', '__filename', 'exports', 'module',
            'require', '__proto__', 'prototype', '[[Scopes]]',
            '[[FunctionLocation]]', 'arguments', 'caller'].includes(item.name)) {
            item.uuid = this.scope.uuid++
            item.modify = false
            total.push(item)
          }
          return total
        }, [])
        return data
      },
      // 点击调试信息
      handleScopeClick(node) {
        this.scope.currentNode = node
        this.protocolInstance.variables(node.variablesReference)
      },
      // 点击调试信息的值
      handleScopeValueClick() {
      },
      // 双击调试信息的值
      handleScopeValueDblclick(node, data) {
        // Local和Global作用域下, 第一层属性为Object的不支持修改 [看了chrome也不支持]
        if (node.parent
          && node.parent.data
          && ['Block', 'Local', 'Global'].includes(node.parent.data.name)
          && data.type === 'Object') {
          return
        }
        data.modify = true
        let currentInput = this.$refs[`input${data.uuid}`]
        if (currentInput) {
          this.$nextTick(currentInput.focus)
        }
      },
      // 调试信息输入框失焦
      handleScopeValueBlur(node, data) {
        data.modify = false
        this.requestSetVariable(node, data)
      },
      // 请求设置变量
      requestSetVariable(node, data) {
        if (node.parent && node.parent.data) {
          let id = node.parent.data.variablesReference
          let params = {
            variablesReference: id,
            name: data.name,
            value: data.value,
          }
          this.protocolInstance.setVariable(params)
        }
      },

      // 启动调试
      launch() {
        this.output.list = []
        this.scope.flag.isDebugging = true
        this.protocolInstance.init(this.language)
      },
      // 继续调试
      handleContinue() {
        this.protocolInstance.continue()
      },
      // 下一步调试
      handleNextStep() {
        this.protocolInstance.nextStep()
      },
      // 步入调试
      handleStepIn() {
        this.protocolInstance.stepIn()
      },
      // 步出调试
      handleStepOut() {
        this.protocolInstance.stepOut()
      },
      // 重启调试
      handleRestart() {
        this.scope.flag.restart = true
        this.protocolInstance.reStart()
      },
      // 停止调试
      handleStop() {
        this.protocolInstance.disconnect()
      },

      // 接收断点 [和当前条件断点合并]
      receiveBreakpoint(bps) {
        let conditionMap = this.bp.list.reduce((total, item) => {
          if (item.condition) {
            total[item.line] = item.condition
          }
          return total
        }, {})
        this.bp.list = bps.map((item) => {
          return {line: item + this.bp.reservedLine, condition: conditionMap[item + this.bp.reservedLine], displayLine: item}
        })
        if (this.scope.flag.isDebugging) {
          this.protocolInstance.setBreakpoint(this.bp.list)
        }
      },
      // 切换操作栏
      switchOperation(data) {
        this.activeOperation = data.text
      },
      // 添加断点条件
      addBreakpointCondition: debounce(function () {
        if (this.scope.flag.isDebugging) {
          this.protocolInstance.setBreakpoint(this.bp.list)
        }
      }, 1000),
      // 设置IDE激活行
      setIDEActiveLine(row) {
        this.$emit('changeActiveLine', {column: 1, line: row.displayLine})  // IDE激活回到第一行
      },
      // 关闭调试
      closeDebug() {
        this.handleStop()
        this.$emit('closeDebug')
      },
      // 清空列表
      clearOutputList() {
        this.output.list = []
      },
      // 过滤列表
      searchOutPutList() {
        this.output.displayList = this.output.list.filter((item) => {
          if (item.label) {
            return item.label.indexOf(this.output.search) !== -1
          }else {
            return false
          }
        })
      }
    },
    mounted() {
      /**
       * 代码逻辑是线性的, 但光看代码不利于理解。
       * 看websocket发送信息和接收信息能更好理解代码思路,
       * 至于websocket发送命令顺序是固定的, 不用纠结为什么, 具体可以看DAP文档: https://microsoft.github.io/debug-adapter-protocol/specification
       */
      // this.linkWebSocket()
    },
    watch: {
      "output.list": function () {
        this.searchOutPutList()
      }
    }
  }
</script>

<style scoped lang="scss">
  @import "@/css/mixin.scss";

  .monaco-debugger {
    width: 100%;
    height: 100%;
    background-color: #3c3f41;
    display: flex;
    flex-direction: column;
    justify-content: space-between;
    font-size: 14px;

    .header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      border-bottom: 1px solid #323232;
      color: #9ebba0;
      padding: 0 10px;
      height: 25px;

      .icon-suoxiao {
        cursor: pointer;

        &:hover {
          background-color: #4c5052;
          border-radius: 2px;
        }
      }
    }

    .content {
      flex: 1;

      &.debugContent {
        color: #fff;
        flex: 1;
        display: flex;
        overflow: auto;

        .asider {
          display: flex;
          flex-direction: column;
          align-items: center;
          border-right: 1px solid #323232;
          padding: 0 3px;

          .iconfont {
            cursor: pointer;
            color: #3592c4;

            &.icon-step-forward,
            &.icon-restart-line,
            &.icon-stop {
              color: #499c54;
            }

            &:hover {
              background-color: #4c5052;
              border-radius: 2px;
            }
          }
        }

        .infoContainer {
          flex: 1;

          .tree {
            width: 100%;
            height: 100%;
            overflow-y: auto;
            @include scrollBar;

            .el-tree {
              color: #ff8e8e;
              background-color: #3c3f41;

              /deep/ .el-tree-node {
                &:focus {
                  .el-tree-node__content {
                    background-color: transparent;
                  }
                }

                .el-tree-node__content {
                  .el-tree-node__expand-icon {
                    color: #aeb9c0;
                    /*&.is-leaf {
                      color: transparent;
                    }*/
                  }

                  &:hover {
                    background-color: #0d293e;
                  }
                }
              }
            }
          }
        }
      }

      &.bpContent {
        overflow: hidden;

        /deep/ .el-table {
          background-color: #3c3f41;

          &:before {
            height: 0;
          }

          tr, th {
            background-color: #3c3f41;
            color: #fff;

            &:hover {
              td.el-table__cell {
                background-color: transparent;
              }
            }
          }

          .el-table__cell {
            border: 1px solid #cccccc30;
          }
        }
      }

      &.consoleContent {
        display: flex;
        flex-direction: column;
        color: #b3bab3;
        overflow: hidden;

        .toolBar {
          display: flex;
          align-items: center;
          justify-content: flex-end;
          padding: 0 10px;

          .el-input {
            width: 20%;
            margin-right: 10px;
            color: #aa8774;

            /deep/.el-input__inner {
              background-color: #45494a;
            }
          }

          .icon-delete {
            cursor: pointer;
          }
        }

        .consoleList {
          flex: 1;
          overflow-x: hidden;
          overflow-y: auto;
          @include scrollBar;

          .consoleItem {
            display: flex;
            justify-content: space-between;
            padding-bottom: 5px;
            border-bottom: 1px solid #cccccc30;
            width: 100%;

            .label {
              width: 100%;
              white-space: pre-line;
              word-break: break-all;
            }
          }
        }
      }
    }

    .footer {
      border-top: 1px solid #323232;
      font-size: 12px;
      color: #afb1b3;
      display: flex;
      justify-content: left;
      align-items: center;
      height: 26px;

      .operation {
        padding: 0 10px;
        cursor: pointer;
        height: 100%;
        display: flex;
        justify-content: center;
        align-items: center;

        .iconfont {
          font-size: 12px;
        }

        &:hover {
          background-color: #323232;
          color: #fff;
        }

        &.active {
          background-color: #323232;
          color: #fff;
        }
      }
    }
  }
</style>

./MonacoHelper/Protocol.js

/**
 * @description DAP的基本协议 [防止耦合, 所以单独抽出来]
 * @author xuelang
 */

class Protocol {
  constructor() {
    this.seq = 0                  // 序列号
    this.websocket = null         // websocket实例
    this.root = ''                // 执行命令的文件夹 [发送Initialize事件后会返回前端]
    this.defaultThreadId = ''     // 当前线程Id
    this.defaultFrameId = ''      // 当前堆栈Id
    this.topScopeReference = {}   // 当前作用域 [代码运行到的局部作用域]
    this.adapterID = ''           // 调试器Id [现在直接拿language充当]
    this.suffix = ''              // 要调试的文件后缀
    this.pathSeparator = '/'      // 路径分隔符
  }

  // 设置websocket
  setWebsocketInstance(ws) {
    this.websocket = ws
  }
  // 设置root
  setRoot(cwd) {
    this.root = cwd
    let lastChar = cwd.substr(cwd.length - 1, );
    // window和linux的路径分隔符不一致
    switch (lastChar) {
      case '/':
        this.pathSeparator = '/'
        break;
      case '\\':
        this.pathSeparator = `\\`
        break;
    }
  }
  // 设置defaultThreadId
  setDefaultThreadId(id) {
    this.defaultThreadId = id
  }
  // 设置defaultFrameId
  setFrameId(id) {
    this.defaultFrameId = id
  }
  // 设置topScopeReference
  setTopScopeReference(v) {
    this.topScopeReference = v
  }


  // 格式化请求参数
  requestMessage(command, argument) {
    let request = {
      command,
      seq: this.seq++,
      type: "request",
      arguments: argument,
    }
    if (!argument) {
      delete request.arguments
    }
    this.request(JSON.stringify(request))
  }
  // 发送请求
  request(message) {
    this.websocket.send(message)
  }


  // 初始化
  init(adapterID) {
    let dict = {
      'js': 'js',
      'python': 'py',
      'ruby': 'rb',
    }
    this.adapterID = adapterID;
    this.suffix = dict[adapterID]
    let argument = {
      "clientID": "vscode",
      "clientName": "Code - OSS Dev",
      "adapterID": adapterID,
      "pathFormat": "path",
      "linesStartAt1": true,
      "columnsStartAt1": true,
      "supportsVariableType": true,
      "supportsVariablePaging": true,
      "supportsRunInTerminalRequest": true,
      "locale": "zh-cn"
    }
    this.requestMessage("initialize", argument)
  }
  // 发起调试
  launch() {
    let argument = {
      type: this.adapterID,
      request: "launch",
      name: "Launch Program",
      program: `${this.root}debug${this.pathSeparator}debug.${this.suffix}`,
      cwd: this.root
    };
    this.requestMessage("launch", argument);
  }
  // 加载源文件
  loadedSources() {
    this.requestMessage("loadedSources");
  }
  // 设置断点
  setBreakpoint(breakpoints) {
    let argument = {
      source: {
        name: `debug.${this.suffix}`,
        path: `${this.root}debug${this.pathSeparator}debug.${this.suffix}`,
      },
      breakpoints,
      sourceModified: false,
    };
    this.requestMessage("setBreakpoints", argument);
  }
  // 执行调试
  evaluate() {
    let argument = {expression: "process.pid"}
    this.requestMessage("evaluate", argument);
  }
  // 设置异常断点
  setExceptionBreakpoints() {
    let argument = {filters: []}
    this.requestMessage("setExceptionBreakpoints", argument);
  }
  // 配置完成
  configurationDone() {
    this.requestMessage("configurationDone");
  }
  // 获取当前线程
  threads() {
    this.requestMessage("threads");
  }
  // 获取当前堆栈
  stackTrace() {
    let argument = {
      threadId: this.defaultThreadId,
      startFrame: 0,
      levels: 1
    }
    this.requestMessage("stackTrace", argument);
  }
  // 断开调试
  disconnect() {
    let argument = {
      restart: false
    }
    this.requestMessage("disconnect", argument);
  }
  // 获取作用域
  scopes() {
    let argument = {
      frameId: this.defaultFrameId,
    }
    this.requestMessage("scopes", argument);
  }
  // 获取变量
  variables(id) {
    if (!this.topScopeReference) return;
    let argument = {
      variablesReference: id,
    }
    this.requestMessage("variables", argument);
  }
  // 获取当前作用域变量
  topVariables() {
    if (!this.topScopeReference) return;
    let argument = {
      variablesReference: this.topScopeReference.variablesReference,
    }
    this.requestMessage("variables", argument);
  }
  // 设置变量
  setVariable(argument) {
    this.requestMessage("setVariable", argument);
  }

  // 继续
  continue() {
    let argument = {
      threadId: this.defaultThreadId
    }
    this.requestMessage("continue", argument);
  }
  // 下一步
  nextStep() {
    let argument = {
      threadId: this.defaultThreadId
    }
    this.requestMessage("next", argument);
  }
  // 步入
  stepIn() {
    let argument = {
      threadId: this.defaultThreadId
    }
    this.requestMessage("stepIn", argument);
  }
  // 步出
  stepOut() {
    let argument = {
      threadId: this.defaultThreadId
    }
    this.requestMessage("stepOut", argument);
  }
  // 重启
  reStart() {
    let argument = {restart: true}
    this.requestMessage("terminate", argument);
  }
}

module.exports = Protocol

./MonacoHelper/jsDebugger.js

/**
 * @description 调试socket信息处理 [js]
 * @author xuelang
 */

export default {
  methods: {
    // 处理websocket信息
    handleMessage(message) {
      message = JSON.parse(message.data)
      if (message.type === 'event') {
        this.handleWsEvent(message)
      } else if (message.type === 'response') {
        this.handleWsResponse(message)
      }
    },
    // 处理Event类型
    handleWsEvent(message) {
      let eventName = message.event
      /**
       * 服务器websocket返回事件类型:
       *
       * initialized  初始化
       * output       输出
       * terminated   终止
       */
      switch (eventName) {
        case 'initialized':
          this.protocolInstance.evaluate()
          break;
        case 'output':
          this.eventOutput(message)
          break;
        case 'terminated':
          this.eventTerminated(message)
          break;
      }
    },
    // 处理Response类型
    handleWsResponse(message) {
      let commandName = message.command
      /**
       * 服务器websocket返回命令类型:
       *
       * initialized      初始化
       * launch           发起调试
       * loadedSources    加载服务端文件
       * evaluate         开始调试
       * setExceptionBreakpoints         设置异常断点
       * configurationDone         配置完成
       * continue         继续执行
       * next             下一步
       * stepIn           步入
       * stepOut          步出
       * threads          获取线程
       * stackTrace       获取堆栈
       * terminate        终止
       * scopes           获取作用域
       * variables        获取变量
       */
      switch (commandName) {
        case 'initialize':
          this.responseInitialize(message)
          break;
        case 'launch':
          this.responseLaunch(message)
          break;
        case 'loadedSources':
          this.responseLoadedSources(message)
          break;
        case 'evaluate':
          this.responseEvaluate(message)
          break;
        case 'setExceptionBreakpoints':
          this.responseSetExceptionBreakpoints(message)
          break;
        case 'configurationDone':
        case 'continue':
        case 'next':
        case 'stepIn':
        case 'stepOut':
          this.responseNext(message)
          break;
        case 'threads':
          this.responseThreads(message)
          break;
        case 'stackTrace':
          this.responseStackTrace(message)
          break;
        case 'terminate':
          this.responseTerminate(message)
          break;
        case 'scopes':
          this.responseScopes(message)
          break;
        case 'variables':
          this.responseVariables(message)
          break;
      }
    },

    // 处理Event类型1
    eventOutput(message) {
      let data = message.body
      switch (data.category) {
        case 'telemetry':
          if (data.output === 'debugStopped') {
            this.scope.list = []
            this.scope.flag.isDebugging = false
            this.$emit('changeDebugLine', {column: 0, line: 0})  // IDE激活回到第一行
          }
          break;
        case 'stderr':
          if (data.line) {
            this.activeOperation = 'Console'
          }
        case 'stdout':
          this.output.list.push({
            id: ++this.output.id,
            label: data.output,
            line: data.line
          })
          break;
      }
    },
    // 处理Event类型2
    eventTerminated() {
      if (this.scope.flag.restart) {
        this.launch()
        this.scope.flag.restart = false
      }
    },


    // 处理Response类型1
    responseInitialize(message) {
      let root = message.body.cwd
      this.protocolInstance.setRoot(root)
      this.protocolInstance.launch()
    },
    // 处理Response类型2
    responseLaunch() {
      this.protocolInstance.loadedSources()
    },
    // 处理Response类型3
    responseLoadedSources() {
      this.protocolInstance.setBreakpoint(this.bp.list)
    },
    // 处理Response类型4
    responseEvaluate(message) {
      if (!message.body) {
          return
      }
      this.protocolInstance.setDefaultThreadId(message.body.result)
      this.protocolInstance.setExceptionBreakpoints()
    },
    // 处理Response类型5
    responseSetExceptionBreakpoints() {
      this.protocolInstance.configurationDone()
    },
    // 处理Response类型6
    responseNext() {
      this.protocolInstance.threads()
    },
    // 处理Response类型7
    responseThreads(message) {
      if (message.body.threads.length === 0) return;
      this.protocolInstance.setDefaultThreadId(message.body.threads[0].id)
      this.protocolInstance.stackTrace()
    },
    // 处理Response类型8
    responseStackTrace(message) {
      // 退出 [不是debug中]
      if (!this.scope.flag.isDebugging) return

      // 失败情况
      if (!message.success) {
        this.protocolInstance.stackTrace()
        return
      }

      // 成功情况
      if (message.body.stackFrames.length === 0) return
      let {id, line, column} = message.body.stackFrames[0]
      this.protocolInstance.setFrameId(id)  // 保存堆栈Id
      this.$emit('changeDebugLine', {column, line: line - this.bp.reservedLine})  // 设置IDE当前行
      this.protocolInstance.scopes()  // 获取当前作用域
    },
    // 处理Response类型9
    responseTerminate() {
      this.protocolInstance.disconnect()
    },
    // 处理Response类型10
    responseScopes(message) {
      if (message.body.scopes.length === 0) return
      let topScope = message.body.scopes[0]
      this.protocolInstance.setTopScopeReference(topScope)  // 设置当前作用域
      this.formatScopeData(message)
      this.protocolInstance.topVariables()  // 获取当前作用域变量
    },
    // 处理Response类型11
    responseVariables(message) {
      // 失败
      if (!message.body || message.body.variables.length === 0) return;

      // 成功
      let data = this.formatVariablesData(message)
      let uuid = this.scope.currentNode ? this.scope.currentNode.uuid : this.scope.list[0].uuid
      this.$refs.scopeTree.updateKeyChildren(uuid, data)
      this.scope.currentNode = ''
    },
  }
}

3.2 后台

dapServer.js

var DebugSession = require("./nodeV2/debugSession");

module.exports = function handleDAPWebsocket(webSocket, request) {

  // console.log(`-> step2: link socket ${request.url}`)

  let socket = {
    send: function (content) {
      return webSocket.send(content, function (error) {
        if (error) {
          throw error;
        }
      });
    },
    onMessage: function (cb) {
      return webSocket.on('message', cb);
    },
    onError: function (cb) {
      return webSocket.on('error', cb);
    },
    onClose: function (cb) {
      return webSocket.on('close', cb);
    },
    dispose: function () {
      return webSocket.close();
    }
  };

  let languageMap = {
    '/js/debug': 'javascript',
    '/python/debug': 'python',
    '/ruby/debug': 'ruby'
  }

  if (webSocket.readyState === webSocket.OPEN) {
    new DebugSession(socket, languageMap[request.url]);
  } else {
    webSocket.on('open', function () {
      new DebugSession(socket, languageMap[request.url]);
    });
  }
}

./nodeV2/debugSession.js

let DebugAdapter = require('./debugAdapter')

let uuid = 1

module.exports = class DebugSession {
  constructor(socket, language) {
    this.id = uuid++
    this.socket = socket
    this.language = language
    this.debugAdapter = null
    // console.log('-> step4: new DebugSession')
    this.start()
  }

  start() {
    if (!this.debugAdapter) {
      this.debugAdapter = new DebugAdapter(this.socket)
    }
    this.socket.onMessage(async (event) => {
      try {
        event = JSON.parse(event)
      } catch (e) {
        console.error(e)
      }
      if (event.type === 'event' || event.type === 'request' || event.type === 'response') {
        // console.log('-> step6: DebugSession receive socket message')
        await this.handleMessage(event)
      }
    })
  }

  async handleMessage(event) {
    // console.log('-> step8: DebugAdapter sendMessage to serverProcess')
    if (event.type === 'request') {
      await this.handleRequest(event);
    } else if (event.type === 'response') {
      this.handleResponse(event);
    } else if (event.type === 'event') {
      await this.handleEvent(event);
    }
  }

  async handleRequest(event) {
    if (event.command === 'initialize') {
      await this.debugAdapter.startSession(this.language)
    }
    this.debugAdapter.sendMessage(event)
    // console.log('DebugProtocol.Request', event)
  }

  handleResponse(event) {
    // console.log('DebugProtocol.Response', event)
  }

  handleEvent(event) {
    this.debugAdapter.sendMessage(event)
    // console.log('DebugProtocol.Response', event)
  }
}

./nodeV2/debugAdapter.js

下面代码的 ../../../plugin/vscode-node-debug2 路径自己改一下, github有该插件

let cp = require('child_process')
let path = require('path')
let os = require('os')
// let stream = require('stream')
let AbstractDebugAdapter = require('./abstractDebugAdapter')
const {rubySpawn} = require('ruby-spawn');

module.exports = class DebugAdapter extends AbstractDebugAdapter {
  static TWO_CRLF = '\r\n\r\n';
  static HEADER_LINESEPARATOR = /\r?\n/;	// allow for non-RFC 2822 conforming line separators
  static HEADER_FIELDSEPARATOR = /: */;

  constructor(socket) {
    super();
    this.serverProcess = null
    this.outputStream = null
    this.rawData = null
    this.contentLength = undefined
    this.wssocket = socket
    // console.log('-> step5: new DebugAdapter')
  }

  connect(readable, writable) {
    this.outputStream = writable;
    this.rawData = Buffer.allocUnsafe(0);
    this.contentLength = -1;

    readable.on('data', (data) => {
      return this.handleData(data)
    });
  }


  sendMessage(message) {
    if (this.outputStream) {
      const json = JSON.stringify(message);
      this.outputStream.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}${DebugAdapter.TWO_CRLF}${json}`, 'utf8');
    }
  }

  stopSession() {
    this.cancelPending();
    if (this.wssocket) {
      this.wssocket.close()
    }
    return Promise.resolve(undefined);
  }

  handleData(data) {
    this.rawData = Buffer.concat([this.rawData, data]);

    while (true) {
      if (this.contentLength >= 0) {
        if (this.rawData.length >= this.contentLength) {
          const message = this.rawData.toString('utf8', 0, this.contentLength);
          this.rawData = this.rawData.slice(this.contentLength);
          this.contentLength = -1;
          if (message.length > 0) {
            try {
              let temp = JSON.parse(message)
              if (temp.command === 'initialize') {
                temp.body.cwd = path.join(__dirname, '../../')
              }
              this.wssocket && this.wssocket.send(JSON.stringify(temp));
            } catch (e) {
              console.error(new Error((e.message || e) + '\n' + message));
            }
          }
          continue;	// there may be more complete messages to process
        }
      } else {
        const idx = this.rawData.indexOf(DebugAdapter.TWO_CRLF);
        if (idx !== -1) {
          const header = this.rawData.toString('utf8', 0, idx);
          const lines = header.split(DebugAdapter.HEADER_LINESEPARATOR);
          for (const h of lines) {
            const kvPair = h.split(DebugAdapter.HEADER_FIELDSEPARATOR);
            if (kvPair[0] === 'Content-Length') {
              this.contentLength = Number(kvPair[1]);
            }
          }
          this.rawData = this.rawData.slice(idx + DebugAdapter.TWO_CRLF.length);
          continue;
        }
      }
      break;
    }
  }

  async startSession(language) {
    // console.log('-> step7: DebugAdapter startSession')

    let serverFile = ''
    let child = {}
    switch (language) {
      case 'javascript':
        serverFile = path.join(__dirname, `../../../plugin/vscode-node-debug2/out/src/nodeDebug.js`);
        const forkOptions = {env: process.env, execArgv: [], silent: true};
        child = cp.fork(serverFile, [], forkOptions);
        break;
      case 'python':
        serverFile = path.join(__dirname, "../../../plugin/debugpy2/src/debugpy/adapter");
        child = cp.spawn('python', [serverFile]);
        break;
      case 'ruby':
        child = rubySpawn('readapt', ['stdio']);
        break;
    }

    if (!child.pid) {
      throw new Error(`Unable to launch debug adapter from ${serverFile}`);
    }
    this.serverProcess = child;

    this.serverProcess.on('error', err => {
      console.error('serverProcess error', err);
    });
    this.serverProcess.on('exit', (code, signal) => {
      console.log('serverProcess exit', code);
    });

    this.serverProcess.stdout.on('close', (error) => {
      // 异常退出时检查一下文件路径是否正确
      console.error('serverProcess stdout close', error);
    });
    this.serverProcess.stdout.on('error', error => {
      console.error('serverProcess stdout error', error);
    });

    this.serverProcess.stdin.on('error', error => {
      console.error('serverProcess stdin error', error);
    });

    this.connect(this.serverProcess.stdout, this.serverProcess.stdin);
  }
}

./nodeV2/abstractDebugAdapter.js

module.exports = class AbstractDebugAdapter {

  constructor() {
    this.sequence = 1
    this.pendingRequests = new Map()
    this.requestCallback = null
    this.eventCallback = null
    this.messageCallback = null
  }

  startSession() {
  }

  stopSession() {
  }

  sendMessage(message) {
  }


  onMessage(callback) {
    if (!!this.eventCallback) {
      console.error(new Error(`attempt to set more than one 'Message' callback`));
    }
    this.messageCallback = callback;
  }

  onEvent(callback) {
    if (!!this.eventCallback) {
      console.error(new Error(`attempt to set more than one 'Event' callback`));
    }
    this.eventCallback = callback;
  }

  onRequest(callback) {
    if (!!this.requestCallback) {
      console.error(new Error(`attempt to set more than one 'Request' callback`));
    }
    this.requestCallback = callback;
  }

  sendResponse(response) {
    if (response.seq > 0) {
      console.error(new Error(`attempt to send more than one response for command ${response.command}`));
    } else {
      this.internalSend('response', response);
    }
  }

  sendRequest(command, args, clb, timeout) {
    const request = {
      command: command
    };
    if (args && Object.keys(args).length > 0) {
      request.arguments = args;
    }
    this.internalSend('request', request);
    if (typeof timeout === 'number') {
      const timer = setTimeout(() => {
        clearTimeout(timer);
        const clb = this.pendingRequests.get(request.seq);
        if (clb) {
          this.pendingRequests.delete(request.seq);
          const err = {
            type: 'response',
            seq: 0,
            request_seq: request.seq,
            success: false,
            command,
            message: `timeout after ${timeout} ms`
          };
          clb(err);
        }
      }, timeout);
    }
    if (clb) {
      this.pendingRequests.set(request.seq, clb);
    }
  }

  acceptMessage(message) {
    if (this.messageCallback) {
      this.messageCallback(message);
    } else {
      switch (message.type) {
        case 'event':
          if (this.eventCallback) {
            this.eventCallback(message);
          }
          break;
        case 'request':
          if (this.requestCallback) {
            this.requestCallback(message);
          }
          break;
        case 'response':
          const response = message;
          const clb = this.pendingRequests.get(response.request_seq);
          if (clb) {
            this.pendingRequests.delete(response.request_seq);
            clb(response);
          }
          break;
      }
    }
  }

  internalSend(typ, message) {
    message.type = typ;
    message.seq = this.sequence++;
    this.sendMessage(message);
  }

  cancelPending() {
    const pending = this.pendingRequests;
    this.pendingRequests = new Map();
    setTimeout(_ => {
      pending.forEach((callback, request_seq) => {
        const err = {
          type: 'response',
          seq: 0,
          request_seq,
          success: false,
          command: 'canceled',
          message: 'canceled'
        };
        callback(err);
      });
    }, 1000);
  }

  dispose() {
    this.cancelPending();
  }
}

第4讲: AST语法树

该部分代码编辑器无关, 只是在实现公司需求过程中, 自己用到了AST语法树, 所以简单记录一下

4.1 获取js函数

let acorn = require("acorn");

/**
 * 从js语法树获取函数信息
 * @param code 代码字符串
 */
export function getJsCodeFn(code){
  let result = {}
  let ast = acorn.parse(code, {ecmaVersion: 2020})
  ast.body.forEach((node) => {
    switch (node.type) {
      case 'FunctionDeclaration':
        node.params.forEach((item) => {
          item.value = ''
        })
        result[node.id.name] = {params: node.params}
        break;
      case 'VariableDeclaration':
        node.declarations.forEach((declar) => {
          if (['FunctionExpression', 'ArrowFunctionExpression'].includes(declar.init.type)) {
            declar.init.params.map((item) => {
              item.value = ''
            })
            result[declar.id.name] = {params: declar.init.params}
          }
        })
        break;
    }
  })
  return result
}

4.2 获取python函数

let filbert = require("filbert")

/**
 * 从python语法树获取函数信息
 * @param code 代码字符串
 */
export function getPythonCodeFn(code) {
  let result = {}
  let ast = filbert.parse(code)
  ast.body.forEach((node) => {
    switch (node.type) {
      case 'FunctionDeclaration':
        node.params.forEach((item) => {
          item.value = ''
        })
        result[node.id.name] = {params: node.params}
        break;
    }
  })
  return result
}

4.3 修改js代码

let fs = require('fs')
let path = require('path')
let parser = require("@babel/parser");
let traverse = require("@babel/traverse").default;
let template = require("@babel/template").default;
let t = require("@babel/types");
let generator = require("@babel/generator").default;

// 全局函数
const _fnList = new Set([])
// 对象方法
const _methodList = new Set([])

/**
 * 格式化内置函数 [js]
 */
function formatJsUseAst({cc}) {
  // 转ast
  const ast = parser.parse(cc);
  // 遍历ast
  traverse(ast, {
    MemberExpression(path) {
      let node = path.node
      let name = node.property.name
      if (path.parent.type === "CallExpression") {
        if (_methodList.has(name)) {
          let buildAwait = template(`await %%expressionName%%`)
          const newNode = buildAwait({
            expressionName: t.cloneNode(path.parent)
          })
          let parent = path.parentPath
          parent.replaceWith(newNode)
          path.skip()
          path.parentPath.skip()
        }
      }
    },
    CallExpression(path) {
      let node = path.node
      let name = node.callee.name
      // if (_fnList.has(name)) {
        let buildAwait = template(`await %%expressionName%%`)
        const newNode = buildAwait({
          expressionName: t.cloneNode(node)
        })
        path.replaceWith(newNode)
        path.skip()
      // }
    },
    FunctionDeclaration(path) {
      path.node.async = true
    },
    ArrowFunctionExpression(path) {
      path.node.async = true
    },
    FunctionExpression(path) {
      path.node.async = true
    }
  });
  // 转代码
  const code = generator(ast, {retainLines: true}).code

  return code
}

文章作者: Alex
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Alex !
  目录