我使用的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的話,歡迎提出啊。