使用codemirror做完項目一段時間了,來進行一個功能總結,希望能幫助到小伙伴們。編輯器包括基礎功能代碼提示、格式化、搜索替換、變量跳轉,代碼字體放大縮小,注釋等,自定義功能包括在編輯器中查看圖片,插入自定義代碼塊,代碼實時高亮等。由於篇幅太長,本篇介紹代碼提示,格式化,搜索替換功能。其它的功能依次見下篇文章~
1)基礎的引入
- html代碼
<textarea ref="code"></textarea>
- javascript代碼
1 const CodeMirror = require("codemirror/lib/codemirror"); // 后續要使用CodeMIrror,故使用require方式引入 2 import "codemirror/lib/codemirror.css"; 3 import "codemirror/mode/xml/xml"; // xml編輯器模式 4 import "codemirror/theme/monokai.css"; // 主題 5 this.editor = CodeMirror.fromTextArea(this.$refs.code, { 6 mode: "application/xml", 7 lineNumbers: true, // 顯示行號 8 styleActiveLine: true, // 高亮當前行 9 theme: "monokai", // 主題 10 }
2)代碼提示功能
實現代碼提示功能首先要定義需要提示的代碼代碼文件,然后需要設置給編輯器
- 提示的代碼:
1 export const tags = { 2 "!attrs": { // 若tag標簽里邊沒有設置attr屬性,則所有的標簽自帶id和class屬性,此為自定義項,非必須 3 id: null, 4 class: ["A", "B", "C"] 5 }, 6 Button: { 7 attrs: { 8 x: null, 9 y: null, 10 w: null, 11 h: null, 12 align: ["center", "left", "right"], 13 alignV: ["center", "top", "bottom"], 14 Visibility: null 15 } 16 } 17 }
- 觸發條件定義
1 CodeMirror.commands.completeAfter = function (cm, pred) { 2 if (!pred || pred()) setTimeout(function () { 3 if (!cm.state.completionActive) 4 cm.showHint({ completeSingle: false }); 5 }, 100); 6 return CodeMirror.Pass; 7 }; 8 CodeMirror.commands.completeIfAfterLt = function (cm) { 9 return CodeMirror.commands.completeAfter(cm, function () { 10 var cur = cm.getCursor(); 11 return cm.getRange(CodeMirror.Pos(cur.line, cur.ch - 1), cur) == "<"; 12 }); 13 }; 14 CodeMirror.commands.completeIfInTag = function (cm) { 15 return CodeMirror.commands.completeAfter(cm, function () { 16 var tok = cm.getTokenAt(cm.getCursor()); 17 var reg = /[""]/; 18 if (tok.type == "string" && (!reg.test(tok.string.charAt(tok.string.length - 1)) || tok.string.length == 1)) return false; 19 var inner = CodeMirror.innerMode(cm.getMode(), tok.state).state; 20 return inner.tagName; 21 }); 22 };
- javascript代碼
import "codemirror/addon/hint/show-hint.css"; import "codemirror/addon/hint/show-hint.js"; import "codemirror/addon/hint/xml-hint.js"; this.editor = CodeMirror.fromTextArea(this.$refs.code, { extraKeys: { "'<'": "completeAfter", "'/'": "completeIfAfterLt", "' '": "completeIfInTag", "'='": "completeIfInTag", } } this.editor.setOption("hintOptions", { schemaInfo: this.tags })
3)格式化
- 格式化定義
1 CodeMirror.defineExtension("autoFormatRange", function (from, to) { 2 var cm = this; 3 var outer = cm.getMode(), 4 text = cm.getRange(from, to).split("\n"); 5 var state = CodeMirror.copyState(outer, cm.getTokenAt(from).state); 6 var tabSize = cm.getOption("tabSize"); 7 8 var out = "", 9 lines = 0, 10 atSol = from.ch == 0; 11 12 function newline() { 13 out += "\n"; 14 atSol = true; 15 ++lines; 16 } 17 18 for (var i = 0; i < text.length; ++i) { 19 var stream = new CodeMirror.StringStream(text[i], tabSize); 20 while (!stream.eol()) { 21 var inner = CodeMirror.innerMode(outer, state); 22 var style = outer.token(stream, state), 23 cur = stream.current(); 24 stream.start = stream.pos; 25 if (!atSol || /\S/.test(cur)) { 26 out += cur; 27 atSol = false; 28 } 29 if ( 30 !atSol && 31 inner.mode.newlineAfterToken && 32 inner.mode.newlineAfterToken( 33 style, 34 cur, 35 stream.string.slice(stream.pos) || text[i + 1] || "", 36 inner.state 37 ) 38 ) 39 newline(); 40 } 41 if (!stream.pos && outer.blankLine) outer.blankLine(state); 42 if (!atSol) newline(); 43 } 44 45 cm.operation(function () { 46 cm.replaceRange(out, from, to); 47 for (var cur = from.line + 1, end = from.line + lines; cur <= end; ++cur) { 48 cm.indentLine(cur, "smart") 49 } 50 }); 51 });
- javascript代碼
1 this.editor = CodeMirror.fromTextArea(this.$refs.code, { 2 extraKeys: { 3 "Shift-Tab": function () { // 選中部分格式化 4 that.formatSelection() 5 }, 6 "Shift-Alt": function () { // 全文格式化,快捷鍵可自定義,根據具體業務此處格式化的同時要處理其它邏輯,故把此處代碼提出來寫 7 that.format() 8 } 9 } 10 } 11 // 選中部分格式化 indentAuto為codemirror自帶api 12 formatSelection() { 13 CodeMirror.commands.indentAuto(this.editor); 14 }, 15 // 格式化 16 format() { 17 this.editor.autoFormatRange({ line: 0, ch: 0 }, { line: this.editor.lineCount() }); 18 }
4)搜索替換
- 搜索替換框
首先附上codemirror自帶搜索替換功能demo地址, https://codemirror.net/demo/search.html ,此種方式完全不符合現在編輯器的操作習慣,經過查詢多方資料,發現一個插件cm-searchbox,此插件的樣式是符合現代編輯器的,附圖 ,但是此插件沒有高亮和計算當前匹配第幾個和一共匹配多少個的功能,我們就在此基礎上完善功能吧!
- 功能實現,cm-search插件中的代碼全部保留,以下都是分析源代碼之后新添加的代碼
為了計算匹配個數,我們需要在最開始的地方引入StringStream對象
1 const StringStream = CodeMirror.StringStream
在每次需要搜索替換時獲取state並調用startSearch方法
1 var state = getSearchState(cm); 2 function SearchState() { 3 this.posFrom = this.posTo = this.lastQuery = this.query = null; 4 this.overlay = null; 5 } 6 function getSearchState(cm) { 7 return cm.state.search || (cm.state.search = new SearchState()); 8 }
1 this.startSearch = function (cm, state, query) { 2 state.query = parseQuery(query); 3 if (self.caseSensitiveOption.checked == undefined) { // caseSensitiveOption.checked=true 開啟區分大小寫 4 self.caseSensitiveOption.checked = false 5 } 6 if (self.wholeWordOption.checked == undefined) { // caseSensitiveOption.checked=true 開啟全字匹配 7 self.wholeWordOption.checked = false 8 } 9 if (self.regExpOption.checked == undefined) { // regExpOption.checked=true 開啟正則 10 self.regExpOption.checked = false 11 } 12 cm.removeOverlay(state.overlay, self.caseSensitiveOption.checked); 13 state.overlay = searchOverlay( 14 state.query, self.caseSensitiveOption.checked, self.wholeWordOption.checked, self.regExpOption.checked); 15 cm.addOverlay(state.overlay, { 16 opaque: true 17 }); 18 }
在startSearch中引用了searchOverlay和parseQuery方法,用於高亮和匹配個數計算
function searchOverlay(query, caseSensitive, wholeWord, regExp) { if (regExp || wholeWord) { if (wholeWord) query = '\\b' + query + '\\b'; query = RegExp(query); } if (typeof query == "string") query = new RegExp(query.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"), caseSensitive ? "g" : "gi"); else if (!query.global) query = new RegExp(query.source, caseSensitive ? "g" : "gi"); // 獲取匹配個數以及當前匹配index,totalMatch為總匹配個數,curMatchIndex為當前匹配索引 totalMatch = 0 var current = cm.listSelections()[0] cm.eachLine((line) => { var stream = new StringStream(line.text, cm.options.tabSize) var match = query.exec(stream.string); var lineNumber = cm.getLineNumber(line) while (match) { totalMatch += 1 if (lineNumber == current.anchor.line && match.index == current.anchor.ch) { curMatchIndex = totalMatch } stream = new StringStream(line.text, cm.options.tabSize) match = query.exec(stream.string); // 從字符串出現的位置的下一位置開始繼續查找 } })
if (totalMatch) {
aceTotal.innerHTML = totalMatch + "中的" + curMatchIndex
} else {
aceTotal.innerHTML = "無結果"
}
// 給匹配到的添加class值cm-searching,實現代碼高亮 return { token: function (stream) { query.lastIndex = stream.pos; var match = query.exec(stream.string); if (match && match.index == stream.pos) { stream.pos += match[0].length || 1; return "searching"; } else if (match) { stream.pos = match.index; } else { stream.skipToEnd(); } } }; } function parseString(string) { return string.replace(/\\(.)/g, function (_, ch) { if (ch == "n") return "\n"; if (ch == "r") return "\r"; return ch; }); }; function parseQuery(query) { if (query.exec) { return query; } var isRE = query.indexOf('/') === 0 && query.lastIndexOf('/') > 0; if (!!isRE) { try { var matches = query.match(/^\/(.*)\/([a-z]*)$/); query = new RegExp(matches[1], matches[2].indexOf("i") == -1 ? "" : "i"); } catch (e) { } // Not a regular expression after all, do a string search } else { query = parseString(query); } if (typeof query == "string" ? query == "" : query.test("")) query = /x^/; return query; }
希望小伙伴們多多指正哈~