使用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; }
希望小伙伴们多多指正哈~