目录
本文收录在《如何开发一个自己的笔记软件》系列中,该系列源码均可在 Blossom 笔记软件 仓库中查看。仓库地址:
安装与使用
源码地址 Codemirror 源码地址
官方文档 Codemirror 官方文档
bash
npm install codemirror
npm install @codemirror/lang-markdown
npm install @codemirror/language-data
创建一个编辑器
Codemirror6 最基础的编辑器是十分简单的,基本就是一个文本框,绝大部分功能都需要通过配置 extensions 参数来实现,好在官方提供了一个经典配置(basicSetup),直接该配置集合可以实现绝大部分编辑器的功能。
官方文档地址:经典配置 basicSetup
下面是一个 Markdown 编辑器的配置示例,各项参数说明在注释中。如果你需要实现其他语言的编辑器,只需要安装对应的语言包,然后在拓展中引入即可。你可以去官方仓库下寻找你需要的语言支持。通常为lang-xxx
的命名方式。
typescript
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
})
重置编辑器
在切换文件时,一般需要重置编辑器的编辑历史内容等等状态,这时可以使用如下方式。
typescript
editor.setState(EditorState.create(内容忽略...))
还有一种方式是直接替换整个编辑器的内容,但是这样在执行撤销操作时(Ctrl + Z)
,会将内容重置成上一个文件内容,这样显然是有问题的。
自定义编辑器样式
你可以在创建 state 时指定样式, 下方是一个简单的示例
typescript
/**
* 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. 获取整个文本内容
typescript
editor.state.doc.toString()
2. 获取选中的文本内容
现代编辑器都是允许同时选中多个内容的,所以如果你想要获取当前选中的内容,返回的会是一个数组,但是需要注意,返回的并不是选中的文本内容,而是选中的开始位置和结束位置。
typescript
import { SelectionRange } from "@codemirror/state"
// 获取选中内容,返回的是一个数组
const ranges:SelectionRange[] = editor.state.selection.ranges
// 注意判断返回数组是否为空
const range:SelectionRange = ranges[0]
// 打印选中的文本在整个文本的开始位置和结束位置
console.log(range.form, range.to)
知道了选中文本的位置后,可以通过如下方式获取文本内容
typescript
// 获取指定范围的文本
const doc:string = editor.state.sliceDoc(range.from, range.to)
3. 获取指定位置的文本内容
上文已经介绍如何获取位置,接着通过如下方式获取
typescript
const doc:string = editor.state.sliceDoc(range.from, range.to)
4. 获取文档的最大长度
typescript
const length:number = editor.state.doc.length
5. 在指定位置插入内容
可以同时在多个位置插入内容,例如在 VS Code 中可以使用Alt
一次选中多个位置,然后同时更改内容。Codemirror 也支持该方式。
typescript
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 进行了简单的封装,可以前往查看:
- Github: codemirror 封装
- Gitee : codemirror 封装