Sorry for the late response. Here is a minimal reproduction of the issue (I used the CSS class cm-comment
in my marker):
import {
EditorSelection,
RangeSet,
RangeSetBuilder,
SelectionRange,
StateEffect,
StateField,
type Transaction,
type StateCommand,
} from "@codemirror/state";
import {
Decoration,
type EditorView,
ViewPlugin,
type ViewUpdate,
type DecorationSet,
type KeyBinding,
} from "@codemirror/view";
import { isMatch } from "lodash-es";
const addComment = StateEffect.define<EditorSelection>();
function cleanRangesOf(selection: EditorSelection) {
const newRanges = selection.ranges.filter(
(range) => range.from !== range.to,
);
return newRanges.length > 0
? EditorSelection.create(newRanges, selection.mainIndex)
: null;
}
export const commentField = StateField.define<EditorSelection[]>({
create(): EditorSelection[] {
return [];
},
update(oldComments: EditorSelection[], tr: Transaction): EditorSelection[] {
let comments = oldComments;
for (const e of tr.effects) {
if (e.is(addComment)) {
// XXX: Not sure if this is the right attribute to use
comments = [...comments, e.value];
}
}
// Map our old annotations to the new state
// ranges, as we don't want our annotations/highlighted portion
// to be static markers of a row and column but instead change with te
comments = comments
.map((x) => cleanRangesOf(x.map(tr.changes, 1)))
.filter((x) => x !== null);
return comments;
},
});
const commentsChanged = (update: ViewUpdate) =>
update.startState.field(commentField).every((val, idx) =>
// OPTIMIZE: this may not be performant?
// perhaps have a "last updated" field
isMatch(val, update.state.field(commentField)[idx]),
) ||
update.transactions.some((tr) => tr.effects.some((e) => e.is(addComment)));
const commentDecorations = ViewPlugin.fromClass(
class {
decorations: DecorationSet;
constructor(view: EditorView) {
this.decorations = this.getDecorations(view, "cm-comment");
}
update(update: ViewUpdate) {
// update.selectionSet also means "if cursor changed"
if (
update.selectionSet ||
update.docChanged ||
commentsChanged(update)
) {
this.decorations = this.getDecorations(
update.view,
"cm-comment",
);
}
}
getDecorations(view: EditorView, classPrefix: string): DecorationSet {
// TODO: optimize algorithm to be linear time complexity
// using some sort of greedy algorithm
const builder = new RangeSetBuilder<Decoration>();
// We can assume a single selection
// because we are not implementing multi-selection support
// for now
const annotationRanges = view.state
.field(commentField)
.map((x) => x.main);
// If you don't add annotations in order, the plugin will crash
annotationRanges.sort((a, b) => a.from - b.from);
console.log(annotationRanges);
// TODO: care about multiple selections
for (const { from, to } of annotationRanges) {
console.log("adding mark");
builder.add(
from,
to,
Decoration.mark({
class: classPrefix,
inclusive: true,
}),
);
}
return builder.finish();
}
},
{
decorations: (v) => v.decorations,
},
);
const createCommentCommand: StateCommand = ({ state, dispatch }) => {
// TODO: multi selection support
if (state.selection.main.empty) return false;
console.log("asd");
dispatch(
state.update({
effects: [addComment.of(state.selection)],
}),
);
return true;
};
export const commentKeymap: KeyBinding[] = [
{
key: "Mod-Alt-x",
run: createCommentCommand,
},
];
export const comments = () => [commentField, commentDecorations];