diff --git a/src/editor/block/block.js b/src/editor/block/block.js
index 8e3cc07f..66cf1df0 100644
--- a/src/editor/block/block.js
+++ b/src/editor/block/block.js
@@ -1,6 +1,6 @@
import { ViewPlugin, EditorView, Decoration, WidgetType, lineNumbers } from "@codemirror/view"
import { layer, RectangleMarker } from "@codemirror/view"
-import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet} from "@codemirror/state";
+import { EditorState, RangeSetBuilder, StateField, Facet , StateEffect, RangeSet, MapMode} from "@codemirror/state";
import { syntaxTree, ensureSyntaxTree } from "@codemirror/language"
import { Note, Document, NoteDelimiter } from "../lang-heynote/parser.terms.js"
import { IterMode } from "@lezer/common";
@@ -229,20 +229,33 @@ const blockLayer = layer({
const preventFirstBlockFromBeingDeleted = EditorState.changeFilter.of((tr) => {
//console.log("change filter!", tr)
const protect = []
- if (!tr.annotations.some(a => a.type === heynoteEvent) && firstBlockDelimiterSize) {
+ if (tr.annotation(heynoteEvent) === undefined && firstBlockDelimiterSize) {
protect.push(0, firstBlockDelimiterSize)
}
// if the transaction is a search and replace, we want to protect all block delimiters
- if (tr.annotations.some(a => a.value === "input.replace" || a.value === "input.replace.all")) {
+ // `isUserEvent` grabs any annotation that is equal to the specified or more specific too
+ // in this case, it returns true to "input.replace" and "input.replace.all".
+ if (tr.isUserEvent("input.replace")) {
const blocks = tr.startState.facet(blockState)
blocks.forEach(block => {
protect.push(block.delimiter.from, block.delimiter.to)
})
//console.log("protected ranges:", protect)
}
- if (protect.length > 0) {
- return protect
+ return protect;
+})
+
+/**
+ * Fix for the default Codemirror keymap "Ctrl-Shift-K", which deletes
+ * lines, but breaks HeynoteBlock delimiter syntax when deleting entire blocks
+ */
+const preventBlockDelimiterDeletion = EditorState.changeFilter.of((tr)=>{
+ // Regular line deletes dont actually trigger this
+ if(tr.isUserEvent("delete.line")){
+ let activeBlock = getActiveNoteBlock(tr.startState);
+ return [activeBlock.delimiter.from, activeBlock.delimiter.to]
}
+ return true;
})
/**
@@ -255,12 +268,16 @@ const preventSelectionBeforeFirstBlock = EditorState.transactionFilter.of((tr) =
tr?.selection?.ranges.forEach(range => {
// change the selection to after the first block if the transaction sets the selection before the first block
if (range && range.from < firstBlockDelimiterSize) {
- range.from = firstBlockDelimiterSize
- //console.log("changing the from selection to", markerSize)
+ // `range.to` & `range.from` should be read-only properties, so we need a new range from the old one:
+ //range.from = firstBlockDelimiterSize
+ tr.selection.replaceRange(Object.assign(range, {from:firstBlockDelimiterSize}))
+ //console.log("changing the from selection to", tr.selection)
}
if (range && range.to < firstBlockDelimiterSize) {
- range.to = firstBlockDelimiterSize
- //console.log("changing the from selection to", markerSize)
+ // `range.to` & `range.from` should be read-only properties, so we need a new range from the old one:
+ //range.to = firstBlockDelimiterSize
+ tr.selection.replaceRange(Object.assign(range, {to:firstBlockDelimiterSize}))
+ //console.log("changing the from selection to", tr.selection)
}
})
return tr
@@ -311,7 +328,7 @@ const emitCursorChange = (editor) => ViewPlugin.fromClass(
update(update) {
// if the selection changed or the language changed (can happen without selection change),
// emit a selection change event
- const langChange = update.transactions.some(tr => tr.annotations.some(a => a.value == LANGUAGE_CHANGE))
+ const langChange = update.transactions.some(tr => tr.isUserEvent(LANGUAGE_CHANGE))
if (update.selectionSet || langChange) {
const cursorLine = getBlockLineFromPos(update.state, update.state.selection.main.head)
@@ -344,5 +361,6 @@ export const noteBlockExtension = (editor) => {
emitCursorChange(editor),
mathBlock,
emptyBlockSelected,
+ preventBlockDelimiterDeletion
]
}
diff --git a/src/editor/copy-paste.js b/src/editor/copy-paste.js
index dc0f1d3a..fab2dfe6 100644
--- a/src/editor/copy-paste.js
+++ b/src/editor/copy-paste.js
@@ -8,7 +8,14 @@ import { setEmacsMarkMode } from "./emacs.js"
const languageTokensMatcher = LANGUAGES.map(l => l.token).join("|")
const blockSeparatorRegex = new RegExp(`\\nāāā(${languageTokensMatcher})(-a)?\\n`, "g")
-
+/**
+ * Given a `EditorState`, returns a object containing
+ * the current selection content as a `string`, alongside a array of ranges.
+ * @param {EditorState} state The state object of the editor.
+ * @returns {Object} object with:
+ * - `text`: the selection content
+ * - `ranges`: a array of selection ranges
+ */
function copiedRange(state) {
let content = [], ranges = []
for (let range of state.selection.ranges) if (!range.empty) {
diff --git a/src/editor/lang-heynote/nested-parser.js b/src/editor/lang-heynote/nested-parser.js
index 4b5aa8e3..2134ae3a 100644
--- a/src/editor/lang-heynote/nested-parser.js
+++ b/src/editor/lang-heynote/nested-parser.js
@@ -15,7 +15,12 @@ import { LANGUAGES } from "../languages.js"
const languageMapping = Object.fromEntries(LANGUAGES.map(l => [l.token, l.parser]))
-
+/**
+ * Creates a parse wrapper that, after parsing the HeyNote language syntax
+ * (such as language tokens `NoteDelimiter` and content `NoteContent`) applies
+ * another parser to the data, attaching the resulting nodes to the syntax tree, with all their props.
+ * @returns {ParseWrapper} a custom Heynote `ParseWrapper`.
+ */
export function configureNesting() {
return parseMixed((node, input) => {
let id = node.type.id