Hello. I’m trying to create piece of functionality that parses comments and does some actions on the back of it. However I struggle to understand the best way to intercept when a comment gets partially deleted (e.g. by removing closing */
) to delete it completely.
Information about comments of interest is stored in the StateField
. For visualisation the /*
and */
of comments are hidden with Decoration.replace
, and comment itself is decorated with Decoration.mark
.
I struggle to initiate view.dispatch
from within StateField
(if the range becomes “broken”). If try to use ViewPlugin
have an error Calls to EditorView.update are not allowed while an update is in progress
.
What is the most optimal way to achieve the behaviour?
Example of code:
import * as cmView from '@codemirror/view'
import * as cmRangeSet from '@codemirror/rangeset'
import * as cmState from "@codemirror/state"
const TAG_PREFIX = '/* A:'
const TAG_POSTFIX = ' */'
const themeBlock = cmView.EditorView.baseTheme({
'.cm-decorated-line': {
borderStyle: 'solid',
borderWidth: '1px',
borderRadius: '3px',
borderColor: 'rgb(150, 150, 150)',
background: 'rgb(220, 219, 169)',
padding: '3px'
},
})
const markBlock = cmView.Decoration.mark({ class: 'cm-decorated-line' })
const hideBlock = cmView.Decoration.replace({ inclusive: false })
const effectStart = cmState.StateEffect.define()
const fieldBlocks = cmState.StateField.define({
create: () => cmRangeSet.RangeSet.empty,
update: (value, tr) => {
value = value.map(tr.changes)
// THE BELOW DOES NOT WORK
// let toRemove = []
// value = value.update({
// filter: (from, to, value) => {
// const text = tr.state.doc.sliceString(from, to)
// const keep = text.startsWith(TAG_PREFIX) && text.endsWith(TAG_POSTFIX)
// if (!keep) toRemove.push({ from, to })
// return keep
// }
// })
// if (toRemove.length) {
// console.log(toRemove)
// tr.state.update({ changes: toRemove })
// }
// add new block
let newBlock = tr.effects.find(e => e.is(effectStart))
if (newBlock) {
value = value.update({
add: [new cmRangeSet.Range(
newBlock.value.from,
newBlock.value.to,
{ type: null, params: [] }
)]
})
}
return value
},
provide: (field) => cmView.EditorView.decorations.from(field, (value) => {
const prefix = TAG_PREFIX.length, postfix = TAG_POSTFIX.length
let decorations = []
for (let iter = value.iter(); iter.value !== null; iter.next()) {
decorations.push(new cmRangeSet.Range(iter.from, iter.from + prefix, hideBlock))
decorations.push(new cmRangeSet.Range(iter.from + prefix, iter.to - postfix, markBlock))
decorations.push(new cmRangeSet.Range(iter.to - postfix, iter.to, hideBlock))
}
return cmRangeSet.RangeSet.of(decorations, true)
})
})
const assistPlugin = cmView.ViewPlugin.fromClass(class {
constructor(view) {
console.log('plugin > constructor')
}
update(view) {
console.log('plugin > update')
if (view.docChanged) {
const ranges = view.state.field(fieldBlocks)
let changes = []
for (let iter = ranges.iter(); iter.value !== null; iter.next()) {
const text = view.state.doc.sliceString(iter.from, iter.to)
if (text.startsWith(TAG_PREFIX) && text.endsWith(TAG_POSTFIX))
changes.push({ from: iter.from, to: iter.to })
}
// THE BELOW FAILS
view.view.dispatch({changes: changes})
}
}
})
function commandStartAssist(view) {
let range = view.state.selection.main
if (view.state.readOnly || !range.empty) return false
// TODO: prevent adding inside existing range
const tag = TAG_PREFIX + ' ' + TAG_POSTFIX
view.dispatch({
changes: {
from: range.head,
insert: tag
},
effects: effectStart.of({
from: range.head,
to: range.head + tag.length
}),
selection: cmState.EditorSelection.cursor(range.head + TAG_PREFIX.length + 1)
})
return true
}
const keymap = cmView.keymap.of([{
key: 'Ctrl-a',
preventDefault: true,
run: commandStartAssist
}])
export const assist = [fieldBlocks, themeBlock, assistPlugin, keymap]