Spaces:
Running
Running
Pass code edits through CRDT.
Browse files
examples/word2vec.py
CHANGED
|
@@ -22,3 +22,8 @@ def word2vec_1000():
|
|
| 22 |
@op(ENV, "Take first N")
|
| 23 |
def first_n(df: pd.DataFrame, *, n=10):
|
| 24 |
return df.head(n)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
@op(ENV, "Take first N")
|
| 23 |
def first_n(df: pd.DataFrame, *, n=10):
|
| 24 |
return df.head(n)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@op(ENV, "Sample N")
|
| 28 |
+
def sample_n(df: pd.DataFrame, *, n=10):
|
| 29 |
+
return df.sample(n)
|
lynxkite-app/src/lynxkite_app/crdt.py
CHANGED
|
@@ -26,11 +26,11 @@ def ws_exception_handler(exception, log):
|
|
| 26 |
return True
|
| 27 |
|
| 28 |
|
| 29 |
-
class
|
| 30 |
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
| 31 |
"""Initialize a room for the workspace with the given name.
|
| 32 |
|
| 33 |
-
The workspace is loaded from "
|
| 34 |
"""
|
| 35 |
crdt_path = pathlib.Path(".crdt")
|
| 36 |
path = crdt_path / f"{name}.crdt"
|
|
@@ -82,6 +82,37 @@ class WebsocketServer(pycrdt_websocket.WebsocketServer):
|
|
| 82 |
return room
|
| 83 |
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
last_ws_input = None
|
| 86 |
|
| 87 |
|
|
@@ -247,14 +278,21 @@ async def execute(name: str, ws_crdt: pycrdt.Map, ws_pyd: workspace.Workspace, d
|
|
| 247 |
print(f"Finished running {name} in {ws_pyd.env}.")
|
| 248 |
|
| 249 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
@contextlib.asynccontextmanager
|
| 251 |
async def lifespan(app):
|
| 252 |
-
global
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
)
|
| 256 |
-
async with
|
| 257 |
-
|
|
|
|
| 258 |
print("closing websocket server")
|
| 259 |
|
| 260 |
|
|
@@ -265,5 +303,12 @@ def sanitize_path(path):
|
|
| 265 |
@router.websocket("/ws/crdt/{room_name}")
|
| 266 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
| 267 |
room_name = sanitize_path(room_name)
|
| 268 |
-
server = pycrdt_websocket.ASGIServer(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 269 |
await server({"path": room_name}, websocket._receive, websocket._send)
|
|
|
|
| 26 |
return True
|
| 27 |
|
| 28 |
|
| 29 |
+
class WorkspaceWebsocketServer(pycrdt_websocket.WebsocketServer):
|
| 30 |
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
| 31 |
"""Initialize a room for the workspace with the given name.
|
| 32 |
|
| 33 |
+
The workspace is loaded from ".crdt" if it exists there, or from a JSON file, or a new workspace is created.
|
| 34 |
"""
|
| 35 |
crdt_path = pathlib.Path(".crdt")
|
| 36 |
path = crdt_path / f"{name}.crdt"
|
|
|
|
| 82 |
return room
|
| 83 |
|
| 84 |
|
| 85 |
+
class CodeWebsocketServer(WorkspaceWebsocketServer):
|
| 86 |
+
async def init_room(self, name: str) -> pycrdt_websocket.YRoom:
|
| 87 |
+
"""Initialize a room for a text document with the given name."""
|
| 88 |
+
crdt_path = pathlib.Path(".crdt")
|
| 89 |
+
path = crdt_path / f"{name}.crdt"
|
| 90 |
+
assert path.is_relative_to(crdt_path)
|
| 91 |
+
ystore = pycrdt_websocket.ystore.FileYStore(path)
|
| 92 |
+
ydoc = pycrdt.Doc()
|
| 93 |
+
ydoc["text"] = text = pycrdt.Text()
|
| 94 |
+
# Replay updates from the store.
|
| 95 |
+
try:
|
| 96 |
+
for update, timestamp in [(item[0], item[-1]) async for item in ystore.read()]:
|
| 97 |
+
ydoc.apply_update(update)
|
| 98 |
+
except pycrdt_websocket.ystore.YDocNotFound:
|
| 99 |
+
pass
|
| 100 |
+
if len(text) == 0:
|
| 101 |
+
if os.path.exists(name):
|
| 102 |
+
with open(name) as f:
|
| 103 |
+
text += f.read()
|
| 104 |
+
room = pycrdt_websocket.YRoom(
|
| 105 |
+
ystore=ystore, ydoc=ydoc, exception_handler=ws_exception_handler
|
| 106 |
+
)
|
| 107 |
+
room.text = text
|
| 108 |
+
|
| 109 |
+
def on_change(changes):
|
| 110 |
+
asyncio.create_task(code_changed(name, changes, text))
|
| 111 |
+
|
| 112 |
+
text.observe(on_change)
|
| 113 |
+
return room
|
| 114 |
+
|
| 115 |
+
|
| 116 |
last_ws_input = None
|
| 117 |
|
| 118 |
|
|
|
|
| 278 |
print(f"Finished running {name} in {ws_pyd.env}.")
|
| 279 |
|
| 280 |
|
| 281 |
+
async def code_changed(name: str, changes: pycrdt.TextEvent, text: pycrdt.Text):
|
| 282 |
+
# TODO: Make this more fancy?
|
| 283 |
+
with open(name, "w") as f:
|
| 284 |
+
f.write(str(text))
|
| 285 |
+
|
| 286 |
+
|
| 287 |
@contextlib.asynccontextmanager
|
| 288 |
async def lifespan(app):
|
| 289 |
+
global ws_websocket_server
|
| 290 |
+
global code_websocket_server
|
| 291 |
+
ws_websocket_server = WorkspaceWebsocketServer(auto_clean_rooms=False)
|
| 292 |
+
code_websocket_server = CodeWebsocketServer(auto_clean_rooms=False)
|
| 293 |
+
async with ws_websocket_server:
|
| 294 |
+
async with code_websocket_server:
|
| 295 |
+
yield
|
| 296 |
print("closing websocket server")
|
| 297 |
|
| 298 |
|
|
|
|
| 303 |
@router.websocket("/ws/crdt/{room_name}")
|
| 304 |
async def crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
| 305 |
room_name = sanitize_path(room_name)
|
| 306 |
+
server = pycrdt_websocket.ASGIServer(ws_websocket_server)
|
| 307 |
+
await server({"path": room_name}, websocket._receive, websocket._send)
|
| 308 |
+
|
| 309 |
+
|
| 310 |
+
@router.websocket("/ws/code/crdt/{room_name}")
|
| 311 |
+
async def code_crdt_websocket(websocket: fastapi.WebSocket, room_name: str):
|
| 312 |
+
room_name = sanitize_path(room_name)
|
| 313 |
+
server = pycrdt_websocket.ASGIServer(code_websocket_server)
|
| 314 |
await server({"path": room_name}, websocket._receive, websocket._send)
|
lynxkite-app/src/lynxkite_app/main.py
CHANGED
|
@@ -39,15 +39,6 @@ def get_catalog(workspace: str):
|
|
| 39 |
return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
|
| 40 |
|
| 41 |
|
| 42 |
-
@app.get("/api/getCode")
|
| 43 |
-
def get_code(path: str):
|
| 44 |
-
path = data_path / path
|
| 45 |
-
assert path.is_relative_to(data_path)
|
| 46 |
-
with open(path) as f:
|
| 47 |
-
code = f.read()
|
| 48 |
-
return {"code": code}
|
| 49 |
-
|
| 50 |
-
|
| 51 |
class SaveRequest(workspace.BaseConfig):
|
| 52 |
path: str
|
| 53 |
ws: workspace.Workspace
|
|
|
|
| 39 |
return {k: {op.name: op.model_dump() for op in v.values()} for k, v in ops.CATALOGS.items()}
|
| 40 |
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
class SaveRequest(workspace.BaseConfig):
|
| 43 |
path: str
|
| 44 |
ws: workspace.Workspace
|
lynxkite-app/web/package-lock.json
CHANGED
|
@@ -29,6 +29,7 @@
|
|
| 29 |
"react-router-dom": "^7.0.2",
|
| 30 |
"swr": "^2.2.5",
|
| 31 |
"unplugin-icons": "^0.21.0",
|
|
|
|
| 32 |
"y-websocket": "^2.0.4",
|
| 33 |
"yjs": "^13.6.20"
|
| 34 |
},
|
|
@@ -7397,6 +7398,23 @@
|
|
| 7397 |
"yjs": "^13.0.0"
|
| 7398 |
}
|
| 7399 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7400 |
"node_modules/y-protocols": {
|
| 7401 |
"version": "1.0.6",
|
| 7402 |
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
|
|
|
|
| 29 |
"react-router-dom": "^7.0.2",
|
| 30 |
"swr": "^2.2.5",
|
| 31 |
"unplugin-icons": "^0.21.0",
|
| 32 |
+
"y-monaco": "^0.1.6",
|
| 33 |
"y-websocket": "^2.0.4",
|
| 34 |
"yjs": "^13.6.20"
|
| 35 |
},
|
|
|
|
| 7398 |
"yjs": "^13.0.0"
|
| 7399 |
}
|
| 7400 |
},
|
| 7401 |
+
"node_modules/y-monaco": {
|
| 7402 |
+
"version": "0.1.6",
|
| 7403 |
+
"resolved": "https://registry.npmjs.org/y-monaco/-/y-monaco-0.1.6.tgz",
|
| 7404 |
+
"integrity": "sha512-sYRywMmcylt+Nupl+11AvizD2am06ST8lkVbUXuaEmrtV6Tf+TD4rsEm6u9YGGowYue+Vfg1IJ97SUP2J+PVXg==",
|
| 7405 |
+
"license": "MIT",
|
| 7406 |
+
"dependencies": {
|
| 7407 |
+
"lib0": "^0.2.43"
|
| 7408 |
+
},
|
| 7409 |
+
"engines": {
|
| 7410 |
+
"node": ">=12.0.0",
|
| 7411 |
+
"npm": ">=6.0.0"
|
| 7412 |
+
},
|
| 7413 |
+
"peerDependencies": {
|
| 7414 |
+
"monaco-editor": ">=0.20.0",
|
| 7415 |
+
"yjs": "^13.3.1"
|
| 7416 |
+
}
|
| 7417 |
+
},
|
| 7418 |
"node_modules/y-protocols": {
|
| 7419 |
"version": "1.0.6",
|
| 7420 |
"resolved": "https://registry.npmjs.org/y-protocols/-/y-protocols-1.0.6.tgz",
|
lynxkite-app/web/package.json
CHANGED
|
@@ -32,6 +32,7 @@
|
|
| 32 |
"react-router-dom": "^7.0.2",
|
| 33 |
"swr": "^2.2.5",
|
| 34 |
"unplugin-icons": "^0.21.0",
|
|
|
|
| 35 |
"y-websocket": "^2.0.4",
|
| 36 |
"yjs": "^13.6.20"
|
| 37 |
},
|
|
|
|
| 32 |
"react-router-dom": "^7.0.2",
|
| 33 |
"swr": "^2.2.5",
|
| 34 |
"unplugin-icons": "^0.21.0",
|
| 35 |
+
"y-monaco": "^0.1.6",
|
| 36 |
"y-websocket": "^2.0.4",
|
| 37 |
"yjs": "^13.6.20"
|
| 38 |
},
|
lynxkite-app/web/src/Code.tsx
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
// Full-page editor for code files.
|
| 2 |
|
| 3 |
-
import Editor from "@monaco-editor/react";
|
| 4 |
-
import {
|
| 5 |
-
import { useEffect } from "react";
|
| 6 |
import { useParams } from "react-router";
|
| 7 |
-
import
|
|
|
|
| 8 |
// @ts-ignore
|
| 9 |
import Atom from "~icons/tabler/atom.jsx";
|
| 10 |
// @ts-ignore
|
|
@@ -13,20 +14,41 @@ import Backspace from "~icons/tabler/backspace.jsx";
|
|
| 13 |
import Close from "~icons/tabler/x.jsx";
|
| 14 |
import favicon from "./assets/favicon.ico";
|
| 15 |
import theme from "./code-theme.ts";
|
|
|
|
|
|
|
| 16 |
|
| 17 |
export default function Code() {
|
| 18 |
-
useEffect(() => {
|
| 19 |
-
const initMonaco = async () => {
|
| 20 |
-
const monaco = await loader.init();
|
| 21 |
-
monaco.editor.defineTheme("lynxkite", theme);
|
| 22 |
-
};
|
| 23 |
-
initMonaco();
|
| 24 |
-
}, []);
|
| 25 |
const { path } = useParams();
|
| 26 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
| 27 |
-
const
|
| 28 |
-
|
| 29 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
return (
|
| 31 |
<div className="workspace">
|
| 32 |
<div className="top-bar bg-neutral">
|
|
@@ -46,29 +68,19 @@ export default function Code() {
|
|
| 46 |
</a>
|
| 47 |
</div>
|
| 48 |
</div>
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
defaultValue={code.data.code}
|
| 63 |
-
theme="lynxkite"
|
| 64 |
-
options={{
|
| 65 |
-
cursorStyle: "block",
|
| 66 |
-
cursorBlinking: "solid",
|
| 67 |
-
minimap: { enabled: false },
|
| 68 |
-
renderLineHighlight: "none",
|
| 69 |
-
}}
|
| 70 |
-
/>
|
| 71 |
-
)}
|
| 72 |
</div>
|
| 73 |
);
|
| 74 |
}
|
|
|
|
| 1 |
// Full-page editor for code files.
|
| 2 |
|
| 3 |
+
import Editor, { type Monaco } from "@monaco-editor/react";
|
| 4 |
+
import type { editor } from "monaco-editor";
|
| 5 |
+
import { useEffect, useRef } from "react";
|
| 6 |
import { useParams } from "react-router";
|
| 7 |
+
import { WebsocketProvider } from "y-websocket";
|
| 8 |
+
import * as Y from "yjs";
|
| 9 |
// @ts-ignore
|
| 10 |
import Atom from "~icons/tabler/atom.jsx";
|
| 11 |
// @ts-ignore
|
|
|
|
| 14 |
import Close from "~icons/tabler/x.jsx";
|
| 15 |
import favicon from "./assets/favicon.ico";
|
| 16 |
import theme from "./code-theme.ts";
|
| 17 |
+
// For some reason y-monaco is gigantic. The other Monaco packages are small.
|
| 18 |
+
const MonacoBinding = await import("y-monaco").then((m) => m.MonacoBinding);
|
| 19 |
|
| 20 |
export default function Code() {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
const { path } = useParams();
|
| 22 |
const parentDir = path!.split("/").slice(0, -1).join("/");
|
| 23 |
+
const ydoc = useRef<any>();
|
| 24 |
+
const wsProvider = useRef<any>();
|
| 25 |
+
const monacoBinding = useRef<any>();
|
| 26 |
+
function beforeMount(monaco: Monaco) {
|
| 27 |
+
monaco.editor.defineTheme("lynxkite", theme);
|
| 28 |
+
}
|
| 29 |
+
function onMount(_editor: editor.IStandaloneCodeEditor) {
|
| 30 |
+
ydoc.current = new Y.Doc();
|
| 31 |
+
const text = ydoc.current.getText("text");
|
| 32 |
+
const proto = location.protocol === "https:" ? "wss:" : "ws:";
|
| 33 |
+
wsProvider.current = new WebsocketProvider(
|
| 34 |
+
`${proto}//${location.host}/ws/code/crdt`,
|
| 35 |
+
path!,
|
| 36 |
+
ydoc.current,
|
| 37 |
+
);
|
| 38 |
+
monacoBinding.current = new MonacoBinding(
|
| 39 |
+
text,
|
| 40 |
+
_editor.getModel()!,
|
| 41 |
+
new Set([_editor]),
|
| 42 |
+
wsProvider.current.awareness,
|
| 43 |
+
);
|
| 44 |
+
}
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
return () => {
|
| 47 |
+
ydoc.current?.destroy();
|
| 48 |
+
wsProvider.current?.destroy();
|
| 49 |
+
monacoBinding.current?.destroy();
|
| 50 |
+
};
|
| 51 |
+
});
|
| 52 |
return (
|
| 53 |
<div className="workspace">
|
| 54 |
<div className="top-bar bg-neutral">
|
|
|
|
| 68 |
</a>
|
| 69 |
</div>
|
| 70 |
</div>
|
| 71 |
+
<Editor
|
| 72 |
+
defaultLanguage="python"
|
| 73 |
+
theme="lynxkite"
|
| 74 |
+
path={path}
|
| 75 |
+
beforeMount={beforeMount}
|
| 76 |
+
onMount={onMount}
|
| 77 |
+
options={{
|
| 78 |
+
cursorStyle: "block",
|
| 79 |
+
cursorBlinking: "solid",
|
| 80 |
+
minimap: { enabled: false },
|
| 81 |
+
renderLineHighlight: "none",
|
| 82 |
+
}}
|
| 83 |
+
/>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
</div>
|
| 85 |
);
|
| 86 |
}
|