如何使用 Codemirror6 创建一个代码编辑器

1,268 阅读5分钟

目录

本文收录在《如何开发一个自己的笔记软件》系列中,该系列源码均可在 Blossom 笔记软件 仓库中查看。仓库地址:

安装与使用

源码地址 Codemirror 源码地址

官方文档 Codemirror 官方文档

npm install codemirror
npm install @codemirror/lang-markdown
npm install @codemirror/language-data

创建一个编辑器

Codemirror6 最基础的编辑器是十分简单的,基本就是一个文本框,绝大部分功能都需要通过配置 extensions 参数来实现,好在官方提供了一个经典配置(basicSetup),直接该配置集合可以实现绝大部分编辑器的功能。

官方文档地址:经典配置 basicSetup

下面是一个 Markdown 编辑器的配置示例,各项参数说明在注释中。如果你需要实现其他语言的编辑器,只需要安装对应的语言包,然后在拓展中引入即可。你可以去官方仓库下寻找你需要的语言支持。通常为lang-xxx的命名方式。

import { EditorState } from "@codemirror/state"
import { EditorView, basicSetup } from "codemirror"
import { ViewUpdate, keymap } from "@codemirror/view"
import { insertTab, indentLess } from "@codemirror/commands"
import { markdown as cmmd } from '@codemirror/lang-markdown'
import { languages } from "@codemirror/language-data"

// 指明编辑器的状态, 编辑器的使用和状态都和该对象有关, 例如编辑记录 redo/undo
const state = EditorState.create({
  // 编辑器的初始内容
  doc: doc,
  // 拓展
  extensions: [
    // 这是一个基础的配置, 包含一系列常用的配置, 如快捷键等等, 行数, 查询等等功能...
    // https://codemirror.net/docs/ref/#codemirror.basicSetup
    basicSetup,
    // 编辑器的语言支持
    cmmd({ codeLanguages: languages }),
    // 样式
    // https://codemirror.net/examples/styling/
    EditorView.theme(cwTheme,{dark,true]}),
    // 快捷键
    keymap.of([
      // 缩进
      { key: 'Tab', run: insertTab, },
      // 取消缩进
      { key: 'Shift-Tab', run: indentLess },
      // 自定义快捷键
      { key: 'Ctrl-s', run(_view: EditorView) { saveCallback(); return true } }
    ]),
    // 修改内容时的监听, 当内容变更时触发
    EditorView.updateListener.of((viewUpd: ViewUpdate) => {
      if (viewUpd.docChanged) {
        updateCallback()
      }
    })
  ]
})

// 创建编辑器实例
EditorView editor = new EditorView({
  // 上方创建的 state
  state: state,
  // 编辑器的父 dom 元素
  parent: parent
})

重置编辑器

在切换文件时,一般需要重置编辑器的编辑历史内容等等状态,这时可以使用如下方式。

editor.setState(EditorState.create(内容忽略...))

还有一种方式是直接替换整个编辑器的内容,但是这样在执行撤销操作时(Ctrl + Z),会将内容重置成上一个文件内容,这样显然是有问题的。

自定义编辑器样式

你可以在创建 state 时指定样式, 下方是一个简单的示例

/**
 * codemirror 样式配置
 * https://codemirror.net/examples/styling/#themes
 */
