Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

prototype: basic Upwelling-style branches, using forks #10

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,20 @@
"@codemirror/language": "^6.9.1",
"@codemirror/language-data": "^6.3.1",
"@codemirror/view": "^6.21.3",
"@effect/schema": "^0.52.0",
"@lezer/highlight": "^1.1.6",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dialog": "^1.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-menubar": "^1.0.4",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.0.0",
"cmdk": "^0.2.0",
"codemirror": "^6.0.1",
"effect": "^2.0.0-next.59",
"lodash": "^4.17.21",
"lorem-ipsum": "^2.0.8",
"lucide-react": "^0.284.0",
Expand Down
23 changes: 19 additions & 4 deletions src/components/App.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import { AutomergeUrl } from "@automerge/automerge-repo";
import { useDocument, useHandle } from "@automerge/automerge-repo-react-hooks";
import { useHandle } from "@automerge/automerge-repo-react-hooks";
import { MarkdownEditor, TextSelection } from "./MarkdownEditor";

import { LocalSession, MarkdownDoc } from "../schema";
import { Navbar } from "./Navbar";
import { LoadingScreen } from "./LoadingScreen";
import { useEffect, useState } from "react";
import { useEffect, useMemo, useState } from "react";

import { EditorView } from "@codemirror/view";
import { CommentsSidebar } from "./CommentsSidebar";
import { useThreadsWithPositions } from "../utils";
import { useTypedDocument } from "@/useTypedDocument";
import { Heads, getHeads } from "@automerge/automerge/next";

function App({ docUrl }: { docUrl: AutomergeUrl }) {
const [doc, changeDoc] = useDocument<MarkdownDoc>(docUrl); // used to trigger re-rendering when the doc loads
const [doc, changeDoc] = useTypedDocument(docUrl, MarkdownDoc); // used to trigger re-rendering when the doc loads
const handle = useHandle<MarkdownDoc>(docUrl);
const [session, setSessionInMemory] = useState<LocalSession>();
const [selection, setSelection] = useState<TextSelection>();
const [activeThreadId, setActiveThreadId] = useState<string | null>();
const [view, setView] = useState<EditorView>();
const [showDiff, setShowDiff] = useState(false);

const localStorageKey = `LocalSession-${docUrl}`;

Expand All @@ -41,6 +44,15 @@ function App({ docUrl }: { docUrl: AutomergeUrl }) {
activeThreadId,
});

const diffHeads: Heads | null = useMemo(() => {
if (!doc) return [];
if (doc?.forkMetadata?.parent && showDiff) {
return [...doc.forkMetadata.parent.forkedAtHeads];
} else {
return null;
}
}, [doc, showDiff]);

if (!doc || !session) {
return <LoadingScreen docUrl={docUrl} handle={handle} />;
}
Expand All @@ -54,13 +66,16 @@ function App({ docUrl }: { docUrl: AutomergeUrl }) {
changeDoc={changeDoc}
session={session}
setSession={setSession}
showDiff={showDiff}
setShowDiff={setShowDiff}
/>
</div>

<div className="flex bg-gray-50 mt-12">
<div className="flex bg-gray-50 mt-11">
<div className="w-full md:w-3/5 lg:w-4/5 max-w-[776px] bg-white md:my-4 md:ml-8 lg:ml-16 xl:ml-48 md:mr-4 border border-gray-200 p-4 rounded-sm">
<MarkdownEditor
handle={handle}
diffHeads={diffHeads}
path={["content"]}
setSelection={setSelection}
setView={setView}
Expand Down
113 changes: 110 additions & 3 deletions src/components/MarkdownEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useRef } from "react";
import React, { useEffect, useRef } from "react";

