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) 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 { class NoteBlockStart extends WidgetType {
constructor(isFirst) { constructor(isFirst) {

View File

@ -3,7 +3,7 @@ import { EditorView } from "@codemirror/view"
import { RangeSet } from "@codemirror/state" import { RangeSet } from "@codemirror/state"
import { FOLD_LABEL_LENGTH } from "@/src/common/constants.js" 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" import { transactionsHasAnnotation, ADD_NEW_BLOCK, transactionsHasHistoryEvent } from "./annotation.js"
@ -35,7 +35,7 @@ const autoUnfoldOnEdit = () => {
if (transactionsHasHistoryEvent(update.transactions)) { if (transactionsHasHistoryEvent(update.transactions)) {
return return
} }
// This fixes so that removing the previous block immediately after a folded block won't unfold the folded block // 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 // Since nothing was inserted, there is no risk of us putting extra characters into folded lines
if (update.changes.inserted.length === 0) { if (update.changes.inserted.length === 0) {
@ -116,32 +116,38 @@ export function foldGutterExtension() {
export const toggleBlockFold = (editor) => (view) => { export const toggleBlockFold = (editor) => (view) => {
const state = view.state const state = view.state
const folds = state.field(foldState, false) || RangeSet.empty const folds = state.field(foldState, false) || RangeSet.empty
const effects = []
state.selection.ranges.map(range => range.head).forEach((pos) => { const foldEffects = []
const block = getNoteBlockFromPos(state, pos) const unfoldEffects = []
let numFolded = 0, numUnfolded = 0
for (const block of getNoteBlocksFromRangeSet(state, state.selection.ranges)) {
const firstLine = state.doc.lineAt(block.content.from) const firstLine = state.doc.lineAt(block.content.from)
let blockIsFolded = false
const blockFolds = [] const blockFolds = []
folds.between(block.content.from, block.content.to, (from, to) => { folds.between(block.content.from, block.content.to, (from, to) => {
if (from <= firstLine.to && to === block.content.to) { if (from <= firstLine.to && to === block.content.to) {
blockIsFolded = true
blockFolds.push({from, to}) blockFolds.push({from, to})
} }
}) })
if (blockFolds.length > 0) { if (blockIsFolded) {
for (const range of blockFolds) { unfoldEffects.push(...blockFolds.map(range => unfoldEffect.of(range)))
// If there are folds in the block, unfold them numFolded++
effects.push(unfoldEffect.of(range))
}
} else { } else {
// If there are no folds in the block, fold it const range = {from: Math.min(firstLine.to, block.content.from + FOLD_LABEL_LENGTH), to: block.content.to}
const line = state.doc.lineAt(block.content.from) if (range.to > range.from) {
effects.push(foldEffect.of({from: Math.min(line.to, block.content.from + FOLD_LABEL_LENGTH), to: block.content.to})) 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({ view.dispatch({
effects: effects, effects: [...(numUnfolded >= numFolded ? foldEffects : unfoldEffects)],
}) })
} }
} }
@ -150,18 +156,20 @@ export const toggleBlockFold = (editor) => (view) => {
export const foldBlock = (editor) => (view) => { export const foldBlock = (editor) => (view) => {
const state = view.state const state = view.state
const blockRanges = [] 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({ 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 folds = state.field(foldState, false) || RangeSet.empty
const blockFolds = [] const blockFolds = []
state.selection.ranges.map(range => range.head).forEach((pos) => { for (const block of getNoteBlocksFromRangeSet(state, state.selection.ranges)) {
const block = getNoteBlockFromPos(state, pos)
const firstLine = state.doc.lineAt(block.content.from) const firstLine = state.doc.lineAt(block.content.from)
folds.between(block.content.from, block.content.to, (from, to) => { 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) { if (from <= firstLine.to && to === block.content.to) {
blockFolds.push({from, to}) blockFolds.push({from, to})
} }
}) })
}) }
if (blockFolds.length > 0) { if (blockFolds.length > 0) {
view.dispatch({ view.dispatch({
effects: blockFolds.map(range => unfoldEffect.of(range)), effects: blockFolds.map(range => unfoldEffect.of(range)),
}) })
} }
} }