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.
This commit is contained in:
Jonatan Heyman 2025-06-13 15:55:45 +02:00
parent 448a26e758
commit 0244ed84db
2 changed files with 53 additions and 31 deletions

View File

@ -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) {

View File

@ -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"
@ -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,16 +179,14 @@ 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({