CodeMirror6 入门用法

8,050 阅读4分钟

Tip: WeChat public account search and follow '进二开物', more sharing JavaScript/TypeScript/React and so on.

你是否对编辑器感兴趣过呢?

编辑器是前端项目中比较复杂的项目之一了。想熟悉编辑器,对自定义 Markdown 编辑器需求,虽然市面上有类似的,且比较成熟,但是有些功能需要自己的定义。所以找到 CodeMirror,同类产品还有 Monoca 以及 ace 等,来支撑编辑器区域,自己定义显示区域。

CodeMirror 到现在已经有两个版本,CodeMirror v5 版本在很多的项目中得到了使用。例如 bytemd 掘金社区的文档就是使用 CodeMirror 编辑器。

V6 版本的 CodeMirror 的其实中文教程和文档都很少,因为 CodeMirror 使用场景,所以还是尝试写文章,来关注 CodeMirror v6 的版本。

使用 Vite 初始化项目

pnpm create vite

选择 TS + React + SWC(编译) 作为项目模板

增加 CodeMirror 编辑器组件

import { useEffect, useRef } from "react";

function CMEditor() {
  const edContainer = useRef<any>();

  return <div ref={edContainer} className="container"></div>;
}

export default CMEditor;

注意:使用最为基础的编辑器作为

安装依赖包

npm i codemirror @codemirror/view

创建一个 Hello World 组件的 CodeMirror 组件

import { useEffect, useRef } from "react";

import { EditorView } from "@codemirror/view";

function CMEditor() {
  const edContainer = useRef<any>();

  useEffect(() => {
    const view = new EditorView({
      doc: "hello world!\nsdfsdf", // 带有 `\n` <img src="会渲染成两行" alt="" width="70%" />
      parent: edContainer.current,
    });

    return () => {
      view.destroy(); // 注意:此后此处要随组件销毁
    };
  }, []);
  return <div ref={edContainer} className="container"></div>;
}

export default CMEditor;

将组件加入到项目主程序中:

import CMEditor from '../src/components/CMEditor'
function App() {

  return (
    <div className="App">
      <CMEditor />
    </div>
  )
}

export default App

显示效果如下:

base-codemirror.png

当然可以通过state 来渲染 doc

在 state 里面定义 doc

使用 EditorState.create 方法创建 state, 然后 options 传入 doc 方法:

import { useEffect, useRef } from "react";

import { EditorView } from "@codemirror/view";
import { EditorState } from "@codemirror/state";

function CMEditor() {
  const edContainer = useRef<any>();

  useEffect(() => {
    const state = EditorState.create({
      doc: "hello world!\nsdfsdf",
    });
    const view = new EditorView({
      parent: edContainer.current,
      state,
    });

    return () => {
      view.destroy();
    };
  }, []);

  return <div ref={edContainer} className="container"></div>;
}

export default CMEditor;

以上定义一个一个基本的 CodeMirror 使用方法的两种定义方法

扩展

上面的两种是最为基本的使用方法,CodeMirror 的内部提供了两种扩展:

  • basicSetup
  • minimalSetup

这两个 setup 导出在 basic-setup 包里面, 安装时使使用 npm install codemirror 因为一些历史原因 CodeMirror 的命名似乎有些混乱。

basicSetup 扩展

const view = new EditorView({
  parent: edContainer.current,
  doc: "hello world!\nsdfsdf",
  extensions: [basicSetup]
});
  • 支持扩展
export const basicSetup: Extension = (() => [
  lineNumbers(), // 显示行
  highlightActiveLineGutter(),
  highlightSpecialChars(),
  history(),
  foldGutter(), // 折叠
  drawSelection(),
  dropCursor(),
  EditorState.allowMultipleSelections.of(true), // 多行选择
  indentOnInput(), // 输入是缩进
  syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
  bracketMatching(),
  closeBrackets(),
  autocompletion(), // 自动完成
  rectangularSelection(),
  crosshairCursor(),
  highlightActiveLine(),
  highlightSelectionMatches(),
  keymap.of([
    ...closeBracketsKeymap,
    ...defaultKeymap,
    ...searchKeymap,
    ...historyKeymap,
    ...foldKeymap,
    ...completionKeymap,
    ...lintKeymap
  ])
])()