import {
EditorView,
Expand All @@ -16,10 +16,9 @@ import {
import { StateEffect, StateField, Range } from "@codemirror/state";
import { markdown } from "@codemirror/lang-markdown";
import { languages } from "@codemirror/language-data";
// import {javascript} from "@codemirror/lang-javascript"

import { tags } from "@lezer/highlight";
import { Prop } from "@automerge/automerge";
import { diff, getHeads, Heads, Patch, Prop } from "@automerge/automerge/next";
import {
plugin as amgPlugin,
PatchSemaphore,
Expand Down Expand Up @@ -53,6 +52,7 @@ export type TextSelection = {

export type EditorProps = {
handle: DocHandle<MarkdownDoc>;
diffHeads: Heads | null;
path: Prop[];
setSelection: (selection: TextSelection) => void;
setView: (view: EditorView) => void;
Expand Down Expand Up @@ -101,6 +101,92 @@ const threadDecorations = EditorView.decorations.compute(
}
) ?? [];

if (decorations.length === 0) {
return Decoration.none;
}

return Decoration.set(decorations);
}
);

// Stuff for patches decoration

const setPatchesEffect = StateEffect.define<Patch[]>();
const patchesField = StateField.define<Patch[]>({
create() {
return [];
},
update(patches, tr) {
for (const e of tr.effects) {
if (e.is(setPatchesEffect)) {
return e.value;
}
}
return patches;
},
});

class DeletionMarker extends WidgetType {
constructor() {
super();
}

toDOM(): HTMLElement {
const box = document.createElement("div");
box.style.display = "inline-block";
box.style.color = "#ff5353";
box.style.fontSize = "0.8em";
box.style.marginTop = "0.3em";
box.style.verticalAlign = "top";
box.innerText = "⌫";
return box;
}

eq() {
// todo: i think this is right for now until we show hover of del text etc
return true;
}

ignoreEvent() {
return true;
}
}

const spliceDecoration = Decoration.mark({ class: "cm-patch-splice" });
const deleteDecoration = Decoration.widget({
widget: new DeletionMarker(),
side: 1,
});

const patchDecorations = EditorView.decorations.compute(
[patchesField],
(state) => {
const patches = state
.field(patchesField)
.filter((patch) => patch.path[0] === "content");

const decorations = patches.flatMap((patch) => {
switch (patch.action) {
case "splice": {
const from = patch.path[1];
const length = patch.value.length;
if (length === 0) {
return [];
}
return [spliceDecoration.range(from, from + length)];
}
case "del": {
const from = patch.path[1];
return [deleteDecoration.range(from)];
}
}
return [];
});

if (decorations.length === 0) {
return Decoration.none;
}

return Decoration.set(decorations);
}
);
Expand Down Expand Up @@ -136,6 +222,9 @@ const theme = EditorView.theme({
".cm-comment-thread": {
backgroundColor: "rgb(255 249 194)",
},
".cm-patch-splice": {
backgroundColor: "rgb(0 255 0 / 20%)",
},
".cm-comment-thread.active": {
backgroundColor: "rgb(255 227 135)",
},
Expand Down Expand Up @@ -256,6 +345,7 @@ export function MarkdownEditor({
setView,
setActiveThreadId,
threadsWithPositions,
diffHeads,
}: EditorProps) {
const containerRef = useRef(null);
const editorRoot = useRef<EditorView>(null);
Expand All @@ -268,6 +358,21 @@ export function MarkdownEditor({
});
}, [threadsWithPositions]);

const doc = handle.docSync();
const view = editorRoot.current;

// // Propagate patches into the codemirror
useEffect(() => {
const doc = handle.docSync();
if (!diffHeads) {
return;
}
const patches = diff(doc, diffHeads, getHeads(doc));
editorRoot.current?.dispatch({
effects: setPatchesEffect.of(patches),
});
}, [handle, doc, diffHeads, view]);

useEffect(() => {
const doc = handle.docSync();
const source = doc.content; // this should use path
Expand Down Expand Up @@ -308,6 +413,8 @@ export function MarkdownEditor({
frontmatterPlugin,
threadsField,
threadDecorations,
patchesField,
patchDecorations,
previewFiguresPlugin,
highlightKeywordsPlugin,
tableOfContentsPreviewPlugin,
Expand Down
Loading