CodeMirror(下面简称为cm)是一款基于JavaScript、面向语言的前端代码编辑器。它支持开箱即用,自带了超过100种语言的库,同时还有很多附加功能,目前得到了jetbrains等公司的支持。在这个分类下,能够与cm并驾齐驱的另一个编辑器则是ACE。由于笔者并没有使用过ACE,因此就不对两者做对比了。接下来笔者会分几个章节来具体介绍介绍cm的基本使用方法和高级功能。

CodeMirror官网:https://codemirror.net/

这里有一个在线demo,大家可以自行调整学习:

See the Pen CodeMirror Learn by jiangmitiao (@jiangmitiao) on CodePen.

1. CodeMirror引入

在cm官网使用手册中,介绍了在项目中引入cm的方式。如果是传统项目,使用cdn或者将cm包放置在项目中即可引入;如果使用了npm等包管理工具,安装cm依赖然后就可以使用了。

有人针对vue框架,封装了cm,提供了一个面向vue的cm组件:vue-codemirror,使用起来比较方便。

由于笔者使用的是vue框架,使用npm作为包管理工具,下面就拿vue-codemirror来做具体介绍,但功能与原生的使用方式基本上无差别。

vue-codemirror的npm安装命令:npm install vue-codemirror --save

安装完成后,首先需要在全局或组件下引入vue-codemirror,笔者这里演示的是在组件下引入。

js/css文件引入:

// 全局引入vue-codemirror
import {codemirror} from 'vue-codemirror';
// 引入主题 可以从 codemirror/theme/ 下引入多个
import 'codemirror/theme/idea.css'
// 引入语言模式 可以从 codemirror/mode/ 下引入多个
import 'codemirror/mode/sql/sql.js';

在组件中引入:

export default {
  components: {
    codemirror
  }
}

2. 使用Codemirror

在vue中引入cm后,接下来就可以使用了,下面是完整的例子:

<template>
  <div>
    <codemirror
      ref="cm"
      v-model="code"
      :options="cmOptions"
      @input="inputChange"
    ></codemirror>
  </div>
</template>

<script>
  // 全局引入vue-codemirror
  import {codemirror} from 'vue-codemirror';
  // 引入css文件
  import 'codemirror/lib/codemirror.css'
  // 引入主题 可以从 codemirror/theme/ 下引入多个
  import 'codemirror/theme/idea.css'
  // 引入语言模式 可以从 codemirror/mode/ 下引入多个
  import 'codemirror/mode/sql/sql.js';

  export default {
    name: 'Simple',
    components: {codemirror},
    data() {
      return {
        code: 'select a from table1 where b = 1',
        cmOptions: {
          // 语言及语法模式
          mode: 'text/x-sql',
          // 主题
          theme: 'idea',
          // 显示函数
          line: true,
          lineNumbers: true,
          // 软换行
          lineWrapping: true,
          // tab宽度
          tabSize: 4,
        }
      }
    },
    methods: {
      inputChange(content) {
        this.$nextTick(() => {
          console.log("code:" + this.code);
          console.log("content:" + content)
        });
      },
    },
  }
</script>

在上边这个例子中,你已经能够在页面中展示编辑器,并且很容易地获取到用户输入。

在这里笔者做一个小提示,v-model是vue的语法糖,vue将v-model的值设置到对应组件的value属性上,并在这个组件上设置一个input事件的监听,将input事件返回的数据绑定到v-model的值上。实际上cm设置数据是执行cmInstance.setValue(value)这个方法,vue-codemirror在组件初始化时,从code/value/content属性中获取数据,并且绑定cm的change方法,在cm的内容发生改变时,抛出一个input事件,附带的值通过cmInstance.getValue()方法得到。

因此,你可以使用下面两种代码,得到的效果和上边的代码一致。

分离写入和读取,不使用vue-codemirror自带的value绑定特效:

<template>
  <div>
    <!-- 下面的:code可以用:value或:content代替 -->
    <codemirror
      ref="cm"
      :code="code"
      :options="cmOptions"
      @input="inputChange"
    ></codemirror>
  </div>
</template>

<script>
  // 全局引入vue-codemirror
  import {codemirror} from 'vue-codemirror';
  // 引入css文件
  import 'codemirror/lib/codemirror.css'
  // 引入主题 可以从 codemirror/theme/ 下引入多个
  import 'codemirror/theme/idea.css'
  // 引入语言模式 可以从 codemirror/mode/ 下引入多个
  import 'codemirror/mode/sql/sql.js';

  export default {
    name: 'Show',
    components: {codemirror},
    data() {
      return {
        code: 'select a from table1 where b = 1',
        cmOptions: {
          // 语言及语法模式
          mode: 'text/x-sql',
          // 主题
          theme: 'idea',
          // 显示函数
          line: true,
          lineNumbers: true,
          // 软换行
          lineWrapping: true,
          // tab宽度
          tabSize: 4,
        }
      }
    },
    methods: {
      inputChange(content) {
        this.code = content;
        this.$nextTick(() => {
          console.log("code:" + this.code);
          console.log("content:" + content)
        });
      },
    },
  }
</script>

使用cm原生方式,在mounted阶段获得原生cm,绑定change效果:

<template>
  <div>
    <!-- 下面的:code可以用:value或:content代替 -->
    <codemirror
      ref="cm"
      :code="code"
      :options="cmOptions"
      @input="inputChange"
    ></codemirror>
  </div>
</template>

<script>
  // 全局引入vue-codemirror
  import {codemirror} from 'vue-codemirror';
  // 引入css文件
  import 'codemirror/lib/codemirror.css'
  // 引入主题 可以从 codemirror/theme/ 下引入多个
  import 'codemirror/theme/idea.css'
  // 引入语言模式 可以从 codemirror/mode/ 下引入多个
  import 'codemirror/mode/sql/sql.js';

  export default {
    name: 'Show',
    components: {codemirror},
    data() {
      return {
        code: 'select a from table1 where b = 1',
        cmOptions: {
          // 语言及语法模式
          mode: 'text/x-sql',
          // 主题
          theme: 'idea',
          // 显示函数
          line: true,
          lineNumbers: true,
          // 软换行
          lineWrapping: true,
          // tab宽度
          tabSize: 4,
        }
      }
    },
    methods: {
      inputChange(content) {
        this.$nextTick(() => {
          console.log("code:" + this.code);
          console.log("content:" + content)
        });
      },
    },
    mounted() {
      this.$refs.cm.codemirror.on("change", (cm) => {
        this.code = cm.getValue();
      })
    }
  }
</script>

2. CodeMirror高级功能

在第一节中,笔者展示了codemirror的简单使用。一般来说,在项目初期上边的简单使用已经足够了,但随着项目的发展,一般会要求在编辑器中增加一些特殊功能,例如高亮正在编辑行、搜索和替换功能、自动提示功能、样式调整等等。

cm在其官网对大多数附加高级功能都有简单介绍,笔者梳理了cm官网上的文档,对其中常用的高级功能进行了尝试。下面是汇总好的使用示例,大家可以根据自己的需要进行调整。

<template>
  <div>
    <codemirror
      ref="cm"
      v-model="code"
      :options="cmOptions"
      @input="inputChange"
    ></codemirror>
  </div>
</template>

<script>
  // 全局引入vue-codemirror
  import {codemirror} from 'vue-codemirror';
  // 引入css文件
  import 'codemirror/lib/codemirror.css'
  // 引入主题 可以从 codemirror/theme/ 下引入多个
  import 'codemirror/theme/idea.css'
  // 引入语言模式 可以从 codemirror/mode/ 下引入多个
  import 'codemirror/mode/sql/sql.js';

  // 搜索功能
  // find:Ctrl-F (PC), Cmd-F (Mac)
  // findNext:Ctrl-G (PC), Cmd-G (Mac)
  // findPrev:Shift-Ctrl-G (PC), Shift-Cmd-G (Mac)
  // replace:Shift-Ctrl-F (PC), Cmd-Alt-F (Mac)
  // replaceAll:Shift-Ctrl-R (PC), Shift-Cmd-Alt-F (Mac)
  import 'codemirror/addon/dialog/dialog.css'
  import 'codemirror/addon/dialog/dialog'
  import 'codemirror/addon/search/searchcursor'
  import 'codemirror/addon/search/search'
  import 'codemirror/addon/search/jump-to-line'
  import 'codemirror/addon/search/matchesonscrollbar'
  import 'codemirror/addon/search/match-highlighter'


  // 代码提示功能 具体语言可以从 codemirror/addon/hint/ 下引入多个
  import 'codemirror/addon/hint/show-hint.css';
  import 'codemirror/addon/hint/show-hint';
  import 'codemirror/addon/hint/sql-hint';

  // 高亮行功能
  import 'codemirror/addon/selection/active-line'
  import 'codemirror/addon/selection/selection-pointer'

  // 调整scrollbar样式功能
  import 'codemirror/addon/scroll/simplescrollbars.css'
  import 'codemirror/addon/scroll/simplescrollbars'

  // 自动括号匹配功能
  import 'codemirror/addon/edit/matchbrackets'

  // 全屏功能 由于项目复杂,自带的全屏功能一般不好使
  import 'codemirror/addon/display/fullscreen.css'
  import 'codemirror/addon/display/fullscreen'

  // 显示自动刷新
  import 'codemirror/addon/display/autorefresh'

  // 多语言支持?
  import 'codemirror/addon/mode/overlay'
  import 'codemirror/addon/mode/multiplex'


  // 代码段折叠功能
  import 'codemirror/addon/fold/foldcode'
  import 'codemirror/addon/fold/foldgutter'
  import 'codemirror/addon/fold/foldgutter.css'

  import 'codemirror/addon/fold/brace-fold'
  import 'codemirror/addon/fold/comment-fold'
  import 'codemirror/addon/fold/xml-fold.js';
  import 'codemirror/addon/fold/indent-fold.js';
  import 'codemirror/addon/fold/markdown-fold.js';
  import 'codemirror/addon/fold/comment-fold.js';

  // merge功能
  import 'codemirror/addon/merge/merge.css'
  import 'codemirror/addon/merge/merge'
  // google DiffMatchPatch
  import DiffMatchPatch from 'diff-match-patch'
  // DiffMatchPatch config with global
  window.diff_match_patch = DiffMatchPatch;
  window.DIFF_DELETE = -1;
  window.DIFF_INSERT = 1;
  window.DIFF_EQUAL = 0;


  export default {
    name: 'Show',
    components: {codemirror},
    data() {
      return {
        code: 'select a from table1 where b = 1',
        cmOptions: {
          // 语言及语法模式
          mode: 'text/x-sql',
          // 主题
          theme: 'idea',
          // 显示函数
          line: true,
          lineNumbers: true,
          // 软换行
          lineWrapping: true,
          // tab宽度
          tabSize: 4,
          // 代码提示功能
          hintOptions: {
            // 避免由于提示列表只有一个提示信息时,自动填充
            completeSingle: false,
            // 不同的语言支持从配置中读取自定义配置 sql语言允许配置表和字段信息,用于代码提示
            tables: {
              "table1": ["c1", "c2"],
            },
          },
          // 高亮行功能
          styleActiveLine: true,
          // 调整scrollbar样式功能
          scrollbarStyle: 'overlay',
          // 自动括号匹配功能
          matchBrackets: true
        }
      }
    },
    methods: {
      inputChange(content) {
        this.$nextTick(() => {
          console.log("code:" + this.code);
          console.log("content:" + content)
        });
      },
    },
    mounted() {
      // 代码提示功能 当用户有输入时,显示提示信息
      this.$refs.cm.codemirror.on('inputRead', cm => {
        cm.showHint();
      })
    }
  }
</script>

3. CodeMirror自定义代码提示

前两节介绍了cm的基本用法和高级功能,但随着项目的发展,有时候需要更进一步的定制才能满足需求。接下来介绍如何实现自定义代码提示。

3.1 自定义hint方法

在methods中自定义代码实现方法:

/**

使用自定义hint,网上没有详细的讲解,这里着重讲一下。

1. 第一个入参cmInstance指的是codeMirror实例,第二个是配置中的的hintOptions值。
2. 从cmInstance中getCursor指的是获取光标实例,光标实例里有行数、列数。
3. 可以从cmInstance的getLine方法里传入一个行数,从而获取行中的字符串。
4. token对象是cmInstance对光标所在字符串进行提取处理,从对应语言的类库中判断光标所在字符串的类型,方便hint提示。token中包含start、end、string、type等属性,start和end指的是光标所在字符串在这一行的起始位置和结束位置,string是提取的字符串,type表示该字符串是什么类型(keyword/operator/string等等不定)
5. 下面方法中返回的结果体意思是:下拉列表中展示hello和world两行提示,from和to表示当用户选择了提示内容后,这些提示内容要替换编辑区域的哪个字符串。方法中的代码含义是替换token全部字符串。

*/
handleShowHint(cmInstance, hintOptions) {
    let cursor = cmInstance.getCursor();
    let cursorLine = cmInstance.getLine(cursor.line);
    let end = cursor.ch;
    let start = end;

    let token = cmInstance.getTokenAt(cursor)
    console.log(cmInstance, cursor, cursorLine, end, token)
    // console.log(hintOptions.tables)
    // return hintOptions.tables;
    return {
        list: ["hello","world"],
        from: {ch: token.start, line: cursor.line},
        to: {ch: token.end, line: cursor.line}
    };
}

接下来修改配置文件中的hintOptions属性,增加hint属性,并指向实现方法:

{ // 省略其他配置项...
    hintOptions: {
        completeSingle: false,
        hint: this.handleShowHint
    }
}

3.2 自定义hint展示内容

自定义代码提示内容后,如果想让弹出的内容与实际插入内容不一样,则需要将返回结果进行调整。这里有一个示例,插入内容是英文,展示内容是中文。

在methods中新增方法:


handleShowHint2(cmInstance, hintOptions) {
  let cursor = cmInstance.getCursor();
  let cursorLine = cmInstance.getLine(cursor.line);
  let end = cursor.ch;
  let start = end;

  let token = cmInstance.getTokenAt(cursor)
  console.log(cmInstance, cursor, cursorLine, end, token)
  return {
    list: [{
      text: "hello",
      displayText: "你好呀",
      displayInfo: "提示信息1",
      render: this.hintRender
          }, {
      text: "world",
      displayText: "世界",
      displayInfo: "提示信息2",
      render: this.hintRender
    }],
    from: {
ch: token.start, line: cursor.line
    },
    to: {
ch: token.end, line: cursor.line
    }
  }
},
hintRender(element, self, data) {
  let div = document.createElement("div");
  div.setAttribute("class", "autocomplete-div");

  let divText = document.createElement("div");
  divText.setAttribute("class", "autocomplete-name");
  divText.innerText = data.displayText;

  let divInfo = document.createElement("div");
  divInfo.setAttribute("class", "autocomplete-hint");
  divInfo.innerText = data.displayInfo;

  div.appendChild(divText);
  div.appendChild(divInfo);
  element.appendChild(div);
}

增加样式调整:

<style>
  .autocomplete-div {
    display: inline-block;
    width: 100%;
  }
  .autocomplete-name {
    display: inline-block;
  }
  .autocomplete-hint {
    display: inline-block;
    float: right;
    color: #0088ff;
    margin-left: 1em;
  }
</style>

最终的效果如下:

2020-12-13T12:45:11.png

此段功能参考了phpmyadmin中的用法:

table.columns.push

columnHintRender

.autocomplete-column-name

3.3 异步返回hint结果

cm提供了一种异步hint的功能,如果我们的数据来自后端,那这个功能就用的上了。具体使用方式如下:

设置hint配置:

{ // 省略其他配置项...
    hintOptions: {
        completeSingle: false,
        hint: this.handleShowHint,
        async: true
    }
}

实现自定义hint:

handleShowHint3(cmInstance, hintOptions) {
  let cursor = cmInstance.getCursor();
  let cursorLine = cmInstance.getLine(cursor.line);
  let end = cursor.ch;
  let start = end;

  let token = cmInstance.getTokenAt(cursor)
  console.log(cmInstance, cursor, cursorLine, end, token)
  // console.log(hintOptions.tables)
  // return hintOptions.tables;
  // 返回一个promise即可
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve({
        list: ["hello", "world"],
        from: {ch: token.start, line: cursor.line},
        to: {ch: token.end, line: cursor.line}
      })
    }, 2000);
  })
}

5. CodeMirror命令API

第二节中我们使用到了cm自带的搜索功能,它虽然默认指定了快捷键,如果你想要自行触发这些功能,cm提供了命令API可以帮助你实现想法。

具体命令见:https://codemirror.net/doc/manual.html#commands

实际调用方式:

methods:{
    find(){
        this.$refs.cm.comdemirror.execCommand("find") 
    }
}

4. 特殊用法和踩过的坑

4.1 自动高度

codemirror默认的高度是300px,如果想要调整默认高度,可以在mounted方法中增加下面一段代码,这段代码的含义是调整cm高度为(当前浏览器高度-200)px,并且在窗口发生变化时,重新再做出调整。

this.$refs.cm.codemirror.setSize("auto", (document.documentElement.clientHeight - 200) + "px")
this.$nextTick(() => {
    window.addEventListener('resize', () => {
        //监听浏览器窗口大小改变
        //浏览器变化执行动作
        this.$refs.cm.codemirror.setSize("auto", (document.documentElement.clientHeight - 200) + "px")
    });
})

4.2 只读模式

在官方文档里提示调整options中的readOnly参数便可以设置为只读,但实际上如果设置值为true后,用户还能在浏览器中看到光标闪烁,如果希望页面上不能编辑,则将该值设置为'nocursor'即可。

但如果设置了'nocursor',那么任何人将无法选中代码,也无法右键复制。如果还想支持选择和复制,那么需要用到以下代码:

this.$refs.cm.codemirror.setOption("readOnly",true)
// 不设的话,默认是530
this.$refs.cm.codemirror.setOption("cursorBlinkRate",-1)

4.3 tab转空格(2021-01-09补充)

如果在新的一行直接使用tab键,大概率会输入一个制表符,但如果从上一行敲回车进入下一行,却默认是空格。这样的逻辑让使用者深恶痛绝,如何让tab键也变成空格呢?在配置json中增加下面配置,既可实现两者逻辑统一。

indentUnit:4,
extraKeys: {
    Tab: (cm) => {
        // 存在文本选择
        if (cm.somethingSelected()) {
            // 正向缩进文本
            cm.indentSelection('add');
        } else {
            // 无文本选择
            //cm.indentLine(cm.getCursor().line, "add");  // 整行缩进 不符合预期
            // 光标处插入 indentUnit 个空格
            //console.log(cm.getOption("tabSize"),cm.getOption("indentUnit"))
            cm.replaceSelection(Array(cm.getOption("indentUnit") + 1).join(" "), "end", "+input");
        }
    },
    "Shift-Tab": (cm) => {
        // 反向缩进
        if (cm.somethingSelected()) {
            // 反向缩进
            cm.indentSelection('subtract');
        } else {
            // cm.indentLine(cm.getCursor().line, "subtract");  // 直接缩进整行
            const cursor = cm.getCursor();
            // 光标回退 indexUnit 字符
            cm.setCursor({line: cursor.line, ch: cursor.ch - cm.getOption("indentUnit")});
        }
        return;
    },
}

5. 小结

codemirror是业界使用很广泛的前端代码编辑器,它的功能很强大。也是因为它功能强大,导致了很多高级功能需要进行独特的配置,如果只看官方文档,一时半会也无法参透其中的含义。因此笔者将其中常用的内容整理出来,方便大家学习参考。

标签: javascript, codemirror, 编辑器, 教程

已有 7 条评论

  1. kiki kiki

    你好,请教下这个自定义提示功能定义完成之后,只会提示自己定义的,如果还想提示官方原有的,应该怎么做呢?

    1. 如果启用自定义提示功能,相当于替换掉了原有的官方提示。如果你还想提示官方内容,那就查找相关源码,把里面的提示内容复制到你的自定义提示里面。

  2. vocalwang vocalwang

    请问search中 (Use /re/ syntax for regexp search)这个提示可以修改吗

  3. shouyangpei shouyangpei

    可是codemirror是npm install的,如何提取到本地,我试了,下载包,但是找不到对应的js文件,感觉应该是src目录下面的codemirror.js,但是引入到项目中,会报错

    1. zjie zjie

      安装时需要注意版本,version5和6目录结构是不一样的

  4. jack zhang jack zhang

    你好,请问下在父组件中调用子组件codemirror,动态的添加了很多的编辑器页面,在实际取值的时候,数据会乱,请问有什么标识可以做吗,而不是通过循环的index

  5. keller keller

    你好博主,文章中代码块的主题很棒!猜测用的CodeMirror编辑器的只读模式吧?是的话具体叫idea,还是Darcula吗?

添加新评论