From 880da60fc2f778626c908e286dd6b491798fd96d Mon Sep 17 00:00:00 2001 From: Maciej Wasilewski Date: Wed, 2 Oct 2024 11:14:06 +0200 Subject: [PATCH] Revert folding --- src/MystEditor.js | 2 - src/components/CodeMirror.js | 18 +------ src/components/Preview.js | 67 ------------------------- src/extensions/index.js | 28 ++--------- src/extensions/markdownLang.js | 76 ----------------------------- src/hooks/markdownFoldButtons.js | 78 ----------------------------- src/hooks/markdownSourceMap.js | 32 ++++-------- src/hooks/useText.js | 84 ++++++-------------------------- src/icons/fold.svg | 3 -- tests/myst-editor.spec.ts | 13 ----- 10 files changed, 29 insertions(+), 372 deletions(-) delete mode 100644 src/extensions/markdownLang.js delete mode 100644 src/hooks/markdownFoldButtons.js delete mode 100644 src/icons/fold.svg diff --git a/src/MystEditor.js b/src/MystEditor.js index 051aa39..f19ff82 100644 --- a/src/MystEditor.js +++ b/src/MystEditor.js @@ -11,7 +11,6 @@ import { EditorTopbar } from "./components/Topbar"; import useCollaboration from "./hooks/useCollaboration"; import useComments from "./hooks/useComments"; import ResolvedComments from "./components/Resolved"; -import { handlePreviewFold } from "./hooks/markdownFoldButtons"; import { handlePreviewClickToScroll } from "./extensions/syncDualPane"; if (!window.myst_editor?.isFresh) { @@ -233,7 +232,6 @@ const MystEditor = ({ ref=${preview} mode=${mode} onClick=${(ev) => { - handlePreviewFold(ev, text.lineMap); handlePreviewClickToScroll(ev, text.lineMap, preview); }} ><${PreviewFocusHighlight} className="cm-previewFocus" /> { @@ -238,12 +225,11 @@ const CodeMirror = ({ text, id, root, mode, spellcheckOpts, highlights, collabor .useHighlighter(highlights) .useCompartment(suggestionCompartment, customHighlighter([])) .useSpellcheck(spellcheckOpts) - .useFoldArrows() .if(collaboration.opts.enabled, (b) => b.useCollaboration({ ...collaboration, editorRef })) .if(collaboration.opts.commentsEnabled, (b) => b.useComments({ ycomments: collaboration.ycomments }).useSuggestionPopup({ ycomments: collaboration.ycomments, editorMountpoint }), ) - .addUpdateListener((update) => (update.docChanged || folded(update)) && text.set(view.state.doc.toString(), update)) + .addUpdateListener((update) => update.docChanged && text.set(view.state.doc.toString(), update)) .useFixFoldingScroll(focusScroll) .useMoveCursorAfterFold() .useCursorIndicator({ lineMap: text.lineMap, preview }) diff --git a/src/components/Preview.js b/src/components/Preview.js index 5583b1b..963b1dc 100644 --- a/src/components/Preview.js +++ b/src/components/Preview.js @@ -3,7 +3,6 @@ import styled from "styled-components"; const Preview = styled.div` background-color: white; padding: 20px; - padding-left: 40px; box-sizing: border-box; height: 100%; border: 1px solid var(--gray-400); @@ -105,7 +104,6 @@ const Preview = styled.div` pre { white-space: pre-wrap; padding: 16px; - max-width: calc(100% - 40px) !important; & > code { padding: 0px; } @@ -116,7 +114,6 @@ const Preview = styled.div` } aside { border-radius: var(--border-radius); - max-width: 100% !important; &.admonition { border: var(--border-2) solid var(--green-500); @@ -337,8 +334,6 @@ const Preview = styled.div` .cm-previewFocus { display: ${(props) => (props.mode === "Both" ? "block" : "none")}; - z-index: 1; - pointer-events: none; } .mermaid { @@ -347,68 +342,6 @@ const Preview = styled.div` display: flex; justify-content: center; } - - *:has(.fold) { - position: relative; - max-width: max-content; - } - - .fold-arrow { - position: absolute; - transform: translate(-25px, -50%); - cursor: pointer; - background: transparent; - border: none; - padding: 0; - padding-right: 25px; - top: 50%; - opacity: 0; - - img { - max-width: unset; - height: unset; - } - - &:hover { - opacity: 1; - } - } - - *:hover > *:not(aside) > .fold-arrow, - aside:hover > .fold-arrow { - opacity: 1 !important; - } - - li > * > .fold-arrow { - transform: translate(-42px, -50%); - } - - aside > .fold-arrow { - transform: translate(-28px, -50%); - top: 20px; - } - - .fold-dots { - background-color: rgb(238, 238, 238); - border: 1px solid rgb(221, 221, 221); - color: rgb(136, 136, 136); - border-radius: 0.2rem; - margin: 0; - padding: 0 1px; - cursor: pointer; - position: absolute; - right: -23px; - top: 50%; - transform: translateY(-50%); - } - - .unfold { - opacity: 1; - - img { - rotate: -90deg; - } - } `; Preview.defaultProps = { className: "myst-preview" }; diff --git a/src/extensions/index.js b/src/extensions/index.js index 956c49b..460c313 100644 --- a/src/extensions/index.js +++ b/src/extensions/index.js @@ -9,17 +9,11 @@ import { customHighlighter } from "./customHighlights"; import { commentExtension } from "../comments"; import { commentAuthoring } from "../comments/lineAuthors"; import { suggestionPopup } from "./suggestions"; -import { foldEffect, unfoldEffect, foldable, ensureSyntaxTree } from "@codemirror/language"; +import { foldEffect, unfoldEffect, foldable } from "@codemirror/language"; import { syncPreviewWithCursor } from "./syncDualPane"; import { cursorIndicator } from "./cursorIndicator"; -import { customCommonMark, fenceFold, headerIndent } from "./markdownLang"; -import { foldArrowGutter } from "../hooks/markdownFoldButtons"; - -const basicExclude = [ - 3, // history - 4, // default fold gutter -]; -const basicSetupWithoutHistory = basicSetup.filter((_, i) => !basicExclude.includes(i)); + +const basicSetupWithoutHistory = basicSetup.filter((_, i) => i != 3); const minimalSetupWithoutHistory = minimalSetup.filter((_, i) => i != 1); const getRelativeCursorLocation = (view) => { @@ -55,14 +49,7 @@ export class ExtensionBuilder { } static defaultPlugins() { - return [ - EditorView.lineWrapping, - markdown({ base: customCommonMark }), - highlightActiveLine(), - headerIndent, - fenceFold, - keymap.of([indentWithTab, { key: "Mod-Z", run: redo }]), - ]; + return [EditorView.lineWrapping, markdown(), highlightActiveLine(), keymap.of([indentWithTab, { key: "Mod-Z", run: redo }])]; } disable(keys) { @@ -215,11 +202,6 @@ export class ExtensionBuilder { return this; } - useFoldArrows() { - this.extensions.push(foldArrowGutter); - return this; - } - create() { return [...this.important, ...this.base, ...this.extensions]; } @@ -227,8 +209,6 @@ export class ExtensionBuilder { /** This function folds all top level syntax nodes, while skiping a number of them defined by the `skip` parameter */ export function skipAndFoldAll(/** @type {EditorView} */ view, skip = 0) { - ensureSyntaxTree(window.myst_editor.main_editor.state, view.state.doc.length, 5000); - view.dispatch({}); let { state } = view; let effects = []; let nProcessedFoldables = 0; diff --git a/src/extensions/markdownLang.js b/src/extensions/markdownLang.js deleted file mode 100644 index d38e5cd..0000000 --- a/src/extensions/markdownLang.js +++ /dev/null @@ -1,76 +0,0 @@ -// Taken from https://github.com/codemirror/lang-markdown/blob/main/src/markdown.ts -import { parser } from "@lezer/markdown"; -import { NodeProp } from "@lezer/common"; -import { foldNodeProp, indentNodeProp, languageDataProp, defineLanguageFacet, Language, foldService, syntaxTree } from "@codemirror/language"; - -const data = defineLanguageFacet({ commentTokens: { block: { open: "" } } }); - -const headingProp = new NodeProp(); -const fenceProp = new NodeProp(); - -// This is here to customize the markdown parser used by Codemirror, in particular the folding nodes. -const commonmark = parser.configure({ - props: [ - foldNodeProp.add((type) => { - return !type.is("Block") || type.is("FencedCode") || type.is("Document") || isHeading(type) != null - ? undefined - : (node, state) => { - return { from: state.doc.lineAt(node.from).to, to: node.to }; - }; - }), - headingProp.add(isHeading), - fenceProp.add((type) => type.is("FencedCode")), - indentNodeProp.add({ - Document: () => null, - }), - languageDataProp.add({ - Document: data, - }), - ], -}); - -function isHeading(type) { - let match = /^(?:ATX|Setext)Heading(\d)$/.exec(type.name); - return match ? +match[1] : undefined; -} - -export const headerIndent = foldService.of((state, start, end) => { - for (let node = syntaxTree(state).resolveInner(end, -1); node; node = node.parent) { - if (node.from < start) break; - let heading = node.type.prop(headingProp); - if (heading == null) continue; - let upto = findSectionEnd(node, heading); - if (upto > end) return { from: end, to: upto }; - } - return null; -}); - -function findSectionEnd(headerNode, level) { - let last = headerNode; - for (;;) { - let next = last.nextSibling, - heading; - if (!next || ((heading = isHeading(next.type)) != null && heading <= level)) break; - last = next; - } - return last.to; -} - -// This foldService disables folding of certain fenced block types. -const disabledBlocks = ["```{image", "```{figure", "```{list-table}"]; -export const fenceFold = foldService.of((state, start, end) => { - for (let node = syntaxTree(state).resolveInner(end, -1); node; node = node.parent) { - if (node.from < start) break; - let fence = node.type.prop(fenceProp); - if (fence == false) continue; - const line = state.doc.lineAt(node.from); - const to = state.doc.line(state.doc.lineAt(node.to).number - 1).to; - for (const block of disabledBlocks) { - if (line.text.includes(block)) return; - } - return { from: line.to, to }; - } - return null; -}); - -export const customCommonMark = new Language(data, commonmark, [headerIndent, fenceFold], "markdown"); diff --git a/src/hooks/markdownFoldButtons.js b/src/hooks/markdownFoldButtons.js deleted file mode 100644 index 1b18de6..0000000 --- a/src/hooks/markdownFoldButtons.js +++ /dev/null @@ -1,78 +0,0 @@ -import { getLineById, SRC_LINE_ID } from "./markdownSourceMap"; -import { toggleFold, foldable, foldedRanges, foldGutter } from "@codemirror/language"; -import foldIcon from "../icons/fold.svg"; - -export function addFoldUI(/** @type {import("markdown-it").Token} */ token, /** @type {string} */ baseOutput, env) { - const id = token.attrGet(SRC_LINE_ID); - if (id) { - const lineNumber = getLineById(env.lineMap.current, id); - const line = window.myst_editor.main_editor.state.doc.line(lineNumber); - const range = foldable(window.myst_editor.main_editor.state, line.from, line.to); - if (range) { - const ranges = foldedRanges(window.myst_editor.main_editor.state); - let folded = false; - ranges.between(range.from, range.to, (from, to) => { - if (from === range.from && to === range.to) { - folded = true; - } - }); - - if (!folded) { - return addFoldArrow(baseOutput, id); - } else { - return addUnfoldButtons(baseOutput, id); - } - } - } - - return baseOutput; -} - -const addFoldArrow = (baseOutput, id) => { - const firstElementEndIdx = baseOutput.indexOf(">") + 1; - return ( - baseOutput.slice(0, firstElementEndIdx) + - `` + - baseOutput.slice(firstElementEndIdx) - ); -}; - -const addUnfoldButtons = (/** @type {string} */ baseOutput, id) => { - const firstElementEndIdx = baseOutput.indexOf(">") + 1; - const arrow = ``; - const dots = ``; - if (baseOutput.endsWith("\n")) { - return arrow + baseOutput.slice(0, baseOutput.indexOf("")) + dots + ""; - } else { - return baseOutput.slice(0, firstElementEndIdx) + arrow + baseOutput.slice(firstElementEndIdx) + dots; - } -}; - -export function handlePreviewFold(/** @type {MouseEvent} */ ev, lineMap) { - /** @type {HTMLElement} */ - let button = ev.target.classList.contains("fold") ? ev.target : ev.target.parentElement; - if (!button.classList.contains("fold")) return; - ev.preventDefault(); - - const lineId = button.getAttribute("data-btn-id"); - const lineNumber = getLineById(lineMap.current, lineId); - const line = window.myst_editor.main_editor.state.doc.line(lineNumber); - window.myst_editor.main_editor.dispatch({ - selection: { anchor: line.to, head: line.to }, - }); - toggleFold(window.myst_editor.main_editor); -} - -export const foldArrowGutter = foldGutter({ - markerDOM(open) { - const img = document.createElement("img"); - img.src = foldIcon; - img.alt = "fold"; - img.title = "Fold line"; - img.classList.add("fold-arrow"); - if (!open) { - img.classList.add("unfold"); - } - return img; - }, -}); diff --git a/src/hooks/markdownSourceMap.js b/src/hooks/markdownSourceMap.js index ecc3f82..d0cb9f7 100644 --- a/src/hooks/markdownSourceMap.js +++ b/src/hooks/markdownSourceMap.js @@ -1,21 +1,11 @@ import markdownIt from "markdown-it"; import { escapeHtml } from "markdown-it/lib/common/utils"; -export const SRC_LINE_ID = "data-line-id"; +const SRC_LINE_ID = "data-line-id"; const randomLineId = () => Math.random().toString().replace(".", ""); -function getLineForToken(token, env) { - let line = token.map[0] + env.startLine - (env.chunkId !== 0); - for (const range of env.foldedLines ?? []) { - if (range.start > line) break; - - line += range.end - range.start + 1; - } - return line; -} - /** @param {markdownIt} md */ -export default function markdownSourceMap(md, transform = (token, out, env) => out) { +export default function markdownSourceMap(md) { md.use(overrideDefaultDirectives); md.use(overrideDefaultRole); md.use(wrapTextInSpan); @@ -33,11 +23,11 @@ export default function markdownSourceMap(md, transform = (token, out, env) => o for (const rule of overrideRules) { const temp = md.renderer.rules[rule]; - md.renderer.rules[rule] = addLineNumberToTokens(temp, transform); + md.renderer.rules[rule] = addLineNumberToTokens(temp); } } -function addLineNumberToTokens(defaultRule, transform) { +function addLineNumberToTokens(defaultRule) { /** * @param {import("markdown-it/index.js").Token[]} tokens * @param {number} idx @@ -69,7 +59,7 @@ function addLineNumberToTokens(defaultRule, transform) { } } } else if (tokens[idx].map) { - const line = getLineForToken(tokens[idx], env); + const line = tokens[idx].map[0] + env.startLine - (env.chunkId !== 0); const id = randomLineId(); if (!env.lineMap.current.has(line)) { env.lineMap.current.set(line, id); @@ -77,7 +67,7 @@ function addLineNumberToTokens(defaultRule, transform) { } } - return transform(tokens[idx], rule(tokens, idx, options, env, self), env); + return rule(tokens, idx, options, env, self); }; } @@ -132,18 +122,14 @@ function wrapFencedLinesInSpan(/** @type {markdownIt} */ md) { } const sanitizedContent = escapeHtml(token.content); - const startLine = getLineForToken(token, env); + const startLine = token.map[0] + env.startLine - (env.chunkId !== 0); let htmlContent = sanitizedContent .split("\n") .filter((_, i, lines) => i !== lines.length - 1) .map((l, i) => { const id = randomLineId(); - if (!env.lineMap.current.has(startLine + i + 1)) { - env.lineMap.current.set(startLine + i + 1, id); - return `${l}`; - } else { - return `${l}`; - } + env.lineMap.current.set(startLine + i + 1, id); + return `${l}`; }) .join("\n"); diff --git a/src/hooks/useText.js b/src/hooks/useText.js index f73da8e..5f70469 100644 --- a/src/hooks/useText.js +++ b/src/hooks/useText.js @@ -8,41 +8,8 @@ import { backslashLineBreakPlugin } from "./markdownLineBreak"; import markdownSourceMap from "./markdownSourceMap"; import { StateEffect } from "@codemirror/state"; import markdownMermaid from "./markdownMermaid"; -import { EditorView, ViewUpdate } from "@codemirror/view"; -import { addFoldUI } from "./markdownFoldButtons"; -import { foldedRanges, ensureSyntaxTree } from "@codemirror/language"; const countOccurences = (str, pattern) => (str?.match(pattern) || []).length; -const getUnfoldedMarkdown = (src, /** @type {EditorView} */ view) => { - const folded = foldedRanges(view.state); - const ranges = []; - const cursor = folded.iter(); - for (let r = cursor; r.value != null; cursor.next()) { - ranges.push({ from: r.from, to: r.to }); - } - - let unfoldedMarkdown = src; - if (ranges.length > 0) { - unfoldedMarkdown = ranges.reduce( - (acc, { from, to }, idx) => { - if (from > acc.lastPos) { - acc.result += src.slice(acc.lastPos, from); - } - acc.lastPos = Math.max(acc.lastPos, to); - if (idx == ranges.length - 1 && acc.lastPos < src.length) { - // add remaining part - acc.result += src.slice(acc.lastPos, src.length); - } - return acc; - }, - { result: "", lastPos: 0 }, - ).result; - } - - const foldedLines = ranges.map((r) => ({ start: view.state.doc.lineAt(r.from).number + 1, end: view.state.doc.lineAt(r.to).number })); - - return [unfoldedMarkdown, foldedLines]; -}; const exposeText = (text) => () => { if (!window.myst_editor) { @@ -87,7 +54,7 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla * * @type {[{ md: string, html: string }[], Dispatch<{newMarkdown: string, force: boolean }>]} */ - const [htmlChunks, updateHtmlChunks] = useReducer((oldChunks, { newMarkdown, force = false, view, foldedLines }) => { + const [htmlChunks, updateHtmlChunks] = useReducer((oldChunks, { newMarkdown, force = false, view }) => { let htmlLookup = {}; if (!force) { htmlLookup = oldChunks.reduce((lookup, { hash, html }) => { @@ -95,7 +62,8 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla return lookup; }, {}); } - const newChunks = splitIntoChunks(newMarkdown, htmlLookup, foldedLines); + + const newChunks = splitIntoChunks(newMarkdown, htmlLookup); if (newChunks.length !== oldChunks.length || force) { // We can't infer which chunks were modified, so we update the entire document @@ -122,8 +90,8 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla .use(markdownitDocutils) .use(markdownReplacer(transforms, parent)) .use(useCustomRoles(customRoles, parent)) - .use(markdownMermaid, { lineMap, parent }) - .use(markdownSourceMap, addFoldUI); + .use(markdownMermaid, { preview, lineMap, parent }) + .use(markdownSourceMap); if (backslashLineBreak) md.use(backslashLineBreakPlugin); return md; }, []); @@ -159,7 +127,7 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla /** Split and parse markdown into chunks of HTML. If `lookup` is not provided then every chunk will be parsed */ const splitIntoChunks = useCallback( - (newMarkdown, lookup = {}, foldedLines) => + (newMarkdown, lookup = {}) => newMarkdown .split(/(?=\n#{1,3} )/g) // Perform a split without removing the delimeter .reduce( @@ -183,33 +151,16 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla .map(({ md, startLine, endLine }, id) => { const hash = new IMurMurHash(md, 42).result(); + // Clear source mappings for chunk we are rerendering if (!lookup[hash]) { - // Clear source mappings for chunk we are rerendering - let unfoldedStart = startLine; - let unfoldedEnd = endLine; - for (const range of foldedLines ?? []) { - if (range.start < unfoldedStart) { - unfoldedStart += range.end - range.start + 1; - } - if (range.start < unfoldedEnd) { - unfoldedEnd += range.end - range.start + 1; - } - } - for (let l = unfoldedStart; l <= unfoldedEnd; l++) { + for (let l = startLine; l <= endLine; l++) { lineMap.current.delete(l); } - - const endLineTo = window.myst_editor.main_editor.state.doc.line(unfoldedEnd).to; - // This might cause longer startup times for larger documents - // Calling this causes the Markdown parser in CodeMirror to parse at least up to the range - // being rendered. Thanks to this, we can query the editor about lines being foldable. - ensureSyntaxTree(window.myst_editor.main_editor.state, endLineTo, 1000); - window.myst_editor.main_editor.dispatch({}); } const html = lookup[hash] || - purify.sanitize(markdown.render(md, { chunkId: id, startLine, lineMap, foldedLines }), { + purify.sanitize(markdown.render(md, { chunkId: id, startLine, lineMap }), { // Taken from Mermaid JS settings: https://github.com/mermaid-js/mermaid/blob/dd0304387e85fc57a9ebb666f89ef788c012c2c5/packages/mermaid/src/mermaidAPI.ts#L50 ADD_TAGS: ["foreignobject"], ADD_ATTR: ["dominant-baseline"], @@ -229,23 +180,17 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla }, [syncText]); return { - set(newMarkdown, /** @type {ViewUpdate} */ update) { + set(newMarkdown, update) { if (update) { shiftLineMap(update); } - let unfoldedMarkdown = newMarkdown; - let foldedLines = []; - if (update?.state) { - [unfoldedMarkdown, foldedLines] = getUnfoldedMarkdown(newMarkdown, update.view); - } - - setText(unfoldedMarkdown); + setText(newMarkdown); setTimeout(() => { try { - updateHtmlChunks({ newMarkdown: unfoldedMarkdown, view: update?.view, foldedLines }); + updateHtmlChunks({ newMarkdown, view: update?.view }); } catch (e) { console.warn(e); - updateHtmlChunks({ newMarkdown: unfoldedMarkdown, force: true, view: update?.view, foldedLines }); + updateHtmlChunks({ newMarkdown, force: true, view: update?.view }); } }); }, @@ -256,8 +201,7 @@ export const useText = ({ initialText, transforms, customRoles, preview, backsla setSyncText(true); }, refresh() { - const [unfoldedMarkdown, foldedLines] = getUnfoldedMarkdown(window.myst_editor.text, window.myst_editor.main_editor); - updateHtmlChunks({ newMarkdown: unfoldedMarkdown, force: true, foldedLines }); + updateHtmlChunks({ newMarkdown: window.myst_editor.text, force: true }); }, onSync(action) { setOnSync({ action }); diff --git a/src/icons/fold.svg b/src/icons/fold.svg deleted file mode 100644 index 578ee56..0000000 --- a/src/icons/fold.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/tests/myst-editor.spec.ts b/tests/myst-editor.spec.ts index 6c16908..8f48c48 100644 --- a/tests/myst-editor.spec.ts +++ b/tests/myst-editor.spec.ts @@ -144,19 +144,6 @@ graph TD (html) => expect(html).toContain(" { - await clearEditor(page); - await insertToMainEditor(page, { - from: 0, - to: 0, - insert: "# h1\n\ntest" - }); - expect(await page.locator(".myst-preview").innerHTML()).toContain("test"); - await page.locator(".myst-preview button").click(); - await page.waitForSelector(`.myst-preview button[title="unfold"]`); - expect(await page.locator(".myst-preview").innerHTML()).not.toContain("test"); - }); }) test.describe.parallel("With collaboration enabled", () => {