在Monaco Editor中实现断点设置的方法详解

 更新时间:2024年04月02日 08:46:18   作者:云末花开  
Monaco Editor 是 vscode 等产品使用的代码编辑器,功能强大(且复杂),由微软维护,本文在 React + TypeScript(Vite)框架下使用 @monaco-editor/react 并介绍开发断点显示时踩到的坑,文中有详细的代码示例供大家参考,需要的朋友可以参考下
(福利推荐:【腾讯云】服务器最新限时优惠活动,云服务器1核2G仅99元/年、2核4G仅768元/3年,立即抢购>>>:9i0i.cn/qcloud

(福利推荐:你还在原价购买阿里云服务器?现在阿里云0.8折限时抢购活动来啦!4核8G企业云服务器仅2998元/3年,立即抢购>>>:9i0i.cn/aliyun

Monaco Editor 是 vscode 等产品使用的代码编辑器,功能强大(且复杂),由微软维护。编辑器没有原生提供设置断点的功能,本文在 React + TypeScript(Vite)框架下使用 @monaco-editor/react 并介绍开发断点显示时踩到的坑。最终展示 鼠标悬浮显示断点按钮,点击后进行断点的设置或移除

本文不涉及调试功能的具体实现,可阅读 DAP 文档

最终实现可直接拉到文末。

搭建 Playground

React + TypeScript,使用的封装为 @monaco-editor/react

建立项目并配置依赖:

yarn create vite monaco-breakpoint
...
yarn add @monaco-editor/react

依赖处理完成后,我们编写简单的代码将编辑器展示到页面中:(App.tsx)

import Editor from '@monaco-editor/react'
import './App.css'

function App() {
  return (
    <>
      <div style={{ width: '600px', height: '450px' }}>
        <Editor theme='vs-dark' defaultValue='// some comment...' />
      </div>
    </>
  )
}
export default App

接下来在该编辑器的基础上添加设置断点的能力。

一种暴力的办法:手动编写断点组件

不想阅读 Monaco 文档,最自然而然的想法就是手动在编辑器的左侧手搭断点组件并显示在编辑器旁边。

首先手动设置如下选项

  • 编辑器高度为完全展开(通过设置行高并指定 height 属性)
  • 禁用代码折叠选项
  • 为编辑器外部容器添加滚动属性
  • 禁用小地图
  • 禁用内部滚动条的消费滚动
  • 禁用超出滚动

编辑器代码将变为:

const [code, setCode] = useState('')

...

<div style={{ width: '600px', height: '450px', overflow: 'auto' }}>
<Editor
  height={code.split('\n').length * 20}
  onChange={(value) => setCode(value!)}
  theme='vs-dark'
  value=[code]
  language='python'
  options={{
	lineHeight: 20,
	scrollBeyondLastLine: false,
	scrollBeyondLastColumn: 0,
	minimap: { enabled: false },
	scrollbar: { alwaysConsumeMouseWheel: false },
	fold: false,
  }}
/>
</div>

现在编辑器的滚动由父容器的滚动条接管,再来编写展示断点的组件。我们希望断点组件展示在编辑器的左侧。 先设置父容器水平布局:

display: 'flex', flexDirection: 'row'

编写断点组件(示例):

  <div style={{ width: '600px', height: '450px', overflow: 'auto', display: 'flex', flexDirection: 'row' }}>
	<div
	  style={{
		width: '20px',
		height: code.split('\n').length * 20,
		background: 'black',
		display: 'flex',
		flexDirection: 'column',
	  }}
>
	  {[...Array(code.split('\n').length)].map((_, index) => (
		<div style={{ width: '20px', height: '20px', background: 'red', borderRadius: '50%' }} key={index} />
	  ))}
	</div>
	<Editor ...

目前断点组件是能够展示了,但本文不在此方案下进行进一步的开发。这个方案的问题:

  • 强行设置组件高度能够展示所有代码,把 Monaco 的性能优化整个吃掉了。这样的编辑器展示代码行数超过一定量后页面会变的非常卡,性能问题严重。
  • 小地图、超行滚动、代码块折叠等能力需要禁用,限制大。

本来目标是少读点 Monaco 的超长文档,结果实际上动了那么多编辑器的配置,最终也还是没逃脱翻文档的结局。果然还是要使用更聪明的办法做断点展示。

使用 Decoration

Monaco Editor Playground 中有使用行装饰器的例子,一些博文也提到了可以使用 DeltaDecoration 为编辑器添加行装饰器。不过跟着写实现的时候出现了一些诡异的问题,于是查到 Monaco 的 changelog 表示该方法已被弃用,应当使用 createDecorationsCollection。 这个 API 的文档在 这里

为了能使用 editor 的方法,我们先拿到编辑器的实例(本文使用的封装库需要这么操作。如果你直接使用了原始的 js 库或者其他封装,应当可以用其他的方式拿到实例)。 安装 monaco-editor js 库:

yarn add monaco-editor

将 Playground 代码修改如下:

import Editor from '@monaco-editor/react'
import * as Monaco from 'monaco-editor/esm/vs/editor/editor.api'
import './App.css'
import { useState } from 'react'

function App() {
  const [code, setCode] = useState('')

  function handleEditorDidMount(editor: Monaco.editor.IStandaloneCodeEditor) {
	// 在这里就拿到了 editor 实例,可以存到 ref 里后面继续用
  }
  return (
    <>
      <div style={{ width: '600px', height: '450px' }}>
        <Editor
		    onChange={(value) => setCode(value!)}
		    theme='vs-dark'
	        value=[code]
	        language='python'
	        onMount={handleEditorDidMount}
        />
      </div>
    </>
  )
}

export default App

Monaco Editor 的装饰器是怎样设置的?

方法 createDecorationsCollection 的参数由 IModelDeltaDecoration 指定。其含有两个参数,分别为 IModelDecorationOptions 和 IRange。Options 参数指定了装饰器的样式等配置信息,Range 指定了装饰器的显示范围(由第几行到第几行等等)。

// 为从第四行到第四行的范围(即仅第四行)添加样式名为breakpoints的行装饰器

const collections: Monaco.editor.IModelDeltaDecoration[] = []
collections.push({
	range: new Monaco.Range(4, 1, 4, 1),
	options: {
		isWholeLine: true,
		linesDecorationsClassName: 'breakpoints',
		linesDecorationsTooltip: '点击添加断点',
	},
})

const bpc = editor.createDecorationsCollection(collections)

该方法的返回实例提供了一系列方法,用于添加、清除、更新、查询该装饰器集合状态。详见 IEditorDecorationsCollection

维护单个装饰器组:样式表

我们先写一些 css:

.breakpoints {
  width: 10px !important;
  height: 10px !important;
  left: 5px !important;
  top: 5px;
  border-radius: 50%;
  display: inline-block;
  cursor: pointer;
}

.breakpoints:hover {
  background-color: rgba(255, 0, 0, 0.5);
}

.breakpoints-active {
  background-color: red;
}

添加装饰器。我们先添加一个 1 到 9999 行的范围来查看效果(省事)。当然更好的办法是监听行号变动。

const collections: Monaco.editor.IModelDeltaDecoration[] = []
collections.push({
	range: new Monaco.Range(1, 1, 9999, 1),
	options: {
		isWholeLine: true,
		linesDecorationsClassName: 'breakpoints',
		linesDecorationsTooltip: '点击添加断点',
	},
})

const bpc = editor.createDecorationsCollection(collections)

装饰器有了,监听断点组件的点击事件。使用 Monaco 的 onMouseDown Listener。

editor.onMouseDown((e) => {
  if (e.event.target.classList.contains('breakpoints')) 
	e.event.target.classList.add('breakpoints-active')
})

看起来似乎没有问题?Monaco 的行是动态生成的,这意味着设置的 css 样式并不能持久显示。

看来还得另想办法。

维护单个装饰器组:手动维护 Range 列表

通过维护 Ranges 列表可以解决上述问题。 不过我们显然能注意到一个问题:我们希望设置的断点是行绑定的。

假设我们手动维护所有设置了断点的行数,显示断点时若有行号变动,断点将保留在原先的行号所在行。监听前后行号变动非常复杂,显然不利于实现。有没有什么办法能够直接利用 Monaco 的机制?

维护两个装饰器组

我们维护两个装饰器组。一个用于展示未设置断点时供点击的按钮(①),另一个用来展示设置后的断点(②)。行号更新后,自动为所有行设置装饰器①,用户点击①时,获取实时行号并设置②。断点的移除和获取均通过装饰器组②实现。

  • 点击①时,调用装饰器组②在当前行设置展示已设置断点。
  • 点击②时,装饰器组②直接移除当前行已设置的断点,露出装饰器①。

如需获取设置的断点情况,可直接调用装饰器组②的 getRanges 方法。

多个装饰器组在编辑器里是怎样渲染的?

两个装饰器都会出现在编辑器中,和行号在同一个父布局下。

顺便折腾下怎么拿到当前行号:设置的装饰器和展示行号的组件在同一父布局下且为相邻元素,暂且先用一个笨办法拿到。

// onMouseDown事件下
const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)

实现

回到 Playground:

import Editor from '@monaco-editor/react'
import * as Monaco from 'monaco-editor/esm/vs/editor/editor.api'
import './App.css'
import { useState } from 'react'

function App() {
  const [code, setCode] = useState('')

  function handleEditorDidMount(editor: Monaco.editor.IStandaloneCodeEditor) {
	// 在这里就拿到了 editor 实例,可以存到 ref 里后面继续用
  }
  return (
    <>
      <div style={{ width: '600px', height: '450px' }}>
        <Editor
		    onChange={(value) => setCode(value!)}
		    theme='vs-dark'
	        value=[code]
	        language='python'
	        onMount={handleEditorDidMount}
	        options={{ glyphMargin: true }} // 加一行设置Monaco展示边距,避免遮盖行号
        />
      </div>
    </>
  )
}

export default App

断点装饰器样式:

.breakpoints {
    width: 10px !important;
    height: 10px !important;
    left: 5px !important;
    top: 5px;
    border-radius: 50%;
    display: inline-block;
    cursor: pointer;
}

.breakpoints:hover {
    background-color: rgba(255, 0, 0, 0.5);
}

.breakpoints-active {
    width: 10px !important;
    height: 10px !important;
    left: 5px !important;
    top: 5px;
    background-color: red;
    border-radius: 50%;
    display: inline-block;
    cursor: pointer;
    z-index: 5;
}

整理下代码:

  const bpOption = {
    isWholeLine: true,
    linesDecorationsClassName: 'breakpoints',
    linesDecorationsTooltip: '点击添加断点',
  }

  const activeBpOption = {
    isWholeLine: true,
    linesDecorationsClassName: 'breakpoints-active',
    linesDecorationsTooltip: '点击移除断点',
  }

  function handleEditorDidMount(editor: Monaco.editor.IStandaloneCodeEditor) {
    const activeCollections: Monaco.editor.IModelDeltaDecoration[] = []
    const collections: Monaco.editor.IModelDeltaDecoration[] = [
      {
        range: new Monaco.Range(1, 1, 9999, 1),
        options: bpOption,
      },
    ]

    const bpc = editor.createDecorationsCollection(collections)
    const activeBpc = editor.createDecorationsCollection(activeCollections)

    editor.onMouseDown((e) => {
      // 加断点
      if (e.event.target.classList.contains('breakpoints')) {
        const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)
        const acc: Monaco.editor.IModelDeltaDecoration[] = []
        activeBpc
          .getRanges()
          .filter((item, index) => activeBpc.getRanges().indexOf(item) === index) // 去重
          .forEach((erange) => {
            acc.push({
              range: erange,
              options: activeBpOption,
            })
          })
        acc.push({
          range: new Monaco.Range(lineNum, 1, lineNum, 1),
          options: activeBpOption,
        })
        activeBpc.set(acc)
      }
      
      // 删断点
      if (e.event.target.classList.contains('breakpoints-active')) {
        const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)
        const acc: Monaco.editor.IModelDeltaDecoration[] = []
        activeBpc
          .getRanges()
          .filter((item, index) => activeBpc.getRanges().indexOf(item) === index)
          .forEach((erange) => {
            if (erange.startLineNumber !== lineNum)
              acc.push({
                range: erange,
                options: activeBpOption,
              })
          })
        activeBpc.set(acc)
      }
    })

    // 内容变动时更新装饰器①
    editor.onDidChangeModelContent(() => {
      bpc.set(collections)
    })
  }

看起来基本没有什么问题了。

注意到空行换行时的内部处理策略是跟随断点,可以在内容变动时进一步清洗。

    editor.onDidChangeModelContent(() => {
      bpc.set(collections)
      const acc: Monaco.editor.IModelDeltaDecoration[] = []
      activeBpc
        .getRanges()
        .filter((item, index) => activeBpc.getRanges().indexOf(item) === index)
        .forEach((erange) => {
          acc.push({
            range: new Monaco.Range(erange.startLineNumber, 1, erange.startLineNumber, 1), // here
            options: {
              isWholeLine: true,
              linesDecorationsClassName: 'breakpoints-active',
              linesDecorationsTooltip: '点击移除断点',
            },
          })
        })
      activeBpc.set(acc)
      props.onBpChange(activeBpc.getRanges())
    })

完整实现

App.tsx:

import Editor from '@monaco-editor/react'
import * as Monaco from 'monaco-editor/esm/vs/editor/editor.api'
import './App.css'
import { useState } from 'react'
import './editor-style.css'

function App() {
  const [code, setCode] = useState('# some code here...')

  const bpOption = {
    isWholeLine: true,
    linesDecorationsClassName: 'breakpoints',
    linesDecorationsTooltip: '点击添加断点',
  }

  const activeBpOption = {
    isWholeLine: true,
    linesDecorationsClassName: 'breakpoints-active',
    linesDecorationsTooltip: '点击移除断点',
  }

  function handleEditorDidMount(editor: Monaco.editor.IStandaloneCodeEditor) {
    const activeCollections: Monaco.editor.IModelDeltaDecoration[] = []
    const collections: Monaco.editor.IModelDeltaDecoration[] = [
      {
        range: new Monaco.Range(1, 1, 9999, 1),
        options: bpOption,
      },
    ]

    const bpc = editor.createDecorationsCollection(collections)
    const activeBpc = editor.createDecorationsCollection(activeCollections)

    editor.onMouseDown((e) => {
      // 加断点
      if (e.event.target.classList.contains('breakpoints')) {
        const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)
        const acc: Monaco.editor.IModelDeltaDecoration[] = []
        activeBpc
          .getRanges()
          .filter((item, index) => activeBpc.getRanges().indexOf(item) === index) // 去重
          .forEach((erange) => {
            acc.push({
              range: erange,
              options: activeBpOption,
            })
          })
        acc.push({
          range: new Monaco.Range(lineNum, 1, lineNum, 1),
          options: activeBpOption,
        })
        activeBpc.set(acc)
      }
      // 删断点
      if (e.event.target.classList.contains('breakpoints-active')) {
        const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)
        const acc: Monaco.editor.IModelDeltaDecoration[] = []
        activeBpc
          .getRanges()
          .filter((item, index) => activeBpc.getRanges().indexOf(item) === index)
          .forEach((erange) => {
            if (erange.startLineNumber !== lineNum)
              acc.push({
                range: erange,
                options: activeBpOption,
              })
          })
        activeBpc.set(acc)
      }
    })

    // 内容变动时更新装饰器①
    editor.onDidChangeModelContent(() => {
      bpc.set(collections)
    })
  }
  return (
    <>
      <div style={{ width: '600px', height: '450px' }}>
        <Editor
          onChange={(value) => {
            setCode(value!)
          }}
          theme='vs-dark'
          value=[code]
          language='python'
          onMount={handleEditorDidMount}
          options={{ glyphMargin: true, folding: false }}
        />
      </div>
    </>
  )
}

