From 0244ed84dbe5d8eeff04777aae9698f4082a2c97 Mon Sep 17 00:00:00 2001 From: Jonatan Heyman Date: Fri, 13 Jun 2025 15:55:45 +0200 Subject: [PATCH] Make foldBlock, unfoldBlock and toggleBlockFold work on multiple blocks when the cursor selection(s) covers multiple blocks. Changed so that the toggleBlockFolds either folds or unfolds all blocks in the selection, instead of swapping the state for each block. --- src/editor/block/block.js | 16 +++++++++ src/editor/fold-gutter.js | 68 +++++++++++++++++++++------------------ 2 files changed, 53 insertions(+), 31 deletions(-) diff --git a/src/editor/block/block.js b/src/editor/block/block.js index ce5ed0f..ea30a65 100644 --- a/src/editor/block/block.js +++ b/src/editor/block/block.js @@ -55,6 +55,22 @@ export function getNoteBlockFromPos(state, pos) { return state.facet(blockState).find(block => block.range.from <= pos && block.range.to >= pos) } +export function getNoteBlocksBetween(state, from, to) { + return state.facet(blockState).filter(block => block.range.from < to && block.range.to >= from) +} + +export function getNoteBlocksFromRangeSet(state, ranges) { + const blocks = [] + const seenBlockStarts = new Set() + for (const range of ranges) { + if (!seenBlockStarts.has(range.from)) { + blocks.push(...getNoteBlocksBetween(state, range.from, range.to)) + seenBlockStarts.add(range.from) + } + } + return blocks +} + class NoteBlockStart extends WidgetType { constructor(isFirst) { diff --git a/src/editor/fold-gutter.js b/src/editor/fold-gutter.js index 11dbe25..3ac88af 100644 --- a/src/editor/fold-gutter.js +++ b/src/editor/fold-gutter.js @@ -3,7 +3,7 @@ import { EditorView } from "@codemirror/view" import { RangeSet } from "@codemirror/state" import { FOLD_LABEL_LENGTH } from "@/src/common/constants.js" -import { getNoteBlockFromPos } from "./block/block.js" +import { getNoteBlockFromPos, getNoteBlocksFromRangeSet } from "./block/block.js" import { transactionsHasAnnotation, ADD_NEW_BLOCK, transactionsHasHistoryEvent } from "./annotation.js" @@ -35,7 +35,7 @@ const autoUnfoldOnEdit = () => { if (transactionsHasHistoryEvent(update.transactions)) { return } - + // This fixes so that removing the previous block immediately after a folded block won't unfold the folded block // Since nothing was inserted, there is no risk of us putting extra characters into folded lines if (update.changes.inserted.length === 0) { @@ -116,32 +116,38 @@ export function foldGutterExtension() { export const toggleBlockFold = (editor) => (view) => { const state = view.state const folds = state.field(foldState, false) || RangeSet.empty - const effects = [] - state.selection.ranges.map(range => range.head).forEach((pos) => { - const block = getNoteBlockFromPos(state, pos) + const foldEffects = [] + const unfoldEffects = [] + let numFolded = 0, numUnfolded = 0 + + for (const block of getNoteBlocksFromRangeSet(state, state.selection.ranges)) { const firstLine = state.doc.lineAt(block.content.from) + let blockIsFolded = false const blockFolds = [] folds.between(block.content.from, block.content.to, (from, to) => { if (from <= firstLine.to && to === block.content.to) { + blockIsFolded = true blockFolds.push({from, to}) } }) - if (blockFolds.length > 0) { - for (const range of blockFolds) { - // If there are folds in the block, unfold them - effects.push(unfoldEffect.of(range)) - } + if (blockIsFolded) { + unfoldEffects.push(...blockFolds.map(range => unfoldEffect.of(range))) + numFolded++ } else { - // If there are no folds in the block, fold it - const line = state.doc.lineAt(block.content.from) - effects.push(foldEffect.of({from: Math.min(line.to, block.content.from + FOLD_LABEL_LENGTH), to: block.content.to})) + const range = {from: Math.min(firstLine.to, block.content.from + FOLD_LABEL_LENGTH), to: block.content.to} + if (range.to > range.from) { + foldEffects.push(foldEffect.of(range)) + } + numUnfolded++ } - }) + } - if (effects.length > 0) { + if (foldEffects.length > 0 || unfoldEffects.length > 0) { + // if multiple blocks are selected, instead of flipping the fold state of all blocks, + // we'll fold all blocks if more blocks are unfolded than folded, and unfold all blocks otherwise view.dispatch({ - effects: effects, + effects: [...(numUnfolded >= numFolded ? foldEffects : unfoldEffects)], }) } } @@ -150,18 +156,20 @@ export const toggleBlockFold = (editor) => (view) => { export const foldBlock = (editor) => (view) => { const state = view.state const blockRanges = [] - state.selection.ranges.map(range => range.head).forEach((pos) => { - const block = getNoteBlockFromPos(state, pos) - if (block) { - const line = state.doc.lineAt(block.content.from) - blockRanges.push({from: Math.min(line.to, block.content.from + FOLD_LABEL_LENGTH), to: block.content.to}) - } - }) - const uniqueBlockRanges = [...new Set(blockRanges.map(JSON.stringify))].map(JSON.parse); - if (uniqueBlockRanges.length > 0) { + for (const block of getNoteBlocksFromRangeSet(state, state.selection.ranges)) { + const line = state.doc.lineAt(block.content.from) + // fold the block content, but only the first line + const from = Math.min(line.to, block.content.from + FOLD_LABEL_LENGTH) + const to = block.content.to + if (from < to) { + // skip empty ranges + blockRanges.push({from, to}) + } + } + if (blockRanges.length > 0) { view.dispatch({ - effects: uniqueBlockRanges.map(range => foldEffect.of(range)), + effects: blockRanges.map(range => foldEffect.of(range)), }) } } @@ -171,20 +179,18 @@ export const unfoldBlock = (editor) => (view) => { const folds = state.field(foldState, false) || RangeSet.empty const blockFolds = [] - state.selection.ranges.map(range => range.head).forEach((pos) => { - const block = getNoteBlockFromPos(state, pos) + for (const block of getNoteBlocksFromRangeSet(state, state.selection.ranges)) { const firstLine = state.doc.lineAt(block.content.from) folds.between(block.content.from, block.content.to, (from, to) => { - //console.log("Fold in block", from, to, "block", block.content.from, block.content.to, firstLine.to) if (from <= firstLine.to && to === block.content.to) { blockFolds.push({from, to}) } }) - }) + } if (blockFolds.length > 0) { view.dispatch({ effects: blockFolds.map(range => unfoldEffect.of(range)), }) } -} \ No newline at end of file +}