Skip to content

Commit

Permalink
[#71231] Simplify text management and Markdown rendering
Browse files Browse the repository at this point in the history
  • Loading branch information
Mikolaj Trzcinski authored and mgielda committed Feb 21, 2025
1 parent 4be6976 commit 34ca1ce
Show file tree
Hide file tree
Showing 20 changed files with 293 additions and 390 deletions.
28 changes: 10 additions & 18 deletions src/MystEditor.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import { StyleSheetManager, styled } from "styled-components";
import CodeMirror from "./components/CodeMirror";
import Preview, { PreviewFocusHighlight } from "./components/Preview";
import Diff from "./components/Diff";
import { useText } from "./hooks/useText";
import { EditorTopbar } from "./components/Topbar";
import ResolvedComments from "./components/Resolved";
import { handlePreviewClickToScroll } from "./extensions/syncDualPane";
import { createMystState, MystState, predefinedButtons, defaultButtons } from "./mystState";
import { batch, computed, signal, effect } from "@preact/signals";
import { syncCheckboxes } from "./hooks/markdownCheckboxes";
import { MystContainer } from "./styles/MystStyles";
import { syncCheckboxes } from "./markdown/markdownCheckboxes";

const EditorParent = styled.div`
font-family: "Lato";
Expand Down Expand Up @@ -95,11 +94,13 @@ const createExtraScopePlugin = (scope) => {
const hideBodyScrollIf = (val) => (document.documentElement.style.overflow = val ? "hidden" : "visible");

const MystEditor = () => {
const { editorView, cache, options, collab } = useContext(MystState);
const { editorView, cache, options, collab, text } = useContext(MystState);
const [fullscreen, setFullscreen] = useState(false);

const preview = useRef(null);
const text = useText({ preview });
useEffect(() => {
text.preview.value = preview.current;
}, [preview.current]);

const [alert, setAlert] = useState(null);
const [users, setUsers] = useReducer(
Expand All @@ -121,11 +122,11 @@ const MystEditor = () => {
fullscreen: () => setFullscreen((f) => !f),
refresh: () => {
cache.transform.clear();
text.renderText(false);
alertFor("Rich links refreshed!", 1);
text.refresh();
},
}),
[text],
[],
);

const buttons = useMemo(
Expand All @@ -143,23 +144,14 @@ const MystEditor = () => {
<StyleSheetManager target={options.parent}>
<MystContainer id="myst-css-namespace">
<EditorParent mode={options.mode.value} fullscreen={fullscreen}>
{options.topbar.value && (
<EditorTopbar
{...{
alert,
users,
text,
buttons,
}}
/>
)}
{options.topbar.value && <EditorTopbar alert={alert} users={users} buttons={buttons} />}
{options.collaboration.value.enabled && !collab.value.ready.value && (
<StatusBanner>Connecting to the collaboration server ...</StatusBanner>
)}
{options.collaboration.value.enabled && collab.value.lockMsg.value && <StatusBanner>{collab.value.lockMsg}</StatusBanner>}
<MystWrapper fullscreen={fullscreen}>
<FlexWrapper id="editor-wrapper">
<CodeMirror text={text} preview={preview} setUsers={setUsers} />
<CodeMirror setUsers={setUsers} />
</FlexWrapper>
<FlexWrapper id="preview-wrapper">
<Preview
Expand All @@ -176,7 +168,7 @@ const MystEditor = () => {
</FlexWrapper>
{options.mode.value === "Diff" && (
<FlexWrapper>
<Diff text={text} />
<Diff />
</FlexWrapper>
)}
{options.collaboration.value.commentsEnabled && options.collaboration.value.resolvingCommentsEnabled && collab.value.ready.value && (
Expand Down
40 changes: 17 additions & 23 deletions src/components/CodeMirror.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,31 +205,21 @@ const CodeEditor = styled.div`
}
`;

const setEditorText = (editor, text) => {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: text,
},
});
};

const CodeMirror = ({ text, setUsers, preview }) => {
const { editorView, options, collab, userSettings, linter } = useContext(MystState);
const CodeMirror = ({ setUsers }) => {
const { editorView, options, collab, userSettings, linter, text } = useContext(MystState);
const editorMountpoint = useRef(null);
const focusScroll = useRef(null);
const lastTyped = useRef(null);
const renderTimer = useRef(null);

useSignalEffect(() => {
if (!options.collaboration.value.enabled || (collab.value.ready.value && !collab.value.lockMsg.value)) return;
text.readyToRender();
editorView.value?.destroy();

const view = new EditorView({
root: options.parent,
state: EditorState.create({
doc: text.get(),
doc: text.text.peek(),
extensions: ExtensionBuilder.basicSetup().useHighlighter(options.transforms.value).readonly().create(),
}),
parent: editorMountpoint.current,
Expand All @@ -256,26 +246,24 @@ const CodeMirror = ({ text, setUsers, preview }) => {

if (collab.value.ytext.toString().length === 0 && options.initialText.length > 0) {
console.warn("[Collaboration] Remote state is empty, overriding with local state");
text.set(options.initialText);
text.text.value = options.initialText;
collab.value.ydoc.transact(() => {
collab.value.ytext.insert(0, options.initialText);
const metaMap = collab.value.ydoc.getMap("meta");
metaMap.set("initial", true);
});
}

text.set(collab.value.ytext.toString());
text.text.value = collab.value.ytext.toString();
collab.value.ytext.observe((ev, tr) => {
if (!tr.local) return;
lastTyped.current = performance.now();
});
}

text.readyToRender();

const startState = EditorState.create({
root: options.parent,
doc: options.collaboration.value.enabled ? collab.value.ytext.toString() : text.get(),
doc: options.collaboration.value.enabled ? collab.value.ytext.toString() : text.text.peek(),
extensions: ExtensionBuilder.basicSetup()
.useHighlighter(options.transforms.value)
.useCompartment(suggestionCompartment, customHighlighter([]))
Expand All @@ -291,11 +279,18 @@ const CodeMirror = ({ text, setUsers, preview }) => {
editorMountpoint,
}),
)
.addUpdateListener((update) => update.docChanged && text.set(view.state.doc.toString(), update))
.addUpdateListener((update) => {
if (!update.docChanged) return;
clearTimeout(renderTimer.current);
renderTimer.current = setTimeout(() => {
text.shiftLineMap(update);
text.text.value = view.state.doc.toString();
});
})
.useFixFoldingScroll(focusScroll)
.useMoveCursorAfterFold()
.useCursorIndicator({ lineMap: text.lineMap, preview })
.if(options.syncScroll.value, (b) => b.useSyncPreviewWithCursor({ lineMap: text.lineMap, preview, lastTyped }))
.useCursorIndicator({ text, preview: text.preview.value })
.if(options.syncScroll.value, (b) => b.useSyncPreviewWithCursor({ text, preview: text.preview.value, lastTyped }))
.if(options.yamlSchema.value, (b) => b.useYamlSchema(options.yamlSchema.value, editorView, linter))
.create(),
});
Expand All @@ -313,7 +308,6 @@ const CodeMirror = ({ text, setUsers, preview }) => {

collab.value?.ycomments?.registerCodeMirror(view);
collab.value?.provider?.watchCollabolators(setUsers);
text.onSync((currentText) => setEditorText(view, currentText));

return () => {
view.destroy();
Expand Down
6 changes: 3 additions & 3 deletions src/components/Diff.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ const initMergeView = ({ old, current, root }) => {
});
};

const Diff = ({ text }) => {
const { options } = useContext(MystState);
const Diff = () => {
const { options, text } = useContext(MystState);
let leftRef = useRef(null);
let rightRef = useRef(null);
let mergeView = useRef(null);
Expand All @@ -44,7 +44,7 @@ const Diff = ({ text }) => {
}
mergeView.current = initMergeView({
old: options.initialText,
current: text.get(),
current: text.text.peek(),
root: options.parent,
});

Expand Down
7 changes: 3 additions & 4 deletions src/components/TemplateManager.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,8 @@ const validateTemplConfig = (templConfig) => {
return templConfig;
};

const TemplateManager = ({ text }) => {
const { options } = useContext(MystState);
const TemplateManager = () => {
const { options, editorView } = useContext(MystState);
const [template, setTemplate] = useState("");
const [readyTemplates, setReadyTemplates] = useState({});
const [selectedTemplate, setSelectedTemplate] = useState(null);
Expand All @@ -97,8 +97,7 @@ const TemplateManager = ({ text }) => {

const changeDocumentTemplate = (template) => {
setTemplate(readyTemplates[template].templatetext);
text.set(readyTemplates[template].templatetext);
text.sync();
editorView.value.dispatch({ changes: { from: 0, to: editorView.value.state.doc.length, insert: readyTemplates[template].templatetext } });
setShowModal(false);
};

Expand Down
4 changes: 2 additions & 2 deletions src/components/Topbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const icons = {
settings: SettingsIcon,
};

export const EditorTopbar = ({ alert, users, text, buttons }) => {
export const EditorTopbar = ({ alert, users, buttons }) => {
const { options, editorView } = useContext(MystState);
const titleHtml = useComputed(() => purify.sanitize(renderMdLinks(options.title.value)));
const emptyDiff = useSignal(false);
Expand Down Expand Up @@ -268,7 +268,7 @@ export const EditorTopbar = ({ alert, users, text, buttons }) => {
<div className="btn-dropdown">{button.dropdown?.()}</div>
</div>
))}
{buttons.find((b) => b.id === "template-manager") && options.templatelist.value && <TemplateManager text={text} />}
{buttons.find((b) => b.id === "template-manager") && options.templatelist.value && <TemplateManager />}
</div>
{alert && <Alert className="topbar-alert"> {alert} </Alert>}
<Title id="document-title" dangerouslySetInnerHTML={{ __html: titleHtml.value }} />
Expand Down
16 changes: 8 additions & 8 deletions src/extensions/cursorIndicator.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
import { EditorView } from "codemirror";
import { markdownUpdatedStateEffect } from "../hooks/useText";
import { findNearestElementForLine } from "../hooks/markdownSourceMap";
import { markdownUpdatedEffect } from "../text";
import { findNearestElementForLine } from "../markdown/markdownSourceMap";

export const cursorIndicator = (lineMap, preview) =>
export const cursorIndicator = (text, preview) =>
EditorView.updateListener.of((update) => {
const cursorLineBefore = update.startState.doc.lineAt(update.startState.selection.main.head).number;
const cursorLineAfter = update.state.doc.lineAt(update.state.selection.main.head).number;
const selectionChanged = update.selectionSet && (cursorLineBefore !== cursorLineAfter || cursorLineBefore === 1);
const markdownUpdated = update.transactions.some((t) => t.effects.some((e) => e.is(markdownUpdatedStateEffect)));
const markdownUpdated = update.transactions.some((t) => t.effects.some((e) => e.is(markdownUpdatedEffect)));
const resized = update.geometryChanged && !update.viewportChanged;
if (update.docChanged || (!selectionChanged && !markdownUpdated && !resized)) {
return;
}

const [matchingElem] = findNearestElementForLine(cursorLineAfter, lineMap, preview.current);
const previewElement = preview.current.querySelector(".cm-previewFocus");
const [matchingElem] = findNearestElementForLine(cursorLineAfter, text.lineMap, preview);
const previewElement = preview.querySelector(".cm-previewFocus");
if (matchingElem) {
const previewRect = preview.current.getBoundingClientRect();
const previewRect = preview.getBoundingClientRect();
let matchingRect = matchingElem.getBoundingClientRect();

previewElement.style.top = `${matchingRect.top - previewRect.top + preview.current.scrollTop}px`;
previewElement.style.top = `${matchingRect.top - previewRect.top + preview.scrollTop}px`;
// the decoration of a list item is not contained within it's clientRect
const leftPos =
matchingRect.left - previewRect.left - 12.5 - (matchingElem.tagName === "LI" || matchingElem.parentElement.tagName === "LI" ? 17 : 0);
Expand Down
8 changes: 4 additions & 4 deletions src/extensions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ export class ExtensionBuilder {
return this;
}

useSyncPreviewWithCursor({ lineMap, preview, lastTyped }) {
this.extensions.push(syncPreviewWithCursor(lineMap, preview, lastTyped));
useSyncPreviewWithCursor({ text, preview, lastTyped }) {
this.extensions.push(syncPreviewWithCursor(text, preview, lastTyped));
return this;
}

useCursorIndicator({ lineMap, preview }) {
this.extensions.push(cursorIndicator(lineMap, preview));
useCursorIndicator({ text, preview }) {
this.extensions.push(cursorIndicator(text, preview));
return this;
}

Expand Down
18 changes: 9 additions & 9 deletions src/extensions/syncDualPane.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { EditorView } from "codemirror";
import { markdownUpdatedStateEffect } from "../hooks/useText";
import { findNearestElementForLine, getLineById } from "../hooks/markdownSourceMap";
import { markdownUpdatedEffect } from "../text";
import { findNearestElementForLine, getLineById } from "../markdown/markdownSourceMap";
import { EditorSelection } from "@codemirror/state";

const previewTopPadding = 20;
Expand All @@ -10,7 +10,7 @@ const debounceTimeout = 100;
*/
const typedToUpdateThreshold = 500;

export const syncPreviewWithCursor = (lineMap, preview, lastTyped) => {
export const syncPreviewWithCursor = (text, preview, lastTyped) => {
let timeout;

return EditorView.updateListener.of((update) => {
Expand All @@ -19,21 +19,21 @@ export const syncPreviewWithCursor = (lineMap, preview, lastTyped) => {
const selectionChanged = update.selectionSet && (cursorLineBefore !== cursorLineAfter || cursorLineBefore === 1);
const timeSinceLastTyped = lastTyped.current === null ? typedToUpdateThreshold : performance.now() - lastTyped.current;
const markdownUpdated =
update.transactions.some((t) => t.effects.some((e) => e.is(markdownUpdatedStateEffect))) && timeSinceLastTyped < typedToUpdateThreshold;
update.transactions.some((t) => t.effects.some((e) => e.is(markdownUpdatedEffect))) && timeSinceLastTyped < typedToUpdateThreshold;
const resized = update.geometryChanged && !update.viewportChanged;
if (update.docChanged || (!selectionChanged && !markdownUpdated && !resized)) {
return;
}

function sync() {
const [matchingElem, matchingLine] = findNearestElementForLine(cursorLineAfter, lineMap, preview.current);
const [matchingElem, matchingLine] = findNearestElementForLine(cursorLineAfter, text.lineMap, preview);
if (matchingElem) {
scrollPreviewElemIntoView({
view: update.view,
matchingLine,
matchingElem,
behavior: "smooth",
preview: preview.current,
preview,
});
}
}
Expand All @@ -60,8 +60,8 @@ function scrollPreviewElemIntoView({ view, matchingLine, matchingElem, behavior

/**
* @param {{ target: HTMLElement }} ev
* @param {{ current: Map<number, string> }} lineMap
* @param {{ current: HTMLElement }} preview
* @param {Map<number, string>} lineMap
* @param {HTMLElement} preview
* @param { EditorView } editor
*/
export function handlePreviewClickToScroll(ev, lineMap, preview, editor) {
Expand All @@ -78,7 +78,7 @@ export function handlePreviewClickToScroll(ev, lineMap, preview, editor) {
}
if (!id) return;

const lineNumber = getLineById(lineMap.current, id);
const lineNumber = getLineById(lineMap, id);
const line = editor.state.doc.line(lineNumber);
const visible = editor.visibleRanges[0];
function setCursor() {
Expand Down
Loading

0 comments on commit 34ca1ce

Please sign in to comment.