Collaborative editing works somewhat
This commit is contained in:
@@ -5,8 +5,6 @@ import { io } from "socket.io-client";
|
||||
|
||||
let socket = io("http://localhost:3000");
|
||||
|
||||
import { serverEvents } from "../../server/src/events";
|
||||
|
||||
let editor = new Editor(
|
||||
new HttpRemoteSpace(`http://${location.hostname}:3000/fs`, socket),
|
||||
document.getElementById("root")!
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { EditorView, ViewPlugin, ViewUpdate } from "@codemirror/view";
|
||||
import { HttpRemoteSpace, Space } from "./space";
|
||||
import {
|
||||
Update,
|
||||
receiveUpdates,
|
||||
sendableUpdates,
|
||||
collab,
|
||||
getSyncedVersion,
|
||||
} from "@codemirror/collab";
|
||||
import { PageMeta } from "./types";
|
||||
import { Text } from "@codemirror/state";
|
||||
|
||||
export class Document {
|
||||
text: Text;
|
||||
meta: PageMeta;
|
||||
|
||||
constructor(text: Text, meta: PageMeta) {
|
||||
this.text = text;
|
||||
this.meta = meta;
|
||||
}
|
||||
}
|
||||
|
||||
export function collabExtension(
|
||||
pageName: string,
|
||||
startVersion: number,
|
||||
space: HttpRemoteSpace,
|
||||
reloadCallback: () => void
|
||||
) {
|
||||
let plugin = ViewPlugin.fromClass(
|
||||
class {
|
||||
private pushing = false;
|
||||
private done = false;
|
||||
|
||||
constructor(private view: EditorView) {
|
||||
if (pageName) {
|
||||
this.pull();
|
||||
}
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
if (update.docChanged) this.push();
|
||||
}
|
||||
|
||||
async push() {
|
||||
let updates = sendableUpdates(this.view.state);
|
||||
if (this.pushing || !updates.length) return;
|
||||
this.pushing = true;
|
||||
let version = getSyncedVersion(this.view.state);
|
||||
let success = await space.pushUpdates(pageName, version, updates);
|
||||
this.pushing = false;
|
||||
|
||||
if (!success) {
|
||||
reloadCallback();
|
||||
}
|
||||
|
||||
// Regardless of whether the push failed or new updates came in
|
||||
// while it was running, try again if there's updates remaining
|
||||
if (sendableUpdates(this.view.state).length) {
|
||||
setTimeout(() => this.push(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
async pull() {
|
||||
while (!this.done) {
|
||||
let version = getSyncedVersion(this.view.state);
|
||||
let updates = await space.pullUpdates(pageName, version);
|
||||
this.view.dispatch(receiveUpdates(this.view.state, updates));
|
||||
}
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.done = true;
|
||||
}
|
||||
}
|
||||
);
|
||||
return [collab({ startVersion }), plugin];
|
||||
}
|
||||
+27
-13
@@ -10,7 +10,7 @@ import { indentWithTab, standardKeymap } from "@codemirror/commands";
|
||||
import { history, historyKeymap } from "@codemirror/history";
|
||||
import { bracketMatching } from "@codemirror/matchbrackets";
|
||||
import { searchKeymap } from "@codemirror/search";
|
||||
import { EditorState, StateField, Transaction } from "@codemirror/state";
|
||||
import { EditorState, StateField, Transaction, Text } from "@codemirror/state";
|
||||
import {
|
||||
drawSelection,
|
||||
dropCursor,
|
||||
@@ -59,6 +59,10 @@ import {
|
||||
} from "./types";
|
||||
import { safeRun } from "./util";
|
||||
|
||||
import { collabExtension } from "./collab";
|
||||
|
||||
import { Document } from "./collab";
|
||||
|
||||
class PageState {
|
||||
editorState: EditorState;
|
||||
scrollTop: number;
|
||||
@@ -94,12 +98,18 @@ export class Editor implements AppEventDispatcher {
|
||||
this.viewDispatch = () => {};
|
||||
this.render(parent);
|
||||
this.editorView = new EditorView({
|
||||
state: this.createEditorState(""),
|
||||
state: this.createEditorState(
|
||||
new Document(Text.of([""]), {
|
||||
name: "",
|
||||
lastModified: new Date(),
|
||||
version: 0,
|
||||
})
|
||||
),
|
||||
parent: document.getElementById("editor")!,
|
||||
});
|
||||
this.pageNavigator = new PathPageNavigator();
|
||||
this.indexer = new Indexer("page-index", space);
|
||||
this.watch();
|
||||
// this.watch();
|
||||
}
|
||||
|
||||
async init() {
|
||||
@@ -176,7 +186,7 @@ export class Editor implements AppEventDispatcher {
|
||||
return this.viewState.currentPage;
|
||||
}
|
||||
|
||||
createEditorState(text: string): EditorState {
|
||||
createEditorState(doc: Document): EditorState {
|
||||
const editor = this;
|
||||
let commandKeyBindings: KeyBinding[] = [];
|
||||
for (let def of this.editorCommands.values()) {
|
||||
@@ -196,7 +206,7 @@ export class Editor implements AppEventDispatcher {
|
||||
}
|
||||
}
|
||||
return EditorState.create({
|
||||
doc: text,
|
||||
doc: doc.text,
|
||||
extensions: [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
@@ -206,6 +216,12 @@ export class Editor implements AppEventDispatcher {
|
||||
customMarkdownStyle,
|
||||
bracketMatching(),
|
||||
closeBrackets(),
|
||||
collabExtension(
|
||||
doc.meta.name,
|
||||
doc.meta.version!,
|
||||
this.space,
|
||||
this.reloadPage.bind(this)
|
||||
),
|
||||
autocompletion({
|
||||
override: [
|
||||
this.plugCompleter.bind(this),
|
||||
@@ -317,6 +333,8 @@ export class Editor implements AppEventDispatcher {
|
||||
});
|
||||
}
|
||||
|
||||
reloadPage() {}
|
||||
|
||||
async plugCompleter(
|
||||
ctx: CompletionContext
|
||||
): Promise<CompletionResult | null> {
|
||||
@@ -439,10 +457,10 @@ export class Editor implements AppEventDispatcher {
|
||||
cachedMeta.lastModified.getTime() !== newPageMeta.lastModified.getTime()
|
||||
) {
|
||||
console.log("File changed on disk, reloading");
|
||||
let pageData = await this.space.readPage(currentPageName);
|
||||
let doc = await this.space.openPage(currentPageName);
|
||||
this.openPages.set(
|
||||
currentPageName,
|
||||
new PageState(this.createEditorState(pageData.text), 0, newPageMeta)
|
||||
new PageState(this.createEditorState(doc), 0, doc.meta)
|
||||
);
|
||||
await this.loadPage(currentPageName, false);
|
||||
}
|
||||
@@ -459,12 +477,8 @@ export class Editor implements AppEventDispatcher {
|
||||
async loadPage(pageName: string, checkNewVersion: boolean = true) {
|
||||
let pageState = this.openPages.get(pageName);
|
||||
if (!pageState) {
|
||||
let pageData = await this.space.readPage(pageName);
|
||||
pageState = new PageState(
|
||||
this.createEditorState(pageData.text),
|
||||
0,
|
||||
pageData.meta
|
||||
);
|
||||
let doc = await this.space.openPage(pageName);
|
||||
pageState = new PageState(this.createEditorState(doc), 0, doc.meta);
|
||||
this.openPages.set(pageName, pageState!);
|
||||
// Freshly loaded, no need to check for a new version either way
|
||||
checkNewVersion = false;
|
||||
|
||||
+60
-12
@@ -1,7 +1,9 @@
|
||||
import { PageMeta } from "./types";
|
||||
import { Socket } from "socket.io-client";
|
||||
import { serverEvents } from "../../server/src/events";
|
||||
import { EventEmitter } from "events";
|
||||
import { Update } from "@codemirror/collab";
|
||||
import { Transaction, Text, ChangeSet } from "@codemirror/state";
|
||||
|
||||
import { Document } from "./collab";
|
||||
|
||||
export interface Space {
|
||||
listPages(): Promise<PageMeta[]>;
|
||||
@@ -13,15 +15,48 @@ export interface Space {
|
||||
|
||||
export class HttpRemoteSpace implements Space {
|
||||
url: string;
|
||||
socket?: Socket;
|
||||
socket: Socket;
|
||||
reqId = 0;
|
||||
|
||||
constructor(url: string, socket: Socket | null) {
|
||||
constructor(url: string, socket: Socket) {
|
||||
this.url = url;
|
||||
// this.socket = socket;
|
||||
this.socket = socket;
|
||||
|
||||
// socket.on("connect", () => {
|
||||
// console.log("connected via SocketIO", serverEvents.pageText);
|
||||
// });
|
||||
socket.on("connect", () => {
|
||||
console.log("connected via SocketIO");
|
||||
});
|
||||
}
|
||||
|
||||
pushUpdates(
|
||||
pageName: string,
|
||||
version: number,
|
||||
fullUpdates: readonly (Update & { origin: Transaction })[]
|
||||
): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
if (this.socket) {
|
||||
let updates = fullUpdates.map((u) => ({
|
||||
clientID: u.clientID,
|
||||
changes: u.changes.toJSON(),
|
||||
}));
|
||||
this.reqId++;
|
||||
this.socket.emit("pushUpdates", this.reqId, pageName, version, updates);
|
||||
this.socket.once("pushUpdatesResp" + this.reqId, (result) => {
|
||||
resolve(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async pullUpdates(
|
||||
pageName: string,
|
||||
version: number
|
||||
): Promise<readonly Update[]> {
|
||||
let updates: Update[] = await this.wsCall("pullUpdates", pageName, version);
|
||||
console.log("Got updates", updates);
|
||||
return updates.map((u) => ({
|
||||
changes: ChangeSet.fromJSON(u.changes),
|
||||
clientID: u.clientID,
|
||||
}));
|
||||
}
|
||||
|
||||
async listPages(): Promise<PageMeta[]> {
|
||||
@@ -35,11 +70,24 @@ export class HttpRemoteSpace implements Space {
|
||||
}));
|
||||
}
|
||||
|
||||
async openPage(name: string) {
|
||||
this.socket!.on(serverEvents.pageText, (pageName, text) => {
|
||||
console.log("Got this", pageName, text);
|
||||
wsCall(eventName: string, ...args: any[]): Promise<any> {
|
||||
return new Promise((resolve) => {
|
||||
this.reqId++;
|
||||
this.socket!.once(`${eventName}Resp${this.reqId}`, resolve);
|
||||
this.socket!.emit(eventName, this.reqId, ...args);
|
||||
});
|
||||
this.socket!.emit(serverEvents.openPage, "start");
|
||||
}
|
||||
|
||||
async openPage(name: string): Promise<Document> {
|
||||
this.reqId++;
|
||||
let [meta, text] = await this.wsCall("openPage", name);
|
||||
console.log("Got this", meta, text);
|
||||
meta.lastModified = new Date(meta.lastModified);
|
||||
return new Document(Text.of(text), meta);
|
||||
}
|
||||
|
||||
async closePage(name: string): Promise<void> {
|
||||
this.socket!.emit("closePage", name);
|
||||
}
|
||||
|
||||
async readPage(name: string): Promise<{ text: string; meta: PageMeta }> {
|
||||
|
||||
@@ -11,6 +11,7 @@ export type Manifest = plugbox.Manifest<NuggetHook>;
|
||||
export type PageMeta = {
|
||||
name: string;
|
||||
lastModified: Date;
|
||||
version?: number;
|
||||
created?: boolean;
|
||||
lastOpened?: Date;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user