mirror of
https://github.com/heyman/heynote.git
synced 2025-06-27 13:01:51 +02:00
Improve block and code folding
- Display the number of folded lines for a folded region - Add commands for folding/unfolding/toggling blocks and assigne them default key bindings - Add toggleFold command (toggles the nearest foldable range) - Prevent editor from loosing focus when fold gutter is clicked - Fix so that a folded region is automatically unfolded if any changes happen on either the start line or the end line of the folded region (even if the change is not within the folded region)
This commit is contained in:
parent
bc863f20fa
commit
a7a4c73bae
@ -20,3 +20,5 @@ export const UPDATE_DOWNLOAD_PROGRESS = "update-download-progress"
|
|||||||
export const UPDATE_START_DOWNLOAD = "auto-update:startDownload"
|
export const UPDATE_START_DOWNLOAD = "auto-update:startDownload"
|
||||||
export const UPDATE_INSTALL_AND_RESTART = "auto-update:installAndRestart"
|
export const UPDATE_INSTALL_AND_RESTART = "auto-update:installAndRestart"
|
||||||
export const UPDATE_CHECK_FOR_UPDATES = "auto-update:checkForUpdates"
|
export const UPDATE_CHECK_FOR_UPDATES = "auto-update:checkForUpdates"
|
||||||
|
|
||||||
|
export const FOLD_LABEL_LENGTH = 50
|
||||||
|
@ -11,7 +11,7 @@ import {
|
|||||||
insertNewlineAndIndent,
|
insertNewlineAndIndent,
|
||||||
toggleComment, toggleBlockComment, toggleLineComment,
|
toggleComment, toggleBlockComment, toggleLineComment,
|
||||||
} from "@codemirror/commands"
|
} from "@codemirror/commands"
|
||||||
import { foldCode, unfoldCode } from "@codemirror/language"
|
import { foldCode, unfoldCode, toggleFold } from "@codemirror/language"
|
||||||
import { selectNextOccurrence } from "@codemirror/search"
|
import { selectNextOccurrence } from "@codemirror/search"
|
||||||
import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown"
|
import { insertNewlineContinueMarkup } from "@codemirror/lang-markdown"
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ import { cutCommand, copyCommand, pasteCommand } from "./copy-paste.js"
|
|||||||
|
|
||||||
import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./mark-mode.js"
|
import { markModeMoveCommand, toggleSelectionMarkMode, selectionMarkModeCancel } from "./mark-mode.js"
|
||||||
import { insertDateAndTime } from "./date-time.js"
|
import { insertDateAndTime } from "./date-time.js"
|
||||||
|
import { foldBlock, unfoldBlock, toggleBlockFold } from "./fold-gutter.js"
|
||||||
|
|
||||||
|
|
||||||
const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock)
|
const cursorPreviousBlock = markModeMoveCommand(gotoPreviousBlock, selectPreviousBlock)
|
||||||
@ -101,6 +102,9 @@ const HEYNOTE_COMMANDS = {
|
|||||||
openCreateNewBuffer: cmd(openCreateNewBuffer, "Buffer", "Create new buffer…"),
|
openCreateNewBuffer: cmd(openCreateNewBuffer, "Buffer", "Create new buffer…"),
|
||||||
cut: cmd(cutCommand, "Clipboard", "Cut selection"),
|
cut: cmd(cutCommand, "Clipboard", "Cut selection"),
|
||||||
copy: cmd(copyCommand, "Clipboard", "Copy selection"),
|
copy: cmd(copyCommand, "Clipboard", "Copy selection"),
|
||||||
|
foldBlock: cmd(foldBlock, "Block", "Fold block"),
|
||||||
|
unfoldBlock: cmd(unfoldBlock, "Block", "Unfold block"),
|
||||||
|
toggleBlockFold: cmd(toggleBlockFold, "Block", "Toggle block fold"),
|
||||||
|
|
||||||
// commands without editor context
|
// commands without editor context
|
||||||
paste: cmdLessContext(pasteCommand, "Clipboard", "Paste from clipboard"),
|
paste: cmdLessContext(pasteCommand, "Clipboard", "Paste from clipboard"),
|
||||||
@ -127,6 +131,7 @@ const HEYNOTE_COMMANDS = {
|
|||||||
indentLess: cmdLessContext(indentLess, "Edit", "Indent less"),
|
indentLess: cmdLessContext(indentLess, "Edit", "Indent less"),
|
||||||
foldCode: cmdLessContext(foldCode, "Edit", "Fold code"),
|
foldCode: cmdLessContext(foldCode, "Edit", "Fold code"),
|
||||||
unfoldCode: cmdLessContext(unfoldCode, "Edit", "Unfold code"),
|
unfoldCode: cmdLessContext(unfoldCode, "Edit", "Unfold code"),
|
||||||
|
toggleFold: cmdLessContext(toggleFold, "Edit", "Toggle fold"),
|
||||||
selectNextOccurrence: cmdLessContext(selectNextOccurrence, "Cursor", "Select next occurrence"),
|
selectNextOccurrence: cmdLessContext(selectNextOccurrence, "Cursor", "Select next occurrence"),
|
||||||
deleteCharBackward: cmdLessContext(deleteCharBackward, "Edit", "Delete character backward"),
|
deleteCharBackward: cmdLessContext(deleteCharBackward, "Edit", "Delete character backward"),
|
||||||
deleteCharForward: cmdLessContext(deleteCharForward, "Edit", "Delete character forward"),
|
deleteCharForward: cmdLessContext(deleteCharForward, "Edit", "Delete character forward"),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction, Prec } from "@codemirror/state"
|
import { Annotation, EditorState, Compartment, Facet, EditorSelection, Transaction, Prec } from "@codemirror/state"
|
||||||
import { EditorView, keymap as cmKeymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
import { EditorView, keymap as cmKeymap, drawSelection, ViewPlugin, lineNumbers } from "@codemirror/view"
|
||||||
import { foldGutter, ensureSyntaxTree } from "@codemirror/language"
|
import { ensureSyntaxTree } from "@codemirror/language"
|
||||||
import { markdown, markdownKeymap } from "@codemirror/lang-markdown"
|
import { markdown, markdownKeymap } from "@codemirror/lang-markdown"
|
||||||
import { undo, redo } from "@codemirror/commands"
|
import { undo, redo } from "@codemirror/commands"
|
||||||
|
|
||||||
@ -27,6 +27,7 @@ import { NoteFormat } from "../common/note-format.js"
|
|||||||
import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
|
import { AUTO_SAVE_INTERVAL } from "../common/constants.js"
|
||||||
import { useHeynoteStore } from "../stores/heynote-store.js";
|
import { useHeynoteStore } from "../stores/heynote-store.js";
|
||||||
import { useErrorStore } from "../stores/error-store.js";
|
import { useErrorStore } from "../stores/error-store.js";
|
||||||
|
import { foldGutterExtension } from "./fold-gutter.js"
|
||||||
|
|
||||||
|
|
||||||
// Turn off the use of EditContext, since Chrome has a bug (https://issues.chromium.org/issues/351029417)
|
// Turn off the use of EditContext, since Chrome has a bug (https://issues.chromium.org/issues/351029417)
|
||||||
@ -85,7 +86,7 @@ export class HeynoteEditor {
|
|||||||
//minimalSetup,
|
//minimalSetup,
|
||||||
this.lineNumberCompartment.of(showLineNumberGutter ? blockLineNumbers : []),
|
this.lineNumberCompartment.of(showLineNumberGutter ? blockLineNumbers : []),
|
||||||
customSetup,
|
customSetup,
|
||||||
this.foldGutterCompartment.of(showFoldGutter ? [foldGutter()] : []),
|
this.foldGutterCompartment.of(showFoldGutter ? [foldGutterExtension()] : []),
|
||||||
this.closeBracketsCompartment.of(bracketClosing ? [getCloseBracketsExtensions()] : []),
|
this.closeBracketsCompartment.of(bracketClosing ? [getCloseBracketsExtensions()] : []),
|
||||||
|
|
||||||
this.readOnlyCompartment.of([]),
|
this.readOnlyCompartment.of([]),
|
||||||
@ -375,7 +376,7 @@ export class HeynoteEditor {
|
|||||||
|
|
||||||
setFoldGutter(show) {
|
setFoldGutter(show) {
|
||||||
this.view.dispatch({
|
this.view.dispatch({
|
||||||
effects: this.foldGutterCompartment.reconfigure(show ? [foldGutter()] : []),
|
effects: this.foldGutterCompartment.reconfigure(show ? foldGutterExtension : []),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
169
src/editor/fold-gutter.js
Normal file
169
src/editor/fold-gutter.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
import { codeFolding, foldGutter, foldState, unfoldEffect, foldEffect } from "@codemirror/language"
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
// This extension fixes so that a folded region is automatically unfolded if any changes happen
|
||||||
|
// on either the start line or the end line of the folded region (even if the change is not within the folded region)
|
||||||
|
const autoUnfoldOnEdit = () => {
|
||||||
|
return EditorView.updateListener.of((update) => {
|
||||||
|
if (!update.docChanged){
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { state, view } = update;
|
||||||
|
const foldRanges = state.field(foldState, false);
|
||||||
|
|
||||||
|
if (!foldRanges || foldRanges.size === 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const unfoldRanges = []
|
||||||
|
|
||||||
|
update.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||||
|
foldRanges.between(0, state.doc.length, (from, to) => {
|
||||||
|
const lineFrom = state.doc.lineAt(from).from
|
||||||
|
const lineTo = state.doc.lineAt(to).to;
|
||||||
|
|
||||||
|
if ((fromA >= lineFrom && fromA <= lineTo) || (toA >= lineFrom && toA <= lineTo)) {
|
||||||
|
unfoldRanges.push({ from, to });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
//console.log("Unfold ranges:", unfoldRanges);
|
||||||
|
if (unfoldRanges.length > 0) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: unfoldRanges.map(range => unfoldEffect.of(range)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function foldGutterExtension() {
|
||||||
|
return [
|
||||||
|
foldGutter({
|
||||||
|
domEventHandlers: {
|
||||||
|
click(view, line, event) {
|
||||||
|
// editor should not loose focus when clicking on the fold gutter
|
||||||
|
view.docView.dom.focus()
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
codeFolding(
|
||||||
|
{
|
||||||
|
//placeholderText: "⯈ Folded",
|
||||||
|
preparePlaceholder: (state, {from, to}) => {
|
||||||
|
// Count the number of lines in the folded range
|
||||||
|
const firstLine = state.doc.lineAt(from)
|
||||||
|
const lineFrom = firstLine.number
|
||||||
|
const lineTo = state.doc.lineAt(to).number
|
||||||
|
const lineCount = lineTo - lineFrom + 1
|
||||||
|
|
||||||
|
const label = firstLine.text
|
||||||
|
//console.log("label", label, "line", firstLine)
|
||||||
|
const labelDom = document.createElement("span")
|
||||||
|
labelDom.textContent = label.slice(0, 100)
|
||||||
|
|
||||||
|
const linesDom = document.createElement("span")
|
||||||
|
linesDom.textContent = `${label.slice(-1).trim() === "" ? '' : ' '}… (${lineCount} lines)`
|
||||||
|
linesDom.style.fontStyle = "italic"
|
||||||
|
|
||||||
|
const dom = document.createElement("span")
|
||||||
|
dom.className = "cm-foldPlaceholder"
|
||||||
|
dom.style.opacity = "0.6"
|
||||||
|
if (firstLine.from === from) {
|
||||||
|
dom.appendChild(labelDom)
|
||||||
|
}
|
||||||
|
dom.appendChild(linesDom)
|
||||||
|
return dom
|
||||||
|
},
|
||||||
|
placeholderDOM: (view, onClick, prepared) => {
|
||||||
|
prepared.addEventListener("click", onClick)
|
||||||
|
return prepared
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
autoUnfoldOnEdit(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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 firstLine = state.doc.lineAt(block.content.from)
|
||||||
|
const blockFolds = []
|
||||||
|
folds.between(block.content.from, block.content.to, (from, to) => {
|
||||||
|
if (from <= firstLine.to && to === block.content.to) {
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
} 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}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (effects.length > 0) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: effects,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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) {
|
||||||
|
view.dispatch({
|
||||||
|
effects: uniqueBlockRanges.map(range => foldEffect.of(range)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const unfoldBlock = (editor) => (view) => {
|
||||||
|
const state = view.state
|
||||||
|
const folds = state.field(foldState, false) || RangeSet.empty
|
||||||
|
const blockFolds = []
|
||||||
|
|
||||||
|
state.selection.ranges.map(range => range.head).forEach((pos) => {
|
||||||
|
const block = getNoteBlockFromPos(state, pos)
|
||||||
|
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)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -89,6 +89,17 @@ export const DEFAULT_KEYMAP = [
|
|||||||
|
|
||||||
cmd("Mod-/", "toggleComment"),
|
cmd("Mod-/", "toggleComment"),
|
||||||
cmd("Alt-Shift-a", "toggleBlockComment"),
|
cmd("Alt-Shift-a", "toggleBlockComment"),
|
||||||
|
|
||||||
|
// fold blocks
|
||||||
|
...(isMac ? [
|
||||||
|
cmd("Alt-Mod-[", "foldBlock"),
|
||||||
|
cmd("Alt-Mod-]", "unfoldBlock"),
|
||||||
|
cmd("Alt-Mod-.", "toggleBlockFold")
|
||||||
|
] : [
|
||||||
|
cmd("Ctrl-Shift-[", "foldBlock"),
|
||||||
|
cmd("Ctrl-Shift-]", "unfoldBlock"),
|
||||||
|
cmd("Ctrl-Shift-.", "toggleBlockFold")
|
||||||
|
]),
|
||||||
|
|
||||||
// search
|
// search
|
||||||
//cmd("Mod-f", "openSearchPanel"),
|
//cmd("Mod-f", "openSearchPanel"),
|
||||||
|
@ -7,6 +7,8 @@ import {styleTags, tags as t} from "@lezer/highlight"
|
|||||||
import { json } from "@codemirror/lang-json"
|
import { json } from "@codemirror/lang-json"
|
||||||
import { javascript } from "@codemirror/lang-javascript"
|
import { javascript } from "@codemirror/lang-javascript"
|
||||||
|
|
||||||
|
import { FOLD_LABEL_LENGTH } from "@/src/common/constants.js"
|
||||||
|
|
||||||
|
|
||||||
function foldNode(node) {
|
function foldNode(node) {
|
||||||
//console.log("foldNode", node);
|
//console.log("foldNode", node);
|
||||||
@ -25,8 +27,9 @@ export const HeynoteLanguage = LRLanguage.define({
|
|||||||
foldNodeProp.add({
|
foldNodeProp.add({
|
||||||
//NoteContent: foldNode,
|
//NoteContent: foldNode,
|
||||||
//NoteContent: foldInside,
|
//NoteContent: foldInside,
|
||||||
NoteContent(node) {
|
NoteContent(node, state) {
|
||||||
return {from:node.from, to:node.to}
|
//return {from:node.from, to:node.to}
|
||||||
|
return {from: Math.min(state.doc.lineAt(node.from).to, node.from + FOLD_LABEL_LENGTH), to: node.to}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
Loading…
x
Reference in New Issue
Block a user