diff --git a/package.json b/package.json index a2ce6bace..631f88493 100644 --- a/package.json +++ b/package.json @@ -16,12 +16,9 @@ "@codemirror/state": "^6.2.0", "@codemirror/view": "^6.7.3", "@tauri-apps/api": "^1.2.0", - "diff": "^5.1.0", "idb-keyval": "^6.2.0", "mm-jsr": "^3.0.2", - "nanoid": "^4.0.0", "svelte-icons": "^2.1.0", - "tauri-plugin-fs-watch-api": "github:tauri-apps/tauri-plugin-fs-watch", "tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log", "yjs": "^13.5.45" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f43f21013..83ad7d65e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,10 +10,8 @@ specifiers: '@tauri-apps/cli': ^1.2.2 '@types/diff': ^5.0.2 autoprefixer: ^10.4.7 - diff: ^5.1.0 idb-keyval: ^6.2.0 mm-jsr: ^3.0.2 - nanoid: ^4.0.0 postcss: ^8.4.14 postcss-load-config: ^4.0.1 svelte: ^3.54.0 @@ -21,7 +19,6 @@ specifiers: svelte-icons: ^2.1.0 svelte-preprocess: ^4.10.7 tailwindcss: ^3.1.5 - tauri-plugin-fs-watch-api: github:tauri-apps/tauri-plugin-fs-watch tauri-plugin-log-api: github:tauri-apps/tauri-plugin-log tslib: ^2.4.1 typescript: ^4.8.4 @@ -33,12 +30,9 @@ dependencies: '@codemirror/state': 6.2.0 '@codemirror/view': 6.7.3 '@tauri-apps/api': 1.2.0 - diff: 5.1.0 idb-keyval: 6.2.0 mm-jsr: 3.0.2 - nanoid: 4.0.0 svelte-icons: 2.1.0 - tauri-plugin-fs-watch-api: github.com/tauri-apps/tauri-plugin-fs-watch/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a tauri-plugin-log-api: github.com/tauri-apps/tauri-plugin-log/33d9b712e9058ed82c110cb186345215f82b88e2 yjs: 13.5.45 @@ -710,11 +704,6 @@ packages: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true - /diff/5.1.0: - resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} - engines: {node: '>=0.3.1'} - dev: false - /dlv/1.1.3: resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} dev: true @@ -1008,12 +997,6 @@ packages: hasBin: true dev: true - /nanoid/4.0.0: - resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==} - engines: {node: ^14 || ^16 || >=18} - hasBin: true - dev: false - /node-releases/2.0.9: resolution: {integrity: sha512-2xfmOrRkGogbTK9R6Leda0DGiXeY3p2NJpy4+gNCffdUvV6mdEJnaDEic1i3Ec2djAo8jWYoJMR5PB0MSMpxUA==} dev: true @@ -1608,14 +1591,6 @@ packages: lib0: 0.2.60 dev: false - github.com/tauri-apps/tauri-plugin-fs-watch/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a: - resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-fs-watch/tar.gz/7d63ad9dfd72ae6c2bb05c148f344adb0521ec3a} - name: tauri-plugin-fs-watch-api - version: 0.0.0 - dependencies: - '@tauri-apps/api': 1.2.0 - dev: false - github.com/tauri-apps/tauri-plugin-log/33d9b712e9058ed82c110cb186345215f82b88e2: resolution: {tarball: https://codeload.github.com/tauri-apps/tauri-plugin-log/tar.gz/33d9b712e9058ed82c110cb186345215f82b88e2} name: tauri-plugin-log-api diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 9af82309d..9bbb50adc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1003,7 +1003,6 @@ dependencies = [ "serde_json", "tauri", "tauri-build", - "tauri-plugin-fs-watch", "tauri-plugin-log", "tauri-plugin-window-state", "uuid 1.3.0", @@ -1727,22 +1726,10 @@ dependencies = [ "kqueue", "libc", "mio", - "serde", "walkdir", "windows-sys", ] -[[package]] -name = "notify-debouncer-mini" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e23e9fa24f094b143c1eb61f90ac6457de87be6987bc70746e0179f7dbc9007b" -dependencies = [ - "crossbeam-channel", - "notify", - "serde", -] - [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2894,20 +2881,6 @@ dependencies = [ "tauri-utils", ] -[[package]] -name = "tauri-plugin-fs-watch" -version = "0.1.0" -source = "git+https://github.com/tauri-apps/plugins-workspace?branch=dev#bf1106a0a5e178ce38ecde56751ba037307a7ae8" -dependencies = [ - "log", - "notify", - "notify-debouncer-mini", - "serde", - "serde_json", - "tauri", - "thiserror", -] - [[package]] name = "tauri-plugin-log" version = "0.1.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 291182d65..0028e6503 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,7 +16,6 @@ tauri-build = { version = "1.2", features = [] } [dependencies] serde = { version = "1.0", features = ["derive"] } tauri = { version = "1.2", features = ["dialog-open", "fs-create-dir", "fs-exists", "fs-read-file", "fs-write-file", "path-all", "shell-sidecar", "window-start-dragging"] } -tauri-plugin-fs-watch = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev", features = ["colored"] } log = "0.4.17" diff --git a/src-tauri/src/crdt.rs b/src-tauri/src/crdt.rs index 566551ff6..7c2d66214 100644 --- a/src-tauri/src/crdt.rs +++ b/src-tauri/src/crdt.rs @@ -4,12 +4,14 @@ use std::time::SystemTime; use yrs::{Doc, GetString, Text, Transact}; #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub struct Delta { operations: Vec, timestamp_ms: u64, } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] +#[serde(rename_all = "camelCase")] pub enum Operation { // corresponds to YText.insert(index, chunk) Insert((u32, String)), @@ -232,9 +234,6 @@ fn test_document_from_deltas() { ], }, ]); - assert_eq!(document.at(0).to_string(), "hello"); - assert_eq!(document.at(1).to_string(), "hello world"); - assert_eq!(document.at(2).to_string(), "held!"); assert_eq!(document.to_string(), "held!"); } diff --git a/src-tauri/src/delta_watchers.rs b/src-tauri/src/delta_watchers.rs index 651903861..368d5b295 100644 --- a/src-tauri/src/delta_watchers.rs +++ b/src-tauri/src/delta_watchers.rs @@ -22,8 +22,8 @@ pub fn unwatch(watchers: &WatcherCollection, project: Project) { } #[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] struct DeltasEvent { - project_id: String, file_path: String, deltas: Vec, } @@ -74,7 +74,6 @@ pub fn watch( &event_name, &DeltasEvent { deltas, - project_id: project.id.clone(), file_path: relative_file_path.to_string(), }, ) @@ -162,7 +161,8 @@ fn register_file_change( } // get commit from refs/gitbutler/current or fall back to HEAD -fn get_meta_commit(repo: &Repository) -> Commit { +// TODO: make this private as soon as possible +pub fn get_meta_commit(repo: &Repository) -> Commit { match repo.revparse_single("refs/gitbutler/current") { Ok(object) => repo.find_commit(object.id()).unwrap(), Err(_) => { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 6415050ee..4ae214ec3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -6,6 +6,7 @@ mod storage; use crdt::Delta; use fs::list_files; +use git2::Repository; use log; use projects::Project; use std::collections::HashMap; @@ -22,26 +23,68 @@ struct AppState { projects_storage: projects::Storage, } -// returns a list of files in directory recursively #[tauri::command] -fn read_dir(path: &str) -> Result, InvokeError> { - let path = Path::new(path); - if path.is_dir() { - let files = list_files(path); - return Ok(files); +fn list_project_files( + state: State<'_, AppState>, + project_id: &str, +) -> Result, InvokeError> { + log::debug!("Listing project files for project: {}", project_id); + if let Some(project) = state.projects_storage.get_project(project_id)? { + let project_path = Path::new(&project.path); + let repo = match Repository::open(project_path) { + Ok(repo) => repo, + Err(e) => panic!("failed to open: {}", e), + }; + let files = list_files(project_path); + let meta_commit = delta_watchers::get_meta_commit(&repo); + let tree = meta_commit.tree().unwrap(); + let non_ignored_files: Vec = files + .into_iter() + .filter_map(|file| { + let file_path = Path::new(&file); + let relative_file_path = file_path.strip_prefix(project_path).unwrap(); + let relative_file_path = relative_file_path.to_str().unwrap(); + if let Ok(_object) = tree.get_path(Path::new(&relative_file_path)) { + Some(relative_file_path.to_string()) + } else { + None + } + }) + .collect(); + Ok(non_ignored_files) } else { - return Err("Path is not a directory".into()); + Err("Project not found".into()) } } -// reads file contents and returns it #[tauri::command] -fn read_file(file_path: &str) -> Result { - let contents = read_to_string(file_path); - if contents.is_ok() { - return Ok(contents.unwrap()); +fn read_project_file( + state: State<'_, AppState>, + project_id: &str, + file_path: &str, +) -> Result, InvokeError> { + log::debug!( + "Reading project file for project: {} and file: {}", + project_id, + file_path + ); + if let Some(project) = state.projects_storage.get_project(project_id)? { + let project_path = Path::new(&project.path); + let repo = match Repository::open(project_path) { + Ok(repo) => repo, + Err(e) => panic!("failed to open: {}", e), + }; + let meta_commit = delta_watchers::get_meta_commit(&repo); + let tree = meta_commit.tree().unwrap(); + if let Ok(object) = tree.get_path(Path::new(&file_path)) { + let blob = object.to_object(&repo).unwrap().into_blob().unwrap(); + let contents = String::from_utf8(blob.content().to_vec()).unwrap(); + Ok(Some(contents)) + } else { + Ok(None) + } } else { - return Err(contents.err().unwrap().to_string().into()); + Err("Project not found".into()) } } @@ -130,7 +173,6 @@ fn main() { Ok(()) }) .plugin(tauri_plugin_window_state::Builder::default().build()) - .plugin(tauri_plugin_fs_watch::init()) .plugin( tauri_plugin_log::Builder::default() .level(log::LevelFilter::Debug) @@ -139,8 +181,8 @@ fn main() { .build(), ) .invoke_handler(tauri::generate_handler![ - read_file, - read_dir, + read_project_file, + list_project_files, add_project, list_projects, delete_project, diff --git a/src-tauri/src/projects.rs b/src-tauri/src/projects.rs index eeaa02e51..a44cca992 100644 --- a/src-tauri/src/projects.rs +++ b/src-tauri/src/projects.rs @@ -79,8 +79,12 @@ impl Project { for file_path in file_paths { let file_path = Path::new(&file_path); let file_deltas = self.get_file_deltas(file_path); + let relative_file_path = file_path.strip_prefix(&deltas_path).unwrap(); if let Some(file_deltas) = file_deltas { - deltas.insert(file_path.to_str().unwrap().to_string(), file_deltas); + deltas.insert( + relative_file_path.to_str().unwrap().to_string(), + file_deltas, + ); } } deltas diff --git a/src/lib/components/CodeViewer.svelte b/src/lib/components/CodeViewer.svelte index 458954aa3..0ee1853bf 100644 --- a/src/lib/components/CodeViewer.svelte +++ b/src/lib/components/CodeViewer.svelte @@ -3,54 +3,53 @@ import { EditorState, StateField, StateEffect } from "@codemirror/state"; import { EditorView, lineNumbers, Decoration } from "@codemirror/view"; - import type { TextDocument } from "$lib/crdt"; - export let doc: TextDocument | null | undefined = null; - $: value = doc?.toString(); - $: lastInserts = doc - ?.getHistory() - .filter((e) => e.deltas.some((x: any) => x["insert"])) - .slice(1) - .slice(-10) - .map((e) => e["deltas"]); + export let value: string; + //$: value = doc?.toString(); + //$: lastInserts = doc + //?.getHistory() + //.filter((e) => e.deltas.some((x: any) => x["insert"])) + //.slice(1) + //.slice(-10) + //.map((e) => e["deltas"]); let element: HTMLDivElement; let view: EditorView; $: view && update(value); - $: view && col(lastInserts, "bg-fuchsia-500"); + //$: view && col(lastInserts, "bg-fuchsia-500"); - function col(inserts: any[] | undefined, c: string) { - if (!inserts) return; - let cs = [ - "bg-fuchsia-900", - "bg-fuchsia-800", - "bg-fuchsia-700", - "bg-fuchsia-600", - "bg-fuchsia-500", - "bg-fuchsia-400", - "bg-fuchsia-300", - "bg-fuchsia-200", - "bg-fuchsia-100", - "bg-fuchsia-50", - ]; - for (let i of inserts.reverse()) { - let op = cs.shift(); - let cls = op ? op : "bg-fuchsia-50"; - console.log(cls); - colorLastEdit(i, cls); - } - } + //function col(inserts: any[] | undefined, c: string) { + //if (!inserts) return; + //let cs = [ + //"bg-fuchsia-900", + //"bg-fuchsia-800", + //"bg-fuchsia-700", + //"bg-fuchsia-600", + //"bg-fuchsia-500", + //"bg-fuchsia-400", + //"bg-fuchsia-300", + //"bg-fuchsia-200", + //"bg-fuchsia-100", + //"bg-fuchsia-50", + //]; + //for (let i of inserts.reverse()) { + //let op = cs.shift(); + //let cls = op ? op : "bg-fuchsia-50"; + //console.log(cls); + //colorLastEdit(i, cls); + //} + //} - function colorLastEdit(e: any[] | undefined, c: string) { - let retain = e?.filter((e) => e["retain"])[0]; - let insert = e?.filter((e) => e["insert"])[0]; - if (retain && insert) { - let start = retain["retain"]; - let end = start + insert["insert"].length; - triggerColor(view, [{ from: start, to: end, c: c }]); - } - } + //function colorLastEdit(e: any[] | undefined, c: string) { + //let retain = e?.filter((e) => e["retain"])[0]; + //let insert = e?.filter((e) => e["insert"])[0]; + //if (retain && insert) { + //let start = retain["retain"]; + //let end = start + insert["insert"].length; + //triggerColor(view, [{ from: start, to: end, c: c }]); + //} + //} onMount(() => (view = create_editor_view())); onDestroy(() => view?.destroy()); @@ -111,14 +110,14 @@ provide: (f) => EditorView.decorations.from(f), }); - const triggerColor = (view: EditorView, positions: any[]) => { - for (const position of positions) { - const effect = addColor.of(position); - view.dispatch({ - effects: [effect], - }); - } - }; + //const triggerColor = (view: EditorView, positions: any[]) => { + //for (const position of positions) { + //const effect = addColor.of(position); + //view.dispatch({ + //effects: [effect], + //}); + //} + //}; let state_extensions = [ EditorView.editable.of(false), diff --git a/src/lib/components/Timeline.svelte b/src/lib/components/Timeline.svelte index e29359477..58ae8ff4f 100644 --- a/src/lib/components/Timeline.svelte +++ b/src/lib/components/Timeline.svelte @@ -6,13 +6,16 @@ ModuleLabel, ModuleGrid, } from "mm-jsr"; + import { createEventDispatcher } from "svelte"; export let min: number, max: number, step: number = 1, - value: number | undefined, + value: number | undefined = undefined, formatter = (value: number): string => `${value}`; + const dispatch = createEventDispatcher<{ value: number }>(); + const jsr = ( container: HTMLElement, config: { @@ -40,6 +43,7 @@ }); jsr.onValueChange(({ real }) => { value = real; + dispatch("value", real); }); }; diff --git a/src/lib/crdt.ts b/src/lib/crdt.ts index 220974984..f13343516 100644 --- a/src/lib/crdt.ts +++ b/src/lib/crdt.ts @@ -1,98 +1,84 @@ -import { Doc } from "yjs"; -import { diffChars } from "diff"; +import { invoke } from "@tauri-apps/api"; +import { appWindow } from "@tauri-apps/api/window"; +import { writable, type Subscriber } from "svelte/store"; -type DeltaRetain = { retain: number }; -type DeltaDelete = { delete: number }; -type DeltaInsert = { insert: string }; +export type OperationDelete = { delete: [number, number] }; +export type OperationInsert = { insert: [number, string] }; -export type Delta = DeltaRetain | DeltaDelete | DeltaInsert; +export type Operation = OperationDelete | OperationInsert; -export namespace Delta { - export const isRetain = (delta: Delta): delta is DeltaRetain => - "retain" in delta; +export namespace Operation { + export const isDelete = ( + operation: Operation + ): operation is OperationDelete => "delete" in operation; - export const isDelete = (delta: Delta): delta is DeltaDelete => - "delete" in delta; - - export const isInsert = (delta: Delta): delta is DeltaInsert => - "insert" in delta; + export const isInsert = ( + operation: Operation + ): operation is OperationInsert => "insert" in operation; } -// Compute the set of Yjs delta operations (that is, `insert` and -// `delete`) required to go from initialText to finalText. -// Based on https://github.com/kpdecker/jsdiff. -const getDeltaOperations = ( - initialText: string, - finalText: string -): Delta[] => { - if (initialText === finalText) { - return []; - } +export type Delta = { timestampMs: number; operations: Operation[] }; - const edits = diffChars(initialText, finalText); - let prevOffset = 0; - let deltas: Delta[] = []; - - for (const edit of edits) { - if (edit.removed && edit.value) { - deltas = [ - ...deltas, - ...[ - ...(prevOffset > 0 ? [{ retain: prevOffset }] : []), - { delete: edit.value.length }, - ], - ]; - prevOffset = 0; - } else if (edit.added && edit.value) { - deltas = [...deltas, ...[{ retain: prevOffset }, { insert: edit.value }]]; - prevOffset = edit.value.length; - } else { - prevOffset = edit.value.length; - } - } - return deltas; +type DeltasEvent = { + deltas: Delta[]; + filePath: string; }; -export type HistoryEntry = { time: number; deltas: Delta[] }; +const list = (params: { projectId: string }) => + invoke>("list_deltas", params); -export class TextDocument { - private doc: Doc = new Doc(); - private history: HistoryEntry[] = []; +export default async (params: { projectId: string }) => { + const files = await invoke("list_project_files", params); + const contents = await Promise.all( + files.map((filePath) => + invoke("read_project_file", { ...params, filePath }) + ) + ); - private constructor(...history: HistoryEntry[]) { - this.doc - .getText() - .applyDelta( - history.sort((a, b) => a.time - b.time).flatMap((h) => h.deltas) - ); - this.history = history; - } + // this is a temporary workaround to get the initial state of the document + // TODO: remove this once sessions api is implemented + const tmpState: Record = Object.fromEntries( + files.map((filePath, index) => [ + filePath, + [ + { + timestampMs: 0, + operations: [{ insert: [0, contents[index]] } as OperationInsert], + }, + ], + ]) + ); - static new(content?: string) { - return new TextDocument({ - time: new Date().getTime(), - deltas: content ? [{ insert: content }] : [], - }); - } + const init = await list(params); - update(content: string) { - const deltas = getDeltaOperations(this.toString(), content); - if (deltas.length == 0) return; - this.doc.getText().applyDelta(deltas); - this.history.push({ time: new Date().getTime(), deltas }); - } + const tmpInit = Object.fromEntries( + Object.entries(tmpState).map(([filePath, deltas]) => [ + filePath, + [...deltas, ...(filePath in init ? init[filePath] : [])], + ]) + ); - getHistory() { - return this.history.slice(); - } - - toString() { - return this.doc.getText().toString(); - } - - at(time: number) { - return new TextDocument( - ...this.history.filter((entry) => entry.time <= time) - ); - } -} + const store = writable>(tmpInit); + const eventName = `deltas://${params.projectId}`; + const unlisten = await appWindow.listen(eventName, (event) => { + store.update((deltas) => ({ + ...deltas, + [event.payload.filePath]: [ + ...(event.payload.filePath in tmpState + ? tmpState[event.payload.filePath] + : []), + ...event.payload.deltas, + ], + })); + }); + return { + subscribe: ( + run: Subscriber>, + invalidate?: (value?: Record) => void + ) => + store.subscribe(run, (value) => { + if (invalidate) invalidate(value); + // unlisten(); + }), + }; +}; diff --git a/src/lib/index.ts b/src/lib/index.ts index 527373caf..b93f9eef3 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,5 +1,3 @@ -export * as tauri from "./tauri"; -export * as watch from "./watch"; export * as crdt from "./crdt"; export * as database from "./database"; export * as projects from "./projects"; diff --git a/src/lib/projects.ts b/src/lib/projects.ts index b3d76f27c..c1ddabae0 100644 --- a/src/lib/projects.ts +++ b/src/lib/projects.ts @@ -1,8 +1,5 @@ -import { database } from "$lib"; -import { writable, type Readable } from "svelte/store"; -import { TextDocument } from "./crdt"; -import { readFile, readDir, NoSuchFileOrDirectoryError } from "./tauri"; -import { EventType, type Event, watch as fsWatch } from "./watch"; +import { invoke } from "@tauri-apps/api"; +import { writable } from "svelte/store"; export type Project = { id: string; @@ -10,75 +7,20 @@ export type Project = { path: string; }; -export const watch = ( - project: Project -): Readable> => { - const tree = writable>({}); +const list = () => invoke("list_projects"); - // TODO (NB: we can probably use git ls-files) - const shouldIgnore = (filepath: string) => { - if (filepath.includes(".git")) return true; - if (filepath.includes("node_modules")) return true; - if (filepath.includes("env")) return true; - if (filepath.includes("__pycache__")) return true; - return false; +const add = (params: { path: string }) => + invoke("add_project", params); + +export default async () => { + const init = await list(); + const store = writable(init); + return { + subscribe: store.subscribe, + add: (params: { path: string }) => + add(params).then((project) => { + store.update((projects) => [...projects, project]); + return project; + }), }; - - const upsertDoc = async (filepath: string) => { - if (shouldIgnore(filepath)) return; - - const content = await readFile(filepath).catch((err) => { - if (err instanceof NoSuchFileOrDirectoryError) { - return undefined; - } else { - throw err; - } - }); - - tree.update((tree) => { - if (content === undefined) { - delete tree[filepath]; - return tree; - } else if (filepath in tree) { - tree[filepath].update(content); - return tree; - } else { - tree[filepath] = TextDocument.new(content); - return tree; - } - }); - }; - - readDir(project.path).then((filepaths) => filepaths.forEach(upsertDoc)); - - fsWatch(project.path, async (event: Event) => { - const isFileCreate = - EventType.isCreate(event.type) && event.type.create.kind === "file"; - const isFileUpdate = - EventType.isModify(event.type) && event.type.modify.kind === "data"; - const isFileRemove = EventType.isRemove(event.type); - - if (isFileCreate || isFileUpdate) { - for (const path of event.paths) { - await upsertDoc(path); - } - } else if (isFileRemove) { - tree.update((tree) => { - for (const path of event.paths) { - delete tree[path]; - } - return tree; - }); - } - }); - - return tree; -}; - -export const store = async () => { - const db = await database.json("projects.json"); - const fromDisk = await db.read(); - const store = writable(fromDisk || []); - store.subscribe(db.write); - return store; }; diff --git a/src/lib/tauri.ts b/src/lib/tauri.ts deleted file mode 100644 index fd799624f..000000000 --- a/src/lib/tauri.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { invoke } from "@tauri-apps/api"; -import { log } from "$lib"; - -export class NoSuchFileOrDirectoryError extends Error { - constructor(message: string) { - super(message); - } -} - -export const readFile = async (filePath: string) => { - log.info("readFile", { path: filePath }); - return invoke("read_file", { filePath }).catch((err) => { - if (err.message === "No such file or directory (os error 2)") { - throw new NoSuchFileOrDirectoryError(err.message); - } else { - throw err; - } - }); -}; - -export const readDir = async (path: string) => { - log.info("readDir", { path }); - return invoke("read_dir", { path }).catch((err) => { - if (err.message === "No such file or directory (os error 2)") { - throw new NoSuchFileOrDirectoryError(err.message); - } else { - throw err; - } - }); -}; diff --git a/src/lib/watch.ts b/src/lib/watch.ts deleted file mode 100644 index 728458ff7..000000000 --- a/src/lib/watch.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { watchImmediate } from "tauri-plugin-fs-watch-api"; - -export type EventTypeModify = { - modify: - | { kind: "metadata"; mode: "ownership" | "any" } - | { kind: "data"; mode: "content" }; -}; - -export type EventTypeRemove = { - remove: { - kind: "file" | "folder"; - }; -}; - -export type EventTypeCreate = { - create: { - kind: "file" | "folder"; - }; -}; - -export type EventType = - | EventTypeCreate - | EventTypeRemove - | EventTypeModify - | any; - -export namespace EventType { - export const isCreate = ( - eventType: EventType - ): eventType is EventTypeCreate => - (eventType as EventTypeCreate).create !== undefined; - - export const isRemove = ( - eventType: EventType - ): eventType is EventTypeRemove => - (eventType as EventTypeRemove).remove !== undefined; - - export const isModify = ( - eventType: EventType - ): eventType is EventTypeModify => - (eventType as EventTypeModify).modify !== undefined; -} - -export type Event = { - type: EventType; - paths: string[]; -}; - -export const watch = ( - path: string | string[], - onEvent: (event: Event) => void -) => watchImmediate(path, { recursive: true }, onEvent); diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index eb4fda2d0..d6079b716 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,8 +3,6 @@ import { open } from "@tauri-apps/api/dialog"; import type { LayoutData } from "./$types"; - import { nanoid } from "nanoid"; - import { path } from "@tauri-apps/api"; import { log } from "$lib"; import { onMount } from "svelte"; import { BackForwardButtons } from "$lib/components"; @@ -12,7 +10,7 @@ onMount(log.setup); export let data: LayoutData; - const projects = data.projects; + const { projects } = data; const onSelectProjectClick = async () => { const selectedPath = await open({ @@ -28,15 +26,7 @@ const projectExists = $projects.some((p) => p.path === projectPath); if (projectExists) return; - const title = await path.basename(projectPath); - $projects = [ - ...$projects, - { - id: nanoid(), - title, - path: projectPath, - }, - ]; + await projects.add({ path: projectPath }); }; @@ -70,5 +60,6 @@ wip + diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts index 4913b68a0..289c2abb1 100644 --- a/src/routes/+layout.ts +++ b/src/routes/+layout.ts @@ -1,4 +1,4 @@ -import { writable } from "svelte/store"; +import { readable } from "svelte/store"; import type { LayoutLoad } from "./$types"; import { building } from "$app/environment"; import type { Project } from "$lib/projects"; @@ -7,9 +7,21 @@ export const ssr = false; export const prerender = true; export const csr = true; -export const load: LayoutLoad = async () => ({ - // tauri apis require window reference which doesn't exist during ssr, so dynamic import here. - projects: building - ? writable([]) - : (await import("$lib/projects")).store(), -}); +export const load: LayoutLoad = async () => { + // tauri apis require window reference which doesn't exist during ssr, so we do not import it here. + if (building) { + return { + projects: { + ...readable([]), + add: () => { + throw new Error("not implemented"); + }, + }, + }; + } else { + const Projects = await import("$lib/projects"); + return { + projects: await Projects.default(), + }; + } +}; diff --git a/src/routes/projects/[id]/+page.svelte b/src/routes/projects/[id]/+page.svelte index cddf069a4..0772c4f7e 100644 --- a/src/routes/projects/[id]/+page.svelte +++ b/src/routes/projects/[id]/+page.svelte @@ -1,17 +1,38 @@
    {#if $showTimeline} - + value.set(e.detail)} /> {/if} - {#each Object.entries($docs) as [filepath, doc]} + {#each Object.entries($docs) as [filepath, value]}
  • {filepath} - +
  • {/each} diff --git a/src/routes/projects/[id]/+page.ts b/src/routes/projects/[id]/+page.ts index a00b46226..4b4804c32 100644 --- a/src/routes/projects/[id]/+page.ts +++ b/src/routes/projects/[id]/+page.ts @@ -1,5 +1,6 @@ import { derived } from "svelte/store"; import type { PageLoad } from "./$types"; +import crdt from "$lib/crdt"; export const prerender = false; @@ -9,5 +10,6 @@ export const load: PageLoad = async ({ parent, params }) => { project: derived(projects, (projects) => projects.find((project) => project.id === params.id) ), + deltas: await crdt({ projectId: params.id }), }; };