我们看到支持可众多的插件:高亮相关,显示行号等等...

minimal

const state = EditorState.create({
  doc: "hello world!\nsdfsdf",
  extensions: [basicSetup]
});
const view = new EditorView({
  parent: edContainer.current,
  state
});
  • 支持扩展
export const minimalSetup: Extension = (() => [
  highlightSpecialChars(),
  history(),
  drawSelection(),
  syntaxHighlighting(defaultHighlightStyle, {fallback: true}),
  keymap.of([
    ...defaultKeymap,
    ...historyKeymap,
  ])
])()

内置扩展命令支持

CodeMirror 命令扩展的方式支持缩进,以及一些自定义的快捷键

import { defaultKeymap, indentWithTab } from '@codemirror/commands';

const startState = EditorState.create({
  doc: 'Hello World',
  extensions: [keymap.of([defaultKeymap, indentWithTab])], // 支持 tab 缩进
});

扩展语言/类型主题支持

CodeMirror 多服务于代码相关的编辑器,要高亮一些语法等等,安装支持包:

npm install @codemirror/lang-javascript @codemirror/theme-one-dark

改写组件:

import { useEffect, useRef } from "react";
import { EditorView } from "@codemirror/view";
import { basicSetup } from 'codemirror';
import { javascript } from '@codemirror/lang-javascript'
import { oneDark } from '@codemirror/theme-one-dark'

function CMEditor() {
  const edContainer = useRef<any>();

  useEffect(() => {
    const view = new EditorView({
      parent: edContainer.current,
      doc: "console.log('this is javascript code')\nfunction red(){retun'this is red function'}",
      extensions: [basicSetup, javascript(), oneDark]
    });

    return () => {
      view.destroy();
    };
  }, []);

  return <div ref={edContainer} className="container"></div>;
}

export default CMEditor;

显示效果:

image.png

更新 doc

使用 state + editor.dispatch(tr)更新数据

import { useEffect, useRef, useState } from "react";
import { EditorView, ViewUpdate } from "@codemirror/view";
import { basicSetup } from "codemirror";
import { javascript } from "@codemirror/lang-javascript";
import { oneDark } from "@codemirror/theme-one-dark";

function CMEditor() {
  const viewRef = useRef();
  const edContainer = useRef<any>();

  useEffect(() => {
    let onUpdateExt = EditorView.updateListener.of((v: ViewUpdate) => {
      if (v.docChanged) {
        viewRef.current!.dispatch({
          state: v.state,
        });
      }
    });
    const viewRef = new EditorView({
      parent: edContainer.current,
      doc: 'if(true) {return false}',
      extensions: [basicSetup, javascript(), oneDark, onUpdateExt],
    });

    return () => {
      viewRef?.current?.destroy();
    };
  }, []);

  return <div ref={edContainer} className="container"></div>;
}

export default CMEditor;

自定义主题和语法高亮

import {EditorView} from "@codemirror/view"

let baseTheme = EditorView.baseTheme({
  ".cm-o-replacement": {
    display: "inline-block",
    width: ".5em",
    height: ".5em",
    borderRadius: ".25em"
  },
  "&light .cm-o-replacement": {
    backgroundColor: "#04c"
  },
  "&dark .cm-o-replacement": {
    backgroundColor: "#5bf"
  }
})

改变基础 theme 的样式 EditorView.baseTheme 样式。


const viewRef = new EditorView({
  parent: edContainer.current,
  doc: 'if(1) {return false}',
  extensions: [baseTheme],
});

语法高亮

import {tags} from "@lezer/highlight"
import {HighlightStyle} from "@codemirror/language"

const myHighlightStyle = HighlightStyle.define([
  {tag: tags.keyword, color: "#fc6"},
  {tag: tags.comment, color: "#f5d", fontStyle: "italic"}
])

小结

codemirror 的入门使用方法,这里讲解使用 React + vite 创建一个项目,并创建编辑器组件的两种方式。codemirror 内置插件 basicSetup/minimal 两种不同的扩展集合。官方提供的命令扩展如: tab 命令等。然后提供了扩展语言:如使用 JavaScript 语言为例,讲解了扩展语言插件和语言的语法高亮等

参考