Skip to content
This repository was archived by the owner on Jul 16, 2026. It is now read-only.

Commit cf79674

Browse files
committed
wip
1 parent 37f316a commit cf79674

6 files changed

Lines changed: 39 additions & 113 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,8 @@
9090
"daisyui": "5.5.5",
9191
"dotenv": "^17.2.3",
9292
"effect": "^3.19.6",
93-
"fast-diff": "^1.3.0",
9493
"katex": "^0.16.25",
94+
"loro-codemirror": "^0.3.3",
9595
"loro-crdt": "^1.10.0",
9696
"svelte": "^5.44.0",
9797
"tiptap-markdown": "^0.9.0",

pnpm-lock.yaml

Lines changed: 16 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/components/codemirror/Codemirror.svelte

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,23 @@
11
<script lang="ts">
22
import { browser } from "$app/environment";
3-
import type { Extension, Text } from "@codemirror/state";
3+
import type { Extension } from "@codemirror/state";
44
import { EditorView } from "@codemirror/view";
55
import { onMount, onDestroy } from "svelte";
66
import type { ClassValue } from "svelte/elements";
77
88
interface Props {
9-
doc: string | Text;
109
extensions: Extension | undefined;
1110
class?: ClassValue;
1211
editorView: EditorView;
1312
}
1413
15-
let {
16-
doc,
17-
extensions = [],
18-
editorView = $bindable(),
19-
...props
20-
}: Props = $props();
14+
let { extensions = [], editorView = $bindable(), ...props }: Props = $props();
2115
2216
let editorElement: HTMLElement;
2317
2418
onMount(() => {
2519
// Initialize CodeMirror
2620
editorView = new EditorView({
27-
doc,
2821
parent: editorElement,
2922
extensions: extensions,
3023
});

src/lib/components/codemirror/Editor.svelte

Lines changed: 6 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
ListOrdered,
1515
Strikethrough,
1616
} from "@lucide/svelte";
17+
import { LoroExtensions } from "loro-codemirror";
1718
import Codemirror from "./Codemirror.svelte";
1819
import {
1920
coreExtensions,
@@ -31,14 +32,14 @@
3132
import Toolbar from "./Toolbar.svelte";
3233
import { wikilinksExtension } from "$lib/editor/wikilinks.ts";
3334
import type { NoteOrFolder } from "$lib/schema.ts";
35+
import type { LoroNoteManager } from "$lib/loro.ts";
3436
3537
interface Props {
36-
content: string;
37-
onchange: (newContent: string) => void;
38+
manager: LoroNoteManager | undefined;
3839
notesList?: NoteOrFolder[];
3940
}
4041
41-
let { content, onchange, notesList = [] }: Props = $props();
42+
let { manager, notesList = [] }: Props = $props();
4243
4344
// svelte-ignore non_reactive_update
4445
let editorView: EditorView;
@@ -101,34 +102,10 @@
101102
const extensions = [
102103
coreExtensions,
103104
wikilinksExtension(notesList),
104-
// Update listener
105-
EditorView.updateListener.of((update) => {
106-
if (update.docChanged) {
107-
const newContent = update.state.doc.toString();
108-
console.debug(
109-
"[Prosemark] Content updated. Preview:",
110-
newContent.slice(0, 50),
111-
);
112-
onchange(newContent);
113-
}
114-
}),
105+
manager !== undefined ? LoroExtensions(manager.doc) : [],
115106
editorTheme,
116107
];
117108
118-
// Update content if it changes externally (from Loro)
119-
$effect(() => {
120-
if (content !== editorView.state.doc.toString()) {
121-
console.debug("[Prosemark] External content update");
122-
editorView.dispatch({
123-
changes: {
124-
from: 0,
125-
to: editorView.state.doc.length,
126-
insert: content,
127-
},
128-
});
129-
}
130-
});
131-
132109
const tools = [
133110
[
134111
{
@@ -196,10 +173,5 @@
196173
<div class="flex h-full flex-col">
197174
<Toolbar {tools} />
198175

199-
<Codemirror
200-
bind:editorView
201-
doc={content}
202-
{extensions}
203-
class="flex-1 overflow-y-auto"
204-
/>
176+
<Codemirror bind:editorView {extensions} class="flex-1 overflow-y-auto" />
205177
</div>

src/lib/loro.ts

Lines changed: 13 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import {
1212
Schema,
1313
Stream,
1414
} from "effect";
15-
import diff from "fast-diff";
1615
import { LoroDoc, LoroText, type Frontiers } from "loro-crdt";
1716

1817
export type Doc = LoroDoc<{
@@ -22,7 +21,7 @@ export type Doc = LoroDoc<{
2221
export class LoroNoteManager {
2322
#noteId: string;
2423
#noteKey: string;
25-
#doc: Doc;
24+
doc: Doc;
2625
#text: LoroText;
2726
#onUpdate: (snapshot: string) => void | Promise<void>;
2827
#eventSource: EventSource | null = null;
@@ -42,12 +41,12 @@ export class LoroNoteManager {
4241
) {
4342
this.#noteId = noteId;
4443
this.#noteKey = noteKey;
45-
this.#doc = new LoroDoc();
46-
this.#text = this.#doc.getText("content");
44+
this.doc = new LoroDoc();
45+
this.#text = this.doc.getText("content");
4746
this.#onUpdate = onUpdate ?? Function.constVoid;
4847

4948
// Initialize frontiers
50-
this.#lastFrontiers = this.#doc.frontiers();
49+
this.#lastFrontiers = this.doc.frontiers();
5150

5251
// 1. Init Hubs
5352
this.#outgoingHub = Effect.runSync(PubSub.unbounded<Uint8Array>());
@@ -66,7 +65,7 @@ export class LoroNoteManager {
6665
this.#persistenceFiber = Effect.runFork(persistenceStream);
6766

6867
// Subscribe to changes
69-
this.#doc.subscribe((event) => {
68+
this.doc.subscribe((event) => {
7069
// Notify content listeners
7170
const content = this.getContent();
7271
this.#contentListeners.forEach((listener) => {
@@ -78,9 +77,9 @@ export class LoroNoteManager {
7877

7978
// Publish local ops for sync
8079
if (event.by === "local") {
81-
const frontiers = this.#doc.frontiers();
80+
const frontiers = this.doc.frontiers();
8281
try {
83-
const update = this.#doc.export({
82+
const update = this.doc.export({
8483
mode: "shallow-snapshot",
8584
frontiers: this.#lastFrontiers,
8685
});
@@ -121,7 +120,7 @@ export class LoroNoteManager {
121120
async init(encryptedSnapshot?: string) {
122121
if (encryptedSnapshot) {
123122
await this.loadEncryptedSnapshot(encryptedSnapshot);
124-
this.#lastFrontiers = this.#doc.frontiers();
123+
this.#lastFrontiers = this.doc.frontiers();
125124
}
126125
}
127126

@@ -163,7 +162,7 @@ export class LoroNoteManager {
163162
}).pipe(
164163
Stream.runForEach((update) =>
165164
Effect.sync(() => {
166-
this.#doc.import(update);
165+
this.doc.import(update);
167166
}),
168167
),
169168
);
@@ -222,50 +221,11 @@ export class LoroNoteManager {
222221
return this.#text.toString();
223222
}
224223

225-
/**
226-
* Update text content using diffs
227-
*/
228-
updateContent(newContent: string) {
229-
const currentContent = this.#text.toString();
230-
if (currentContent === newContent) return;
231-
232-
console.debug("[Loro] Updating content with diff...");
233-
234-
// Calculate diff
235-
const diffs = diff(currentContent, newContent);
236-
237-
let index = 0;
238-
for (const [type, text] of diffs) {
239-
switch (type) {
240-
// DELETE
241-
case -1: {
242-
this.#text.delete(index, text.length);
243-
break;
244-
}
245-
246-
// EQUAL
247-
case 0: {
248-
index += text.length;
249-
break;
250-
}
251-
252-
// INSERT
253-
case 1: {
254-
this.#text.insert(index, text);
255-
index += text.length;
256-
break;
257-
}
258-
}
259-
}
260-
261-
this.commit();
262-
}
263-
264224
/**
265225
* Get encrypted snapshot for storage
266226
*/
267227
async getEncryptedSnapshot(): Promise<string> {
268-
const snapshot = this.#doc.export({
228+
const snapshot = this.doc.export({
269229
mode: "snapshot",
270230
}) as Uint8Array<ArrayBuffer>;
271231
const encrypted = await encryptData(snapshot, this.#noteKey);
@@ -281,7 +241,7 @@ export class LoroNoteManager {
281241
Either.getOrThrow,
282242
) as Uint8Array<ArrayBuffer>;
283243
const decrypted = await decryptData(encryptedBytes, this.#noteKey);
284-
this.#doc.import(decrypted);
244+
this.doc.import(decrypted);
285245
} catch (error) {
286246
console.error("Failed to load encrypted snapshot:", error);
287247
throw error;
@@ -292,13 +252,13 @@ export class LoroNoteManager {
292252
* Apply update from another peer
293253
*/
294254
applyUpdate(update: Uint8Array) {
295-
this.#doc.import(update);
255+
this.doc.import(update);
296256
}
297257

298258
/**
299259
* Commit current changes
300260
*/
301261
commit() {
302-
this.#doc.commit();
262+
this.doc.commit();
303263
}
304264
}

src/routes/notes/[id]/+page.svelte

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,7 @@
9898

9999
<div class="relative h-full flex-1 overflow-hidden">
100100
{#if !(note?.isFolder ?? true)}
101-
<Editor
102-
content={editorContent}
103-
{notesList}
104-
onchange={(newContent: string) => {
105-
// Hook in Loro
106-
loroManager?.updateContent(newContent);
107-
}}
108-
/>
101+
<Editor manager={loroManager} {notesList} />
109102
{:else if note?.isFolder}
110103
<div class="flex h-full items-center justify-center text-slate-400">
111104
<div class="text-center">

0 commit comments

Comments
 (0)