fix some performance problem, add partial render and undo depth (#222)

* fix some performance problem, add partial render and undo depth

* optimization of performance: rewrite getBlock, getBlocks ...

* optimization of performance: cache the result of tokenization

* optimization of performance: render code block only needed
This commit is contained in:
冉四夕 2018-04-30 01:46:09 +08:00 committed by GitHub
parent d19dc9f4b8
commit f569d2e9d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 255 additions and 159 deletions

2
.github/TODOLIST.md vendored
View File

@ -68,6 +68,8 @@
- [ ] Refactor data structure of contentState, add copyBlock...
- [ ] Refactor the History data structure
##### Documents
- [ ] Manual

View File

@ -3,60 +3,7 @@ import { isCursorAtFirstLine, isCursorAtLastLine, isCursorAtBegin, isCursorAtEnd
import { findNearestParagraph } from '../utils/domManipulate'
import selection from '../selection'
const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
const arrowCtrl = ContentState => {
ContentState.prototype.firstInDescendant = function (block) {
const children = block.children
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (children.length) {
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
return this.firstInDescendant(children[1])
} else {
return this.firstInDescendant(children[0])
}
}
}
ContentState.prototype.lastInDescendant = function (block) {
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (block.children.length) {
const children = block.children
let lastChild = children[children.length - 1]
while (lastChild.editable === false) {
lastChild = this.getPreSibling(lastChild)
}
return this.lastInDescendant(lastChild)
}
}
ContentState.prototype.findPreBlockInLocation = function (block) {
const parent = this.getParent(block)
const preBlock = this.getPreSibling(block)
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
return this.lastInDescendant(preBlock)
} else if (parent) {
return this.findPreBlockInLocation(parent)
} else {
return null
}
}
ContentState.prototype.findNextBlockInLocation = function (block) {
const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block)
if (block.nextSibling && nextBlock.editable !== false) {
return this.firstInDescendant(nextBlock)
} else if (parent) {
return this.findNextBlockInLocation(parent)
} else {
return null
}
}
ContentState.prototype.findNextRowCell = function (cell) {
if (!/th|td/.test(cell.type)) throw new Error(`block with type ${cell && cell.type} is not a table cell`)
const row = this.getParent(cell)
@ -126,6 +73,7 @@ const arrowCtrl = ContentState => {
if (show && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown)) {
event.preventDefault()
event.stopPropagation()
switch (event.key) {
case EVENT_KEYS.ArrowDown:
if (index < list.length - 1) {
@ -147,6 +95,7 @@ const arrowCtrl = ContentState => {
const anchorBlock = block.functionType === 'html' ? this.getParent(this.getParent(block)) : block
let activeBlock
event.preventDefault()
event.stopPropagation()
switch (event.key) {
case EVENT_KEYS.ArrowLeft: // fallthrough
case EVENT_KEYS.ArrowUp:
@ -236,6 +185,7 @@ const arrowCtrl = ContentState => {
if (activeBlock) {
event.preventDefault()
event.stopPropagation()
const offset = activeBlock.type === 'p' ? 0 : (event.key === EVENT_KEYS.ArrowUp ? activeBlock.text.length : 0)
const key = activeBlock.type === 'p' ? activeBlock.children[0].key : activeBlock.key
this.cursor = {
@ -258,6 +208,7 @@ const arrowCtrl = ContentState => {
(preBlock && preBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowLeft && left === 0)
) {
event.preventDefault()
event.stopPropagation()
const key = preBlock.key
const offset = 0
this.cursor = {
@ -274,6 +225,7 @@ const arrowCtrl = ContentState => {
(nextBlock && nextBlock.type === 'pre' && event.key === EVENT_KEYS.ArrowRight && right === 0)
) {
event.preventDefault()
event.stopPropagation()
const key = nextBlock.key
const offset = 0
this.cursor = {
@ -289,6 +241,7 @@ const arrowCtrl = ContentState => {
(event.key === EVENT_KEYS.ArrowLeft && start.offset === 0)
) {
event.preventDefault()
event.stopPropagation()
if (!preBlock) return
const key = preBlock.key
const offset = preBlock.text.length
@ -302,6 +255,7 @@ const arrowCtrl = ContentState => {
(event.key === EVENT_KEYS.ArrowRight && start.offset === block.text.length)
) {
event.preventDefault()
event.stopPropagation()
let key
if (nextBlock) {
key = nextBlock.key

View File

@ -2,7 +2,7 @@ import createDOMPurify from 'dompurify'
import codeMirror, { setMode, setCursorAtLastLine } from '../codeMirror'
import { createInputInCodeBlock } from '../utils/domManipulate'
import { escapeInBlockHtml } from '../utils'
import { codeMirrorConfig, CLASS_OR_ID, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config'
import { codeMirrorConfig, BLOCK_TYPE7, DOMPURIFY_CONFIG } from '../config'
const DOMPurify = createDOMPurify(window)
@ -72,9 +72,10 @@ const codeBlockCtrl = ContentState => {
return !!match
}
ContentState.prototype.pre2CodeMirror = function (isRenderCursor) {
ContentState.prototype.pre2CodeMirror = function (isRenderCursor, emptyPres) {
if (!emptyPres.length) return
const { eventCenter } = this
const selector = `pre.${CLASS_OR_ID['AG_CODE_BLOCK']}, pre.${CLASS_OR_ID['AG_HTML_BLOCK']}`
const selector = emptyPres.map(key => `pre#${key}`).join(', ')
const pres = document.querySelectorAll(selector)
Array.from(pres).forEach(pre => {
const id = pre.id

View File

@ -48,8 +48,13 @@ export class History {
}
}
push (state) {
const UNDO_DEPTH = 20
this.stack.splice(this.index + 1)
this.stack.push(deepCopy(state))
if (this.stack.length > UNDO_DEPTH) {
this.stack.shift()
this.index = this.index - 1
}
this.index = this.index + 1
}
}

View File

@ -1,3 +1,4 @@
import { setCursorAtLastLine } from '../codeMirror'
import { getUniqueId } from '../utils'
import selection from '../selection'
import StateRender from '../parser/StateRender'
@ -42,17 +43,7 @@ const prototypes = [
importMarkdown
]
// deep first search
const convertBlocksToArray = blocks => {
const result = []
blocks.forEach(block => {
result.push(block)
if (block.children.length) {
result.push(...convertBlocksToArray(block.children))
}
})
return result
}
const HAS_TEXT_BLOCK_REG = /^(h\d|span|th|td|hr|pre)/
// use to cache the keys which you don't want to remove.
const exemption = new Set()
@ -91,7 +82,20 @@ class ContentState {
}
setCursor () {
selection.setCursorRange(this.cursor)
const { start: { key } } = this.cursor
const block = this.getBlock(key)
if (block.type !== 'pre') {
selection.setCursorRange(this.cursor)
} else {
const cm = this.codeBlocks.get(key)
const { pos } = block
if (pos) {
cm.focus()
cm.setCursor(pos)
} else {
setCursorAtLastLine(cm)
}
}
}
render (isRenderCursor = true) {
@ -100,12 +104,19 @@ class ContentState {
matches.forEach((m, i) => {
m.active = i === index
})
this.stateRender.render(blocks, cursor, activeBlocks, matches)
const emptyPres = this.stateRender.render(blocks, cursor, activeBlocks, matches)
this.pre2CodeMirror(isRenderCursor, emptyPres)
if (isRenderCursor) this.setCursor()
this.pre2CodeMirror(isRenderCursor)
this.renderMath()
}
partialRender (block) {
const { cursor } = this
this.stateRender.partialRender(block, cursor)
this.setCursor()
this.renderMath(block)
}
/**
* A block in Aganippe present a paragraph(block syntax in GFM) or a line in paragraph.
* a line block must in a `p block` and `p block`'s children must be line blocks.
@ -137,11 +148,11 @@ class ContentState {
// getBlocks
getBlocks () {
for (const [ key, cm ] of this.codeBlocks.entries()) {
const value = cm.getValue()
const block = this.getBlock(key)
if (block) block.text = value
}
// for (const [ key, cm ] of this.codeBlocks.entries()) {
// const value = cm.getValue()
// const block = this.getBlock(key)
// if (block) block.text = value
// }
return this.blocks
}
@ -149,12 +160,22 @@ class ContentState {
return this.cursor
}
getArrayBlocks () {
return convertBlocksToArray(this.blocks)
}
getBlock (key) {
return this.getArrayBlocks().filter(block => block.key === key)[0]
let result = null
const travel = blocks => {
for (const block of blocks) {
if (block.key === key) {
result = block
return
}
const { children } = block
if (children.length) {
travel(children)
}
}
}
travel(this.blocks)
return result
}
getParent (block) {
@ -183,15 +204,6 @@ class ContentState {
return block.nextSibling ? this.getBlock(block.nextSibling) : null
}
getFirstBlock () {
const arrayBlocks = this.getArrayBlocks()
if (arrayBlocks.length) {
return arrayBlocks[0]
} else {
throw new Error('article need at least has one paragraph')
}
}
/**
* if target is descendant of parent return true, else return false
* @param {[type]} parent [description]
@ -459,39 +471,76 @@ class ContentState {
return null
}
getLastBlock () {
const arrayBlocks = this.getArrayBlocks()
const len = arrayBlocks.length
if (len) {
return arrayBlocks[len - 1]
} else {
throw new Error('article need at least has one paragraph')
firstInDescendant (block) {
const children = block.children
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (children.length) {
if (children[0].type === 'input' || (children[0].type === 'div' && children[0].editable === false)) { // handle task item
return this.firstInDescendant(children[1])
} else {
return this.firstInDescendant(children[0])
}
}
}
wordCount () {
const blocks = this.getBlocks()
let paragraph = blocks.length
lastInDescendant (block) {
if (block.children.length === 0 && HAS_TEXT_BLOCK_REG.test(block.type)) {
return block
} else if (block.children.length) {
const children = block.children
let lastChild = children[children.length - 1]
while (lastChild.editable === false) {
lastChild = this.getPreSibling(lastChild)
}
return this.lastInDescendant(lastChild)
}
}
findPreBlockInLocation (block) {
const parent = this.getParent(block)
const preBlock = this.getPreSibling(block)
if (block.preSibling && preBlock.type !== 'input' && preBlock.type !== 'div' && preBlock.editable !== false) { // handle task item and table
return this.lastInDescendant(preBlock)
} else if (parent) {
return this.findPreBlockInLocation(parent)
} else {
return null
}
}
findNextBlockInLocation (block) {
const parent = this.getParent(block)
const nextBlock = this.getNextSibling(block)
if (block.nextSibling && nextBlock.editable !== false) {
return this.firstInDescendant(nextBlock)
} else if (parent) {
return this.findNextBlockInLocation(parent)
} else {
return null
}
}
getLastBlock () {
const { blocks } = this
const len = blocks.length
return this.lastInDescendant(blocks[len - 1])
}
wordCount (markdown) {
let paragraph = this.blocks.length
let word = 0
let character = 0
let all = 0
const travel = block => {
if (block.text) {
const text = block.text
const removedChinese = text.replace(/[\u4e00-\u9fa5]/g, '')
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
const chineseWordLength = text.length - removedChinese.length
word += chineseWordLength + tokens.length
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
all += text.length
}
if (block.children.length) {
block.children.forEach(child => travel(child))
}
}
const removedChinese = markdown.replace(/[\u4e00-\u9fa5]/g, '')
const tokens = removedChinese.split(/[\s\n]+/).filter(t => t)
const chineseWordLength = markdown.length - removedChinese.length
word += chineseWordLength + tokens.length
character += tokens.reduce((acc, t) => acc + t.length, 0) + chineseWordLength
all += markdown.length
blocks.forEach(block => travel(block))
return { word, paragraph, character, all }
}

View File

@ -4,8 +4,9 @@ import { CLASS_OR_ID } from '../config'
import 'katex/dist/katex.min.css'
const mathCtrl = ContentState => {
ContentState.prototype.renderMath = function () {
const mathEles = document.querySelectorAll(`.${CLASS_OR_ID['AG_MATH_RENDER']}`)
ContentState.prototype.renderMath = function (block) {
const selector = block ? `#${block.key} .${CLASS_OR_ID['AG_MATH_RENDER']}` : `.${CLASS_OR_ID['AG_MATH_RENDER']}`
const mathEles = document.querySelectorAll(selector)
const { loadMathMap } = this
for (const math of mathEles) {
const content = math.getAttribute('data-math')

View File

@ -1,8 +1,8 @@
import selection from '../selection'
import { tokenizer } from '../parser/parse'
import { conflict } from '../utils'
import { conflict, isMetaKey } from '../utils'
import { getTextContent } from '../utils/domManipulate'
import { CLASS_OR_ID } from '../config'
import { CLASS_OR_ID, EVENT_KEYS } from '../config'
const INLINE_UPDATE_FREGMENTS = [
'^([*+-]\\s)', // Bullet list
@ -64,8 +64,7 @@ const updateCtrl = ContentState => {
switch (true) {
case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
this.updateHr(block, hr)
return true
return this.updateHr(block, hr)
case !!bullet:
this.updateList(block, 'bullet', bullet)
@ -81,8 +80,7 @@ const updateCtrl = ContentState => {
return true
case !!header:
this.updateHeader(block, header, text)
return true
return this.updateHeader(block, header, text)
case !!blockquote:
this.updateBlockQuote(block)
@ -96,11 +94,15 @@ const updateCtrl = ContentState => {
// thematic break
ContentState.prototype.updateHr = function (block, marker) {
block.type = 'hr'
block.text = marker
block.children.length = 0
const { key } = block
this.cursor.start.key = this.cursor.end.key = key
if (block.type !== 'hr') {
block.type = 'hr'
block.text = marker
block.children.length = 0
const { key } = block
this.cursor.start.key = this.cursor.end.key = key
return true
}
return false
}
ContentState.prototype.updateList = function (block, type, marker = '') {
@ -230,8 +232,10 @@ const updateCtrl = ContentState => {
block.type = newType
block.text = text
block.children.length = 0
this.cursor.start.key = this.cursor.end.key = block.key
return true
}
this.cursor.start.key = this.cursor.end.key = block.key
return false
}
ContentState.prototype.updateBlockQuote = function (block) {
@ -272,12 +276,16 @@ const updateCtrl = ContentState => {
const { start, end } = selection.getCursorRange()
const key = start.key
const block = this.getBlock(key)
// bugfix: #67 problem 1
if (block && block.icon) return event.preventDefault()
if (isMetaKey(event)) {
return
}
const { start: oldStart, end: oldEnd } = this.cursor
if (event.type === 'keyup' && (event.key === 'ArrowUp' || event.key === 'ArrowDown') && floatBox.show) {
if (event.type === 'keyup' && (event.key === EVENT_KEYS.ArrowUp || event.key === EVENT_KEYS.ArrowDown) && floatBox.show) {
return
}
@ -322,7 +330,6 @@ const updateCtrl = ContentState => {
end.offset !== oldEnd.offset
) {
this.cursor = { start, end }
// this.render()
}
return
}
@ -331,6 +338,7 @@ const updateCtrl = ContentState => {
const paragraph = document.querySelector(`#${key}`)
let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
let needRender = false
let isPartialRender = false
if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
// first cell in thead
const cursorBlock = block.children[1].children[0].children[0].children[0]
@ -342,6 +350,7 @@ const updateCtrl = ContentState => {
}
return this.render()
}
// remove temp block which generated by operation on code block
if (block && block.key !== oldKey) {
let oldBlock = this.getBlock(oldKey)
@ -362,7 +371,7 @@ const updateCtrl = ContentState => {
if (block && block.type === 'pre') {
if (block.key !== oldKey) {
this.cursor = lastCursor = { start, end }
this.render()
if (event.type === 'click') this.render()
}
return
}
@ -405,12 +414,16 @@ const updateCtrl = ContentState => {
needRender = true
}
if (key === oldKey) {
isPartialRender = true
}
this.cursor = lastCursor = { start, end }
const checkMarkedUpdate = this.checkNeedRender(block)
const checkInlineUpdate = this.isCollapse() && this.checkInlineUpdate(block)
if (checkMarkedUpdate || checkInlineUpdate || needRender) {
this.render()
isPartialRender && !checkInlineUpdate ? this.partialRender(block) : this.render()
}
}
}

View File

@ -1,7 +1,7 @@
.ag-float-box {
position: absolute;
left: -10000px;
top: -10000px;
left: -1000px;
top: -1000px;
opacity: 0;
max-height: 168px;
min-width: 130px;
@ -12,7 +12,7 @@
border-radius: 4px;
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, .1);
list-style: none;
transition: opacity .4s ease-in;
transition: opacity .15s ease-in;
overflow: auto;
background: #fff;
z-index: 10000;

View File

@ -110,8 +110,8 @@ class Aganippe {
dispatchChange () {
const { eventCenter } = this
const markdown = this.getMarkdown()
const wordCount = this.getWordCount()
const markdown = this.markdown = this.getMarkdown()
const wordCount = this.getWordCount(markdown)
const cursor = this.getCursor()
eventCenter.dispatch('change', markdown, wordCount, cursor)
}
@ -435,13 +435,12 @@ class Aganippe {
}
exportUnstylishHtml () {
const blocks = this.contentState.getBlocks()
const markdown = new ExportMarkdown(blocks).generate()
const { markdown } = this
return exportHtml(markdown)
}
getWordCount () {
return this.contentState.wordCount()
getWordCount (markdown) {
return this.contentState.wordCount(markdown)
}
getCursor () {

View File

@ -1,5 +1,5 @@
import virtualize from 'snabbdom-virtualize/strings'
import { LOWERCASE_TAGS, CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
import { CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
import { conflict, isLengthEven, union, isEven, getIdWithoutSet, loadImage, getImageSrc } from '../utils'
import { insertAfter, operateClassName } from '../utils/domManipulate'
import { tokenizer } from './parse'
@ -19,6 +19,7 @@ class StateRender {
constructor (eventCenter) {
this.eventCenter = eventCenter
this.loadImageMap = new Map()
this.tokenCache = new Map()
this.container = null
}
@ -52,11 +53,16 @@ class StateRender {
}
/**
* [render]: 2 steps:
* render vdom
* [render All blocks]
* @param {[Array]} blocks [description]
* @param {[Object]} cursor [description]
* @param {[Object]} activeBlocks [description]
* @param {[Array]} matches [description]
* @return {[undefined]} [description]
*/
render (blocks, cursor, activeBlocks, matches) {
const selector = `${LOWERCASE_TAGS.div}#${CLASS_OR_ID['AG_EDITOR_ID']}`
const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`
const emptyPres = []
const renderBlock = block => {
const type = block.type === 'hr' ? 'p' : block.type
@ -145,7 +151,14 @@ class StateRender {
const { text } = block
let children = ''
if (text) {
children = tokenizer(text, highlights).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
let tokens = null
if (highlights.length === 0 && this.tokenCache.has(text)) {
tokens = this.tokenCache.get(text)
} else {
tokens = tokenizer(text, highlights)
this.tokenCache.set(text, tokens)
}
children = tokens.reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
}
if (/th|td/.test(block.type)) {
@ -179,12 +192,6 @@ class StateRender {
Object.assign(data.dataset, { role: block.type })
}
if (block.type === 'pre') {
if (block.lang) Object.assign(data.dataset, { lang: block.lang })
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
children = ''
}
if (block.type === 'input') {
const { checked, type, key } = block
Object.assign(data.attrs, { type: 'checkbox' })
@ -200,6 +207,19 @@ class StateRender {
blockSelector += `.${CLASS_OR_ID['AG_TEMP']}`
}
if (block.type === 'pre') {
const { key, lang } = block
const pre = document.querySelector(`pre#${key}`)
if (pre) {
operateClassName(pre, isActive ? 'add' : 'remove', CLASS_OR_ID['AG_ACTIVE'])
return toVNode(pre)
}
if (lang) Object.assign(data.dataset, { lang })
blockSelector += block.functionType === 'code' ? `.${CLASS_OR_ID['AG_CODE_BLOCK']}` : `.${CLASS_OR_ID['AG_HTML_BLOCK']}`
children = ''
emptyPres.push(key)
}
return h(blockSelector, data, children)
}
}
@ -213,6 +233,48 @@ class StateRender {
const oldVdom = toVNode(rootDom)
patch(oldVdom, newVdom)
return emptyPres
}
partialRender (block, cursor) {
const { key, text } = block
const type = block.type === 'hr' ? 'p' : block.type
let selector = `${type}#${key}`
const oldDom = document.querySelector(selector)
const oldVdom = toVNode(oldDom)
selector += `.${CLASS_OR_ID['AG_PARAGRAPH']}.${CLASS_OR_ID['AG_ACTIVE']}`
if (type === 'span') {
selector += `.${CLASS_OR_ID['AG_LINE']}`
}
const data = {
attrs: {},
dataset: {}
}
let children = ''
if (text) {
children = tokenizer(text, []).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
}
if (/th|td/.test(type)) {
const { align } = block
if (align) {
Object.assign(data.attrs, { style: `text-align:${align}` })
}
}
if (/^h\d$/.test(block.type)) {
Object.assign(data.dataset, { head: block.type })
}
if (/^h/.test(block.type)) { // h\d or hr
Object.assign(data.dataset, { role: block.type })
}
const newVdom = h(selector, data, children)
patch(oldVdom, newVdom)
}
hr (h, cursor, block, token, outerClass) {

View File

@ -335,22 +335,27 @@ const importRegister = ContentState => {
ContentState.prototype.importCursor = function (cursor) {
// set cursor
if (cursor) {
const blocks = this.getArrayBlocks()
const travel = blocks => {
for (const block of blocks) {
const { text, key } = block
const { key, text, children } = block
if (text) {
const offset = block.text.indexOf(CURSOR_DNA)
const offset = text.indexOf(CURSOR_DNA)
if (offset > -1) {
// remove the CURSOR_DNA in the block text
block.text = text.substring(0, offset) + text.substring(offset + CURSOR_DNA.length)
this.cursor = {
start: { key, offset },
end: { key, offset }
}
return
}
}
if (children.length) {
travel(children)
}
}
}
if (cursor) {
travel(this.blocks)
} else {
const lastBlock = this.getLastBlock()
const key = lastBlock.key

View File

@ -43,7 +43,7 @@
created () {
this.$nextTick(() => {
const { markdown = '', theme } = this
const { markdown = '', theme, cursor } = this
const container = this.$refs.sourceCode
const codeMirrorConfig = {
value: markdown,
@ -60,11 +60,16 @@
}
}
if (theme === 'dark') codeMirrorConfig.theme = 'railscasts'
this.editor = codeMirror(container, codeMirrorConfig)
const editor = this.editor = codeMirror(container, codeMirrorConfig)
bus.$on('file-loaded', this.setMarkdown)
bus.$on('dotu-select', this.handleSelectDoutu)
this.listenChange()
if (cursor) {
editor.setCursor(cursor)
} else {
setCursorAtLastLine(editor)
}
})
},
beforeDestroy () {