前置知识
需求: 实现一个代码编辑器, 支持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-languageclient和vscode-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>
<span v-show="!data.isScope">=</span>
<span class="label"
v-show="!data.modify"
@click.stop.prevent="handleScopeValueClick"
@dblclick.prevent.stop="handleScopeValueDblclick(node, data)">
{{ data.value }}
</span>
<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>
<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
}