export const cwTheme: any = {
  // 编辑器总体
  "&": {
    color: "var(--bl-editor-color)",
    backgroundColor: "var(--bl-editor-bg-color)",
    fontSize: '14px'
  },
  // 左侧的行数等信息所在的边栏
  ".cm-gutters": {
    backgroundColor: 'var(--bl-editor-gutters-bg-color)',
    borderColor: 'var(--bl-editor-gutters-border-color)',
    fontSize: '12px'
  },
  // 选中某行时,边栏的样式
  ".cm-activeLineGutter": {
    backgroundColor: 'var(--bl-editor-gutters-bg-color)',
    color: 'var(--el-color-primary)'
  },
  // 行号
  ".cm-lineNumbers": {
    width: '40px'
  },
  // 编辑器正文的内容
  ".cm-content": {
    whiteSpace: "break-spaces",
    wordWrap: "break-word",
    width: "calc(100% - 55px)",
    overflow: 'auto',
    padding: '0',
    caretColor: "#707070"
  },
  // 编辑器每一行的内容
  ".cm-line": {
    // color: '#707070'
    // caretColor: 'var(--bl-editor-caret-color) !important',
    wordWrap: 'break-word',
    wordBreak: 'break-all',
    padding: '0'
  },
  // 当前选中行
  ".cm-activeLine": {
    backgroundColor: 'var(--bl-editor-active-line-gutter-bg-color)',
  },
  // 搜索时匹配到的样式
  ".cm-selectionMatch": {
    backgroundColor: 'var(--bl-editor-selection-match-bg-color)'
  },
  // 各类关键字
  ".ͼ1.cm-focused": {
    outline: 'none'
  },
  ".ͼ2 .cm-activeLine": {
    backgroundColor: 'var(--bl-editor-active-line-gutter-bg-color)',
  },
  ".ͼ5": {
    color: 'var(--bl-editor-c5-color)',
    fontWeight: '700'
  },
  ".ͼ6": {
    color: '#707070',
    fontWeight: '500'
  },
  ".ͼ7": {
    backgroundColor: 'var(--bl-editor-c7-bg-color)',
    color: 'var(--bl-editor-c7-color)'
  },
  ".ͼc": {
    color: 'var(--bl-editor-cc-color)',
  },
  // ͼm: 注释   #940
  ".ͼm": {
    color: 'var(--bl-editor-cm-color)'
  },
  // ͼb: 关键字 #708
  ".ͼb": {
    color: 'var(--bl-editor-cb-color)'
  },
  // ͼd: 数字 #708
  ".ͼd": {
    color: 'var(--bl-editor-cd-color)'
  },
  // ͼe: 字符串 #a11
  ".ͼe": {
    color: 'var(--bl-editor-ce-color)'
  },
  //ͼi: 类名: 
  ".ͼi": {
    color: 'var(--bl-editor-ci-color)'
  },
  //ͼg: 方法名和参数
  ".ͼg": {
    color: 'var(--bl-editor-cg-color)'
  }
}

const state = EditorState.create({
  // 拓展
  extensions: [
    // 样式
    // https://codemirror.net/examples/styling/
    EditorView.theme(cwTheme,{dark,true]}),
  ]
})

一些常用的API

1. 获取整个文本内容

editor.state.doc.toString()

2. 获取选中的文本内容

现代编辑器都是允许同时选中多个内容的,所以如果你想要获取当前选中的内容,返回的会是一个数组,但是需要注意,返回的并不是选中的文本内容,而是选中的开始位置和结束位置。

import { SelectionRange } from "@codemirror/state"

// 获取选中内容,返回的是一个数组
const ranges:SelectionRange[] = editor.state.selection.ranges

// 注意判断返回数组是否为空
const range:SelectionRange = ranges[0]

// 打印选中的文本在整个文本的开始位置和结束位置
console.log(range.form, range.to)

知道了选中文本的位置后,可以通过如下方式获取文本内容

// 获取指定范围的文本
const doc:string = editor.state.sliceDoc(range.from, range.to)

3. 获取指定位置的文本内容

上文已经介绍如何获取位置,接着通过如下方式获取

const doc:string = editor.state.sliceDoc(range.from, range.to)

4. 获取文档的最大长度

const length:number = editor.state.doc.length

5. 在指定位置插入内容

可以同时在多个位置插入内容,例如在 VS Code 中可以使用Alt一次选中多个位置,然后同时更改内容。Codemirror 也支持该方式。

import { EditorSelection, SelectionRange } from "@codemirror/state"

let changeByRange = {
  /* 
  创建变更的内容, 可以是个数组
  from 是插入的开始位置,to 是插入的结束,from 和 to 可以相同,如果不同,则是替换 from->to 的内容
  insert 是插入的内容
  */
  changes: [
    { from: istFrom, to: istTo, insert: content }
  ],
  /* 内容修改完成之后,将光标移动到的位置,相同就是移动到该位置,不同则是选择该范围 */
  range: EditorSelection.range(selectFrom, selectTo)
}

// 执行变更
editor.dispatch(
  editor.state.changeByRange((_range: SelectionRange) => {
    return changeByRange
  })
)

封装

在作者使用时,对 codemirror 进行了简单的封装,可以前往查看: