罗列篇:Codemirror6

2,609 阅读3分钟

官网
Demo

相较于上一个版本,Codemirror 6在保持和版本5基本兼容的情况下进行了一次较大的重构,在使用和维护上都有了很大的进步。

首先需要注意包名的区别,版本5中的@codemirror/basic-setup被重命名为codemirror,并且不再导出 EditorState,因此,如果想要完成一个基础的样例,我们首先得要安装codemirror@codemirror/state

版本6中的包名基本上以@codemirror开头

import { basicSetup, EditorView } from 'codemirror'
import { EditorState } from "@codemirror/state"

let startState = EditorState.create({
  doc: "Hello World",
  extensions: [ basicSetup ],
})

let view = new EditorView({
  state: startState,
  parent: document.body
})

basicSetup内包含了一系列常用的插件和设置,基本上使用它就可以设置好一个基础的编辑器了。

const basicSetup = (() => [
    view.lineNumbers(),
    view.highlightActiveLineGutter(),
    view.highlightSpecialChars(),
    commands.history(),
    language.foldGutter(),
    view.drawSelection(),
    view.dropCursor(),
    state.EditorState.allowMultipleSelections.of(true),
    language.indentOnInput(),
    language.syntaxHighlighting(language.defaultHighlightStyle, { fallback: true }),
    language.bracketMatching(),
    autocomplete.closeBrackets(),
    autocomplete.autocompletion(),
    view.rectangularSelection(),
    view.crosshairCursor(),
    view.highlightActiveLine(),
    search.highlightSelectionMatches(),
    view.keymap.of([
        ...autocomplete.closeBracketsKeymap,
        ...commands.defaultKeymap,
        ...search.searchKeymap,
        ...commands.historyKeymap,
        ...language.foldKeymap,
        ...autocomplete.completionKeymap,
        ...lint.lintKeymap
    ])
])();

Codemirror 6中的语言插件都是以@codemirror/lang-开头,如@codemirror/lang-cpp@codemirror/lang-javascript,引入插件并且加入state.extensions就行,同理,主题高亮也是以相同的方式引入。

import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'
...
extensions: [ javascript(), oneDark ]

不过有些时候我们需要切换编辑器的语言配置等,这时候就需要借助Compartments这个类,将某种配置包装在其中,后续就可以动态配置了。

import { javascript } from '@codemirror/lang-javascript'
import { cpp } from '@codemirror/lang-cpp'

...
extensions: [
  basicSetup,
  langConfig.of(javascript()), // 注意这里就需要以这种方式引入动态配置
  readonlyConfig.of(EditorState.readOnly.of(false)),
]
...

let activeLang = 'js'
function toggleLanguage() {
  const v = view.value!
  activeLang = activeLang === 'js' ? 'cpp' : 'js'
  const isJs = activeLang === 'js'
  v.dispatch({
    changes: [
      {
        from: 0,
        to: v.state.doc.length,
        insert: isJs ? codeJs : codeCpp,
      },
    ],
    effects: langConfig.reconfigure(isJs ? javascript() : cpp()),
  })
}

首先需要注意上述langConfig.reconfigure的写法,其次,也可以看出触发编辑器变化都需要通过view.dispatch的方式,比如说上面的改变代码内容,就是用的view.dispatch({changes:[]}),像是修改只读、修改光标位置同理也是使用这种方式。

let readonly = false
function toggleReadonly() {
  const v = view.value!
  readonly = !readonly
  v.dispatch({
    effects: readonlyConfig.reconfigure(EditorState.readOnly.of(readonly)),
  })
}

function focus() {
  const v = view.value!
  v.focus()
  v.dispatch({
    selection: { anchor: v.state.doc.length },
  })
}

function printValue() {
  const v = view.value!
  // 编辑器的值可以通过view.state.doc.toString()获得
  console.log(v.state.doc.toString())
}

说完基础设置、语言支持、动态修改配置后,寻常的需求基本可以满足,不过如果需要自定义语言支持或者自定义编辑器主题和代码高亮样式的话,也可以参考官方的文档,虽然文档写得比较不直观,但也只能将就着看了,毕竟国内关于Codemirror 6的细节介绍还是相对较少。

import { tags } from '@lezer/highlight'
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'

// 自定义编辑器主题,这里使用的是一款不错的配色NordCSS
const themeNord = EditorView.theme(
    {
      '&': {
        color: 'var(--nord6)',
        backgroundColor: 'var(--nord0)',
        fontSize: '1rem',
      },
      '.cm-content': {
        caretColor: 'var(--nord6)',
        fontFamily: `'Sarasa Mono SC', 'Courier New', Courier, monospace`,
      },
      '&.cm-focused .cm-cursor': {
        borderLeftColor: 'var(--nord13)',
        borderLeftWidth: '2px',
      },
      '&.cm-focused .cm-selectionBackground, ::selection': {
        backgroundColor: 'var(--nord3)',
      },
      '.cm-gutters': {
        backgroundColor: 'var(--nord2)',
        color: 'var(--nord4)',
        border: 'none',
      },
    },
    { dark: true }
  )

  // 自定义代码高亮主题,这就比较繁杂些了,以下只是简单的样式
  const highlightNord = syntaxHighlighting(
    HighlightStyle.define([
      { tag: tags.keyword, color: 'var(--nord9)' },
      { tag: tags.comment, color: 'var(--nord14)', fontStyle: 'italic' },
      { tag: tags.number, color: 'var(--nord15)' },
      { tag: tags.string, color: 'var(--nord14)' },
      { tag: tags.bool, color: 'var(--nord13)' },
      { tag: tags.variableName, color: 'var(--nord12)' },
    ])
  )
  
  // 以插件的形式引入自定义内容
  extensions: [
    themeNord,
    highlightNord,
  ]

需要注意的是并非所有版本5的语言都移植到了版本6中,因此如果需要引入仅有版本5支持的语言,需要借助legacy-modes,不过就我的尝试而言,这种方式并不是完全可行,有些时候语言高亮会出问题。