在Monaco Editor中實(shí)現(xiàn)斷點(diǎn)設(shè)置的方法詳解
Monaco Editor 是 vscode 等產(chǎn)品使用的代碼編輯器,功能強(qiáng)大(且復(fù)雜),由微軟維護(hù)。編輯器沒(méi)有原生提供設(shè)置斷點(diǎn)的功能,本文在 React + TypeScript(Vite)框架下使用 @monaco-editor/react
并介紹開(kāi)發(fā)斷點(diǎn)顯示時(shí)踩到的坑。最終展示 鼠標(biāo)懸浮顯示斷點(diǎn)按鈕,點(diǎn)擊后進(jìn)行斷點(diǎn)的設(shè)置或移除。
本文不涉及調(diào)試功能的具體實(shí)現(xiàn),可閱讀 DAP 文檔
最終實(shí)現(xiàn)可直接拉到文末。
搭建 Playground
React + TypeScript,使用的封裝為 @monaco-editor/react
。
建立項(xiàng)目并配置依賴(lài):
yarn create vite monaco-breakpoint ... yarn add @monaco-editor/react
依賴(lài)處理完成后,我們編寫(xiě)簡(jiǎn)單的代碼將編輯器展示到頁(yè)面中:(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
接下來(lái)在該編輯器的基礎(chǔ)上添加設(shè)置斷點(diǎn)的能力。
一種暴力的辦法:手動(dòng)編寫(xiě)斷點(diǎn)組件
不想閱讀 Monaco 文檔,最自然而然的想法就是手動(dòng)在編輯器的左側(cè)手搭斷點(diǎn)組件并顯示在編輯器旁邊。
首先手動(dòng)設(shè)置如下選項(xiàng)
- 編輯器高度為完全展開(kāi)(通過(guò)設(shè)置行高并指定 height 屬性)
- 禁用代碼折疊選項(xiàng)
- 為編輯器外部容器添加滾動(dòng)屬性
- 禁用小地圖
- 禁用內(nèi)部滾動(dòng)條的消費(fèi)滾動(dòng)
- 禁用超出滾動(dòng)
編輯器代碼將變?yōu)椋?/p>
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>
現(xiàn)在編輯器的滾動(dòng)由父容器的滾動(dòng)條接管,再來(lái)編寫(xiě)展示斷點(diǎn)的組件。我們希望斷點(diǎn)組件展示在編輯器的左側(cè)。 先設(shè)置父容器水平布局:
display: 'flex', flexDirection: 'row'
編寫(xiě)斷點(diǎn)組件(示例):
<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 ...
目前斷點(diǎn)組件是能夠展示了,但本文不在此方案下進(jìn)行進(jìn)一步的開(kāi)發(fā)。這個(gè)方案的問(wèn)題:
- 強(qiáng)行設(shè)置組件高度能夠展示所有代碼,把 Monaco 的性能優(yōu)化整個(gè)吃掉了。這樣的編輯器展示代碼行數(shù)超過(guò)一定量后頁(yè)面會(huì)變的非???,性能問(wèn)題嚴(yán)重。
- 小地圖、超行滾動(dòng)、代碼塊折疊等能力需要禁用,限制大。
本來(lái)目標(biāo)是少讀點(diǎn) Monaco 的超長(zhǎng)文檔,結(jié)果實(shí)際上動(dòng)了那么多編輯器的配置,最終也還是沒(méi)逃脫翻文檔的結(jié)局。果然還是要使用更聰明的辦法做斷點(diǎn)展示。
使用 Decoration
在 Monaco Editor Playground 中有使用行裝飾器的例子,一些博文也提到了可以使用 DeltaDecoration
為編輯器添加行裝飾器。不過(guò)跟著寫(xiě)實(shí)現(xiàn)的時(shí)候出現(xiàn)了一些詭異的問(wèn)題,于是查到 Monaco 的 changelog 表示該方法已被棄用,應(yīng)當(dāng)使用 createDecorationsCollection
。 這個(gè) API 的文檔在 這里。
為了能使用 editor 的方法,我們先拿到編輯器的實(shí)例(本文使用的封裝庫(kù)需要這么操作。如果你直接使用了原始的 js 庫(kù)或者其他封裝,應(yīng)當(dāng)可以用其他的方式拿到實(shí)例)。 安裝 monaco-editor js 庫(kù):
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 實(shí)例,可以存到 ref 里后面繼續(xù)用 } 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 的裝飾器是怎樣設(shè)置的?
方法 createDecorationsCollection
的參數(shù)由 IModelDeltaDecoration 指定。其含有兩個(gè)參數(shù),分別為 IModelDecorationOptions 和 IRange。Options 參數(shù)指定了裝飾器的樣式等配置信息,Range 指定了裝飾器的顯示范圍(由第幾行到第幾行等等)。
// 為從第四行到第四行的范圍(即僅第四行)添加樣式名為breakpoints的行裝飾器 const collections: Monaco.editor.IModelDeltaDecoration[] = [] collections.push({ range: new Monaco.Range(4, 1, 4, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints', linesDecorationsTooltip: '點(diǎn)擊添加斷點(diǎn)', }, }) const bpc = editor.createDecorationsCollection(collections)
該方法的返回實(shí)例提供了一系列方法,用于添加、清除、更新、查詢(xún)?cè)撗b飾器集合狀態(tài)。詳見(jiàn) IEditorDecorationsCollection。
維護(hù)單個(gè)裝飾器組:樣式表
我們先寫(xiě)一些 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; }
添加裝飾器。我們先添加一個(gè) 1 到 9999 行的范圍來(lái)查看效果(省事)。當(dāng)然更好的辦法是監(jiān)聽(tīng)行號(hào)變動(dòng)。
const collections: Monaco.editor.IModelDeltaDecoration[] = [] collections.push({ range: new Monaco.Range(1, 1, 9999, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints', linesDecorationsTooltip: '點(diǎn)擊添加斷點(diǎn)', }, }) const bpc = editor.createDecorationsCollection(collections)
裝飾器有了,監(jiān)聽(tīng)斷點(diǎn)組件的點(diǎn)擊事件。使用 Monaco 的 onMouseDown Listener。
editor.onMouseDown((e) => { if (e.event.target.classList.contains('breakpoints')) e.event.target.classList.add('breakpoints-active') })
看起來(lái)似乎沒(méi)有問(wèn)題?Monaco 的行是動(dòng)態(tài)生成的,這意味著設(shè)置的 css 樣式并不能持久顯示。
看來(lái)還得另想辦法。
維護(hù)單個(gè)裝飾器組:手動(dòng)維護(hù) Range 列表
通過(guò)維護(hù) Ranges 列表可以解決上述問(wèn)題。 不過(guò)我們顯然能注意到一個(gè)問(wèn)題:我們希望設(shè)置的斷點(diǎn)是行綁定的。
假設(shè)我們手動(dòng)維護(hù)所有設(shè)置了斷點(diǎn)的行數(shù),顯示斷點(diǎn)時(shí)若有行號(hào)變動(dòng),斷點(diǎn)將保留在原先的行號(hào)所在行。監(jiān)聽(tīng)前后行號(hào)變動(dòng)非常復(fù)雜,顯然不利于實(shí)現(xiàn)。有沒(méi)有什么辦法能夠直接利用 Monaco 的機(jī)制?
維護(hù)兩個(gè)裝飾器組
我們維護(hù)兩個(gè)裝飾器組。一個(gè)用于展示未設(shè)置斷點(diǎn)時(shí)供點(diǎn)擊的按鈕(①),另一個(gè)用來(lái)展示設(shè)置后的斷點(diǎn)(②)。行號(hào)更新后,自動(dòng)為所有行設(shè)置裝飾器①,用戶(hù)點(diǎn)擊①時(shí),獲取實(shí)時(shí)行號(hào)并設(shè)置②。斷點(diǎn)的移除和獲取均通過(guò)裝飾器組②實(shí)現(xiàn)。
- 點(diǎn)擊①時(shí),調(diào)用裝飾器組②在當(dāng)前行設(shè)置展示已設(shè)置斷點(diǎn)。
- 點(diǎn)擊②時(shí),裝飾器組②直接移除當(dāng)前行已設(shè)置的斷點(diǎn),露出裝飾器①。
如需獲取設(shè)置的斷點(diǎn)情況,可直接調(diào)用裝飾器組②的 getRanges 方法。
多個(gè)裝飾器組在編輯器里是怎樣渲染的?
兩個(gè)裝飾器都會(huì)出現(xiàn)在編輯器中,和行號(hào)在同一個(gè)父布局下。
順便折騰下怎么拿到當(dāng)前行號(hào):設(shè)置的裝飾器和展示行號(hào)的組件在同一父布局下且為相鄰元素,暫且先用一個(gè)笨辦法拿到。
// onMouseDown事件下 const lineNum = parseInt(e.event.target.nextElementSibling?.innerHTML as string)
實(shí)現(xiàn)
回到 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 實(shí)例,可以存到 ref 里后面繼續(xù)用 } return ( <> <div style={{ width: '600px', height: '450px' }}> <Editor onChange={(value) => setCode(value!)} theme='vs-dark' value=[code] language='python' onMount={handleEditorDidMount} options={{ glyphMargin: true }} // 加一行設(shè)置Monaco展示邊距,避免遮蓋行號(hào) /> </div> </> ) } export default App
斷點(diǎn)裝飾器樣式:
.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: '點(diǎn)擊添加斷點(diǎn)', } const activeBpOption = { isWholeLine: true, linesDecorationsClassName: 'breakpoints-active', linesDecorationsTooltip: '點(diǎn)擊移除斷點(diǎn)', } 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) => { // 加斷點(diǎn) 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) } // 刪斷點(diǎn) 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) } }) // 內(nèi)容變動(dòng)時(shí)更新裝飾器① editor.onDidChangeModelContent(() => { bpc.set(collections) }) }
看起來(lái)基本沒(méi)有什么問(wèn)題了。
注意到空行換行時(shí)的內(nèi)部處理策略是跟隨斷點(diǎn),可以在內(nèi)容變動(dòng)時(shí)進(jìn)一步清洗。
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: '點(diǎn)擊移除斷點(diǎn)', }, }) }) activeBpc.set(acc) props.onBpChange(activeBpc.getRanges()) })
完整實(shí)現(xiàn)
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: '點(diǎn)擊添加斷點(diǎn)', } const activeBpOption = { isWholeLine: true, linesDecorationsClassName: 'breakpoints-active', linesDecorationsTooltip: '點(diǎn)擊移除斷點(diǎn)', } 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) => { // 加斷點(diǎn) 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) } // 刪斷點(diǎn) 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) } }) // 內(nèi)容變動(dòng)時(shí)更新裝飾器① 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中實(shí)現(xiàn)斷點(diǎn)設(shè)置的方法詳解的詳細(xì)內(nèi)容,更多關(guān)于Monaco Editor斷點(diǎn)設(shè)置的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
javascript算法之?dāng)?shù)組反轉(zhuǎn)
這篇文章主要介紹了javascript算法之?dāng)?shù)組反轉(zhuǎn),文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-08-08js對(duì)字符串和數(shù)字進(jìn)行加法運(yùn)算的一些情況
這篇文章主要介紹了js對(duì)字符串和數(shù)字進(jìn)行加法運(yùn)算的一些情況,需要的朋友可以參考下2023-02-02微信小程序轉(zhuǎn)盤(pán)抽獎(jiǎng)的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了微信小程序轉(zhuǎn)盤(pán)抽獎(jiǎng)的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07用js實(shí)現(xiàn)簡(jiǎn)單算法的實(shí)例代碼
下面小編就為大家?guī)?lái)一篇用js實(shí)現(xiàn)簡(jiǎn)單算法的實(shí)例代碼。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-09-09javascript將中國(guó)數(shù)字格式轉(zhuǎn)換成歐式數(shù)字格式的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇javascript將中國(guó)數(shù)字格式轉(zhuǎn)換成歐式數(shù)字格式的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08JS構(gòu)造一個(gè)html文本內(nèi)容成文件流形式發(fā)送到后臺(tái)
本文通過(guò)實(shí)例代碼給大家介紹了JS構(gòu)造一個(gè)html文本內(nèi)容成文件流形式發(fā)送到后臺(tái)的相關(guān)資料,需要的朋友可以參考下2018-07-07Javascript點(diǎn)擊其他任意地方隱藏關(guān)閉DIV實(shí)例
這篇文章主要分享一個(gè)Javascript點(diǎn)擊其他任意地方隱藏關(guān)閉DIV實(shí)例,需要的朋友可以參考下。2016-06-06TypeScript里string和String的區(qū)別
這篇文章主要介紹了TypeScript里string和String的區(qū)別,真的不止是大小寫(xiě)的區(qū)別,string表示原生類(lèi)型,而String表示對(duì)象,下文更多詳細(xì)內(nèi)容需要的小伙伴可以參考一下2022-03-03兼容IE、FireFox、Chrome等瀏覽器的xml處理函數(shù)js代碼
JavaScript 兼容IE、FireFox、Chrome等瀏覽器的xml處理函數(shù)(xml同步/異步加載、xsl轉(zhuǎn)換、selectSingleNode、selectNodes)2011-11-11