export default App

editor-style.css:

.breakpoints {
  width: 10px !important;
  height: 10px !important;
  left: 5px !important;
  top: 5px;
  border-radius: 50%;
  display: inline-block;
  cursor: pointer;
}

.breakpoints:hover {
  background-color: rgba(255, 0, 0, 0.5);
}

.breakpoints-active {
  width: 10px !important;
  height: 10px !important;
  left: 5px !important;
  top: 5px;
  background-color: red;
  border-radius: 50%;
  display: inline-block;
  cursor: pointer;
  z-index: 5;
}

以上就是在Monaco Editor中实现断点设置的方法详解的详细内容,更多关于Monaco Editor断点设置的资料请关注程序员之家其它相关文章!

相关文章

  • JS判断数组四种实现方法详解

    JS判断数组四种实现方法详解

    这篇文章主要介绍了JS判断数组四种实现方法详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-06-06
  • laydate时间日历插件使用方法详解

    laydate时间日历插件使用方法详解

    这篇文章主要为大家详细介绍了laydate时间日历插件的使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-11-11
  • JS实现对json对象排序并删除id相同项功能示例

    JS实现对json对象排序并删除id相同项功能示例

    这篇文章主要介绍了JS实现对json对象排序并删除id相同项功能,涉及javascript针对json格式数据的遍历、运算、判断、添加、删除等相关操作技巧,需要的朋友可以参考下
    2018-04-04
  • 深入学习Bootstrap表单

    深入学习Bootstrap表单

    这篇文章主要为大家详细介绍了Bootstrap表单的基础知识,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12
  • 微信小程序授权登陆及每次检查是否授权实例代码

    微信小程序授权登陆及每次检查是否授权实例代码

    这篇文章主要介绍了关于微信小程序授权登陆及每次检查是否授权,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-09-09
  • js自定义Tab选项卡效果

    js自定义Tab选项卡效果

    这篇文章主要为大家详细介绍了js自定义Tab选项卡效果,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2017-06-06
  • JavaScript获取服务器时间的方法详解

    JavaScript获取服务器时间的方法详解

    这篇文章主要介绍了JavaScript获取服务器时间的方法,结合实例形式详细分析了javascript基于ajax获取服务器时间的相关操作技巧,需要的朋友可以参考下
    2016-12-12
  • js实现按钮开关单机下拉菜单效果

    js实现按钮开关单机下拉菜单效果

    这篇文章主要介绍了js实现按钮开关单机下拉菜单效果,代码简单易懂,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-11-11
  • 原生JavaScript创建不可变对象的方法简单示例

    原生JavaScript创建不可变对象的方法简单示例

    这篇文章主要介绍了原生JavaScript创建不可变对象的方法,结合简单实例形式分析了基于原生JavaScript创建不可变对象的相关原理、实现方法与操作注意事项,需要的朋友可以参考下
    2020-05-05
  • JAVASCRIPT下判断IE与FF的比较简单的方式

    JAVASCRIPT下判断IE与FF的比较简单的方式

    在JAVASCRIPT当中可以通过取当前浏览器返回值来判断当前使用什么浏览器。
    2008-10-10

最新评论

?


http://www.vxiaotou.com