monaco editor各種功能實現總結


 

我使用的vue,以下是Editor.vue部分代碼,只顯示了初始化部分。monaco.editor.create方法生成了一個新的編輯器對象,第一個參數是html對象,第二個是options,里面有很多參數,這里只隨便設置了兩個:主題和自適應layout,接下來將使用這里定義的this.editor對象進行操作,下面提到的方法都定義在methods對象里面(注意由於定義在對象里面,所以下面的所有方法都沒有function標志), css式樣都定義在<style></style>里面。

<template>
<div ref="main" style="width: 100%;height: 100%;margin-left: 5px;"></div>
</template>

<script>
import * as monaco from 'monaco-editor/esm/vs/editor/editor.main.js'
import 'monaco-editor/esm/vs/basic-languages/javascript/javascript.contribution'
import { StandaloneCodeEditorServiceImpl } from 'monaco-editor/esm/vs/editor/standalone/browser/standaloneCodeServiceImpl.js'
export default {
name: 'Editor',
data () {
return {
editor: null,
//黑色主題,vs是白色主題,我喜歡黑色
curTheme: 'vs-dark'
}
},
methods: {},
mounted () {
//注意這個初始化沒有指定model,可以自己創建一個model,然后使用this.editor.setModel設置進去
//創建model時指定uri,之后可以通過monaco.editor.getModel(uri)獲取指定的model
//沒有設置model的話,接下來的代碼沒有辦法執行
this.editor = monaco.editor.create(this.$refs.main, {theme: this.curTheme, automaticLayout: true})
}
</script>
<style>
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

  

1、添加刪除斷點
需要注意的是,刪除斷點的操作我之前不是這么寫的,而是在添加斷點的操作let ids = model.deltaDecorations([], [value])有一個返回值是添加的斷點的Id集合,我將該集合按照每個model分類存了起來,然后在刪除的時候直接操作model.deltaDecorations(ids, []),剛開始並沒有發現問題是好用的,然而,后來發現當刪除大段多行的文字,並且這些文字里面包含好幾個斷點的時候,斷點會堆積到最上面,視覺上只有一個斷點,但是其實是很多個斷點疊加在一起,效果就是運行removeBreakpoint時候沒有反應,並且換行的時候,下面一行也會出現斷點。后來通過監控model的內容change事件將多余的breakpoint刪除了,但是為了防止萬一,刪除斷點的方法也改成了下面這種復雜的方法。

//添加斷點
async addBreakPoint (line) {
let model = this.editor.getModel()
if (!model) return
let value = {range: new monaco.Range(line, 1, line, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints' }}
model.deltaDecorations([], [value])
},
//刪除斷點,如果指定了line,刪除指定行的斷點,否則刪除當前model里面的所有斷點
async removeBreakPoint (line) {
let model = this.editor.getModel()
if (!model) return
let decorations
let ids = []
if (line !== undefined) {
decorations = this.editor.getLineDecorations(line)
} else {
decorations = this.editor.getAllDecorations()
}
for (let decoration of decorations) {
if (decoration.options.linesDecorationsClassName === 'breakpoints') {
ids.push(decoration.id)
}
}
if (ids && ids.length) {
model.deltaDecorations(ids, [])
}
},
//判斷該行是否存在斷點
hasBreakPoint (line) {
let decorations = this.editor.getLineDecorations(line)
for (let decoration of decorations) {
if (decoration.options.linesDecorationsClassName === 'breakpoints') {
return true
}
}
return false
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
這段css是控制breakpoint的樣式的,我是個css小白,將就着看吧,,,,

<style>
.breakpoints{
background: red;
background: radial-gradient(circle at 3px 3px, white, red);
width: 10px !important;
height: 10px !important;
left: 0px !important;
top: 3px;
border-radius: 5px;
}
</style>
1
2
3
4
5
6
7
8
9
10
11

  

這段代碼是為了解決breakpoint堆積的問題,監聽了ChangeModelContent事件,在內容發生改變之后進行相應的處理。(添加在mounted中editor初始化之后)

this.editor.onDidChangeModelContent((e) => {
let model = this.editor.getModel()
//必須在nextTick處理,不然getPosition返回的位置有問題
this.$nextTick(() => {
//獲取當前的鼠標位置
let pos = this.editor.getPosition()
if (pos) {
//獲取當前的行
let line = pos.lineNumber
//如果當前行的內容為空,刪除斷點(空行不允許設置斷點,我自己規定的,,,)

if (this.editor.getModel().getLineContent(line).trim() === '') {
this.removeBreakPoint(line)
} else {
//如果當前行存在斷點,刪除多余的斷點只保留一個
if (this.hasBreakPoint(line)) {
this.removeBreakPoint(line)
this.addBreakPoint(line)
}
}
}
})
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

  


最后的breakpoint的效果圖大概如下:

到之前為止,我們只是定義了添加刪除breakpoint的方法,你可以在代碼里面調用方法進行添加刪除breakpoint的操作,但是實際上大多編輯器都是通過點擊指定行的方式添加breakpoint的,為了達到點擊添加的目的,我們需要監聽一下MouseDown事件,添加相應的操作:

this.editor.onMouseDown(e => {
//我建立了很多不同種類的編輯器js, text等,這里只允許js編輯器添加breakpoint,如果你想在mousedown里面做點別的,放在這個前面啊,否則,return了,,,,
if (!this.isJsEditor()) return
//這里限制了一下點擊的位置,只有點擊breakpoint應該出現的位置,才會創建,其他位置沒反應
if (e.target.detail && e.target.detail.offsetX && e.target.detail.offsetX >= 0 && e.target.detail.offsetX <= 10) {
let line = e.target.position.lineNumber
//空行不創建

if (this.editor.getModel().getLineContent(line).trim() === '') {
return
}

  


//如果點擊的位置沒有的話創建breakpoint,有的話,刪除

if (!this.hasBreakPoint(line)) {
this.addBreakPoint(line)
} else {
this.removeBreakPoint(line)
}

  


//如果存在上個位置,將鼠標移到上個位置,否則使editor失去焦點

if (this.lastPosition) {
this.editor.setPosition(this.lastPosition)
} else {
document.activeElement.blur()
}
}

  


//更新lastPosition為當前鼠標的位置(只有點擊編輯器里面的內容的時候)

if (e.target.type === 6 || e.target.type === 7) {
this.lastPosition = this.editor.getPosition()
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
isJsEditor () {
return this.editor.getModel().getLanguageIdentifier().language === 'javascript'
}
1
2
3

  


上述的代碼最下面的部分設置位置那部分,其實和設置斷點沒有關系,我只是覺得,點擊的時候會改變鼠標的位置特別不科學,於是自己處理了一下位置,可以刪除的。 另外e.target.type這個主要是判斷點擊的位置在哪里,這里6,7表示是編輯器里面的內容的位置,具體可以參考官方文檔。以下截圖是從官方文檔截得:

到上面為止,添加斷點部分基本上完成了,但是我使用了一下vscode(它使用monaco editor做的編輯器),發現人家在鼠標移動到該出現breakpoint的時候會出現一個半透明的圓點,表示點擊這個位置可以出現breakpoint?或者表示breakpoint應該出現在這個位置?不管它什么原因,我覺得我也應該有。
注意啊,這里因為鼠標移開就刪除了,所以完全沒有刪除真的breakpoint時那樣麻煩。

//添加一個偽breakpoint

addFakeBreakPoint (line) {
if (this.hasBreakPoint(line)) return
let value = {range: new monaco.Range(line, 1, line, 1), options: { isWholeLine: true, linesDecorationsClassName: 'breakpoints-fake' }}
this.decorations = this.editor.deltaDecorations(this.decorations, [value])
},

  


//刪除所有的偽breakpoint

removeFakeBreakPoint () {
this.decorations = this.editor.deltaDecorations(this.decorations, [])
}
1
2
3
4
5
6
7
8
9
10

  


這個是css樣式,一個半透明的圓點

<style>
.breakpoints-fake{
background: rgba(255, 0, 0, 0.2);
width: 10px !important;
height: 10px !important;
left: 0px !important;
top: 3px;
border-radius: 5px;
}
</style>
1
2
3
4
5
6
7
8
9
10

  

最后添加mouse相關的事件監聽:

this.editor.onMouseMove(e => {
if (!this.isJsEditor()) return
this.removeFakeBreakPoint()
if (e.target.detail && e.target.detail.offsetX && e.target.detail.offsetX >= 0 && e.target.detail.offsetX <= 10) {
let line = e.target.position.lineNumber
this.addFakeBreakPoint(line)
}
})
this.editor.onMouseLeave(() => {
this.removeFakeBreakPoint()
})
//這個是因為鼠標放在breakpoint的位置,然后焦點在editor里面,點擊enter的話,出現好多偽breakpoint,emmmm,我也不知道怎么回事,沒辦法,按enter鍵的話,強制刪除所有的偽breakpoint
this.editor.onKeyDown(e => {
if (e.code === 'Enter') {
this.removeFakeBreakPoint()
}
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

  


好吧,大概就可以用了,實際使用可能會有更多問題,具體問題具體分析,慢慢解決吧,我真的覺得這個部分簡直全是問題,,,,添加個斷點真不容易,其實我推薦自己做斷點,不用它的破decoration,,,,

2、插入文本
在當前鼠標的位置插入指定文本的代碼如下,比較麻煩,但是也沒有太多代碼,如果你已經選定了一段代碼的話,應該會替換當前選中的文本。

insertContent (text) {
if (this.editor) {
let selection = this.editor.getSelection()
let range = new monaco.Range(selection.startLineNumber, selection.startColumn, selection.endLineNumber, selection.endColumn)
let id = { major: 1, minor: 1 }
let op = {identifier: id, range: range, text: text, forceMoveMarkers: true}
this.editor.executeEdits(this.root, [op])
this.editor.focus()
}
}
1
2
3
4
5
6
7
8
9
10

  


3、手動觸發Action
這個方法特別簡單也沒有,但是關鍵是你得知道Action的id是什么,,,你問我怎么知道的,我去看的源碼。
很坑有沒有,不過我通過看源碼發現了一個可以調用的方法require('monaco-editor/esm/vs/editor/browser/editorExtensions.js').EditorExtensionsRegistry.getEditorActions()這個結果是一個Action數組,包括注冊了的Action的各種信息,當然也包括id。(ps: trigger的第一個參數沒發現有什么用,就都用anything代替了)

trigger (id) {
if (!this.editor) return
this.editor.trigger('anyString', id)
}
1
2
3
4

  


舉個例子,format document的Action對象大概就是下面這個樣子,我們可以通過trigger('editor.action.formatDocument')觸發格式化文件的功能。

{
"id": "editor.action.formatDocument",
"precondition": {
"key": "editorReadonly"
},
"_kbOpts": {
"kbExpr": {
"key": "editorTextFocus",
"_defaultValue": false
},
"primary": 1572,
"linux": {
"primary": 3111
},
"weight": 100
},
"label": "Format Document",
"alias": "Format Document",
"menuOpts": {
"when": {
"key": "editorHasDocumentFormattingProvider",
"_defaultValue": false
},
"group": "1_modification",
"order": 1.3
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

  


4、多model支持轉到定義和查找引用
這個之前出過很多錯誤,網上的搜到的很多答案根本不好用,為了弄明白為啥不好用我還去閱讀了相關的源碼,下面說一下好用的版本:

//這個函數是從網上找的,用於自定義一個TextModelService,替換原先的

getTextModelService () {
return {
createModelReference (uri) {
const model = {
load () {
return Promise.resolve(model)
},
dispose () {
},
textEditorModel: monaco.editor.getModel(uri)
}
return Promise.resolve({
object: model,
dispose () {
}
})
}
}
},

  


//這個兩個方法是為了替換CodeEditorService,可以看出和上面的實現不一樣,區別在哪里呢
//本來也是打算按照上面的方法來做的,但是也看到了上面的方法需要定義各種需要用到的方法,你得很理解這個Service才可以自己定義啊
//這個就不需要了,只通過原型修改了兩個相關的方法,然后其他的就不需要關心了
//上面的好處是在創建editor的時候使用上面的service代替,只影響替換了的editor,下面這個直接影響了所有的editor
//具體使用什么方法可以自己考量,我這個service采用了這種方法,主要是因為自定義的service各種報錯,失敗了,,,

initGoToDefinitionCrossModels () {
let self = this
StandaloneCodeEditorServiceImpl.prototype.findModel = function (editor, resource) {
let model = null
if (resource !== null) {
model = monaco.editor.getModel(resource)
}
return model
}

StandaloneCodeEditorServiceImpl.prototype.doOpenEditor = function (editor, input) {
//這個this.findModel調用的是StandaloneCodeEditorServiceImpl.prototype.findModel這個方法
let model = this.findModel(editor, input.resource)
if (model) {
editor.setModel(model)
} else {
return null
}
let selection = input.options.selection
if (selection) {
if (typeof selection.endLineNumber === 'number' && typeof selection.endColumn === 'number')
editor.setSelection(selection)
editor.revealRangeInCenter(selection, 1 /* Immediate */)
} else {
let pos = {
lineNumber: selection.startLineNumber,
column: selection.startColumn
}
editor.setPosition(pos)
editor.revealPositionInCenter(pos, 1 /* Immediate */)
}
editor.focus()
}
return editor
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

  

initGoToDefinitionCrossModels這個方法需要在mounted里面調用一下,不然什么都不會發生。然后創建editor的方法也要修改一下:

//第三個參數表示使用指定的service替換默認的

this.editor = monaco.editor.create(this.$refs.main, {
theme: this.curTheme,
automaticLayout: true
}, {
textModelService: this.getTextModelService()
})
1
2
3
4
5
6
7

  


之前網上有推薦使用new StandaloneCodeEditorServiceImpl()生成一個codeEditorService,然后像替換textModelService一樣替換codeEditorService的,親測不好用,new這個操作里面有一些額外的操作,並不可以,想要替換的話,個人認為應該如textModelService一樣,自己定義一個對象(可以讀讀源碼了解一下需要實現的方法)。
完成了以上內容,再執行右鍵-》go to definition就可以跳到定義了,其他如peek definition和find all references都可以正常執行了。

5、全局搜索
monaco編輯器支持單個model內部的搜索,mac快捷鍵是cmd+f,沒有找到全局的搜索,如果我們想在打開的文件夾下面的每個model里面進行搜索的話,需要自己操作一下:

findAllMatches (searchText) {
let result = {}
if (searchText) {
//注意如果你一個model都沒有注冊的話,這里什么都拿不到
//舉個例子啊,下面將一個路徑為filePath,語言為lang,文件內容為fileContent的本地文件注冊為model

//monaco.editor.createModel(fileContent, lang, monaco.Uri.file(filePath))
monaco.editor.getModels().forEach(model => {
result[model.uri.toString()] = []
for (let match of model.findMatches(searchText)) {
result[model.uri.toString()].push({
text: model.getLineContent(match.range.startLineNumber),
range: match.range,
model: model
})
}
})
}
return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

  


上面的方法返回的是monaco.editor里面注冊過的每個model對應的搜索對象,包括當前行的文本,目標對象的范圍,和model對象。返回的結果可以用於顯示,如果想要點擊指定的文本跳到對應的model的話,需要做如下操作:

//這里range和model,對應findAllMatches返回結果集合里面對象的range和model屬性

goto (range, model) {
//設置model,如果是做編輯器的話,打開了多個文本,還會涉及到標簽頁的切換等其他細節,這里不考慮這些
this.editor.setModel(model)
//選中指定range的文本
this.editor.setSelection(range)
//把選中的位置放到中間顯示
this.editor.revealRangeInCenter(range)
}
1
2
3
4
5
6
7
8
9

  


6、Git新舊版本比較使用DiffEditor
async showDiffEditor (filePath, language) {
//這個方法是我自己定義的,因為用於顯示git的修改對比,所以是使用的git命令獲取的相關的原始文本
let oriText = await git.catFile(filePath)
let originalModel = monaco.editor.createModel(oriText, language)
//修改后的文本這里在打開文件之前我都初始化好了,所以可以直接通過該方法獲得,沒有提前創建好的話,可以參照上面的例子創建

let modifiedModel = monaco.editor.getModel(monaco.Uri.file(filePath))

if (!this.diffEditor) {
//創建一個diffEditor,readOnly表示只讀,this.$refs.main是html對象
this.diffEditor = monaco.editor.createDiffEditor(this.$refs.main, {
enableSplitViewResizing: false,
automaticLayout: true,
readOnly: true
})
}

this.diffEditor.setModel({
original: originalModel,
modified: modifiedModel
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
7、添加Completions和Defaults
添加一個default對象,代碼是從官方的文檔找到的,然后自己改寫了下面的引用部分。主要作用是這么做之后,在編輯器里面輸入tools.js文件里面定義的toolUtls.之后,將會提示toString這個function,並且顯示注釋信息。感覺和competition挺像啊。

initDefaults () {
// validation settings
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false
})
// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES6,
allowNonTsExtensions: true
})
let toolsPath = path.join(__dirname, 'tools.js')
let str = require('fs').readFileSync(toolsPath).toString()
monaco.languages.typescript.javascriptDefaults.addExtraLib(str, 'tools.js')
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
tools.js文件:

let toolUtls = {
/**
* convert obj to string
*/
toString (obj) {}
}
1
2
3
4
5
6

  

至於添加completion也有官方文檔,很容易實現:

addCompletions () {
//keyMap是一個普通對象(比如:let keyMap = {Man: 1, Woman: 2})
//這樣做的好處是,假如一個方法需要的參數都是類型,但是類型使用1,2,3,4這種數字表示,你很難記住對應的類型名稱
//通過這種方式,你輸入Man的時候可以插入1 /*Man*/,參數仍然是數字,但是看起來有意義多了,輸入也比較方便
//為了key的提示更清楚,可以使用People_Man,People_Woman這種相同前綴的key值,輸入People就會提示各種type了

let suggestions = []
for (let key in keyMap) {
suggestions.push({
label: key,
kind: monaco.languages.CompletionItemKind.Enum,
insertText: keyMap[key].toString() + ` /*${key}*/`
})
}
monaco.languages.registerCompletionItemProvider('javascript', {
provideCompletionItems: () => {
return {
suggestions: suggestions
}
}
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

  


對了為了可以順利的找到worker,需要在webpack的配置文件里面添加const MonacoWebpackPlugin = require(‘monaco-editor-webpack-plugin’)定義,在plugins里面添加new MonacoWebpackPlugin(),這個其實支持參數設置的,我設置失敗了,emmm,網上的解決方案都沒能解決問題,好在刪除參數的話,啥事兒沒有,所以就這么用了。
本來還打算實現refactor功能,不過由於沒有時間,這個功能無線擱置了,如果有誰實現了,歡迎分享啊。另外,上述的實現都是我自己研究的,不排除有bug,發現bug的話,歡迎提出啊。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM