diff --git a/bun.lockb b/bun.lockb index a6fff26..e9a557a 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/examples/tiny/en-GB.json b/examples/tiny/en-GB.json new file mode 100644 index 0000000..1607a8b --- /dev/null +++ b/examples/tiny/en-GB.json @@ -0,0 +1,7 @@ +{ + "keyA": "Value a", + "keyB": "Value b", + "keyC": "Value c", + "keyD": "Value d", + "keyE": "Value e" +} \ No newline at end of file diff --git a/examples/tiny/nl-NL.json b/examples/tiny/nl-NL.json new file mode 100644 index 0000000..78ec2f4 --- /dev/null +++ b/examples/tiny/nl-NL.json @@ -0,0 +1,7 @@ +{ + "keyA": "Waarde a", + "keyB": "Waarde b", + "keyC": "Waarde c", + "keyD": "Waarde d", + "keyE": "Waarde e" +} \ No newline at end of file diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 9fdd7e1..e11d368 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,8 +1,9 @@ import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, onMount, ParentComponent, Show, useContext } from "solid-js"; -import { createStore, produce, unwrap } from "solid-js/store"; +import { createStore, produce, reconcile, unwrap } from "solid-js/store"; import { SelectionProvider, useSelection, selectable } from "../selectable"; import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities"; import css from './grid.module.css'; +import diff from "microdiff"; selectable // prevents removal of import @@ -18,6 +19,7 @@ export interface GridContextType { readonly selection: Accessor; mutate(prop: string, lang: string, value: string): void; remove(props: string[]): void; + insert(prop: string): void; } export interface GridApi { @@ -27,6 +29,7 @@ export interface GridApi { selectAll(): void; clear(): void; remove(keys: string[]): void; + insert(prop: string): void; } const GridContext = createContext(); @@ -54,6 +57,10 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => { setState('numberOfRows', Object.keys(state.rows).length); }); + createEffect(() => { + console.log(mutations()); + }); + const ctx: GridContextType = { rows, mutations, @@ -64,9 +71,6 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => { }, remove(props: string[]) { - console.log(props); - - setState('rows', produce(rows => { for (const prop of props) { delete rows[prop]; @@ -74,7 +78,14 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => { return rows; })); + }, + insert(prop: string) { + setState('rows', produce(rows => { + rows[prop] = { en: '' }; + + return rows + })) }, }; @@ -143,6 +154,9 @@ const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => remove(props: string[]) { gridContext.remove(props); }, + insert(prop: string) { + gridContext.insert(prop); + }, }; createEffect(() => { diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index 28728ec..d3f7381 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -79,6 +79,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return mutations.map(m => { const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); + console.log(m.key, key, lang, entries); + return { ...m, key, file: entries.get(key)?.[lang] }; }); })); @@ -195,10 +197,12 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { remove(Object.keys(selection())); }, { key: 'delete', modifier: Modifier.None }), + inserNewKey: createCommand('insert new key', () => { + api()?.insert('this.is.some.key'); + }), + inserNewLanguage: noop.withLabel('insert new language'), } as const; - const commandCtx = useCommands(); - return
@@ -218,9 +222,9 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { - + - + diff --git a/src/utilities.ts b/src/utilities.ts index f20d938..0f5651c 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -58,7 +58,10 @@ export function* deepDiff(a: T1, b: T2, pa return; } - for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b))) { + for (const [[keyA, valueA], [keyB, valueB]] of zip(entriesOf(a), entriesOf(b)).take(10)) { + // console.log('deepdiff', keyA, valueA, keyB, valueB); + // continue; + if (!keyA && !keyB) { throw new Error('this code should not be reachable, there is a bug with an unhandled/unknown edge case'); } @@ -70,7 +73,6 @@ export function* deepDiff(a: T1, b: T2, pa } if (keyA && !keyB) { - // value was added yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete }; continue; @@ -113,22 +115,125 @@ const entriesOf = (subject: object): Iterable = return Object.entries(subject); }; -const zip = function* (a: Iterable, b: Iterable): Generator { - const iterA = Iterator.from(a); - const iterB = Iterator.from(b); +const zip = function* (a: Iterable, b: Iterable): Generator { + const iterA = bufferredIterator(a); + const iterB = bufferredIterator(b); - while (true) { - const { done: doneA, value: entryA = [] } = iterA.next() ?? {}; - const { done: doneB, value: entryB = [] } = iterB.next() ?? {}; + const EMPTY = [undefined, undefined] as [string | number | undefined, any]; - if (doneA && doneB) { - break; + while (!iterA.done || !iterB.done) { + // if we have a match on the keys of a and b we can simply consume and yield + if (iterA.current.key === iterB.current.key) { + yield [iterA.consume(), iterB.consume()]; + + iterA.advance(); + iterB.advance(); } - yield [entryA, entryB] as const; + // key of a aligns with last key in buffer b + // conclusion: a has key(s) that b does not + else if (iterA.current.key === iterB.top.key) { + const a = iterA.pop()!; + + for (const [key, value] of iterA.flush()) { + yield [[key, value], EMPTY]; + } + + yield [a, iterB.consume()]; + + iterB.advance(); + } + + // the reverse case, key of b is aligns with the last key in buffer a + // conclusion: a is missing key(s) the b does have + else if (iterB.current.key === iterA.top.key) { + const b = iterB.pop()!; + + for (const [key, value] of iterB.flush()) { + yield [EMPTY, [key, value]]; + } + + yield [iterA.consume(), b]; + + iterA.advance(); + } + + // Neiter of the above cases are hit. + // conclusion: there still is no alignment. + else { + iterA.advance(); + iterB.advance(); + } } }; +const bufferredIterator = (subject: Iterable) => { + const iterator = Iterator.from(subject); + const buffer: T[] = []; + let cursor: number = 0; + let done = false; + + const next = () => { + const res = iterator.next(); + done = res.done ?? false; + + if (!done) { + cursor = buffer.push(res.value) - 1; + } + }; + + next(); + + return { + advance() { + if (buffer.length > 0 && cursor < (buffer.length - 1)) { + cursor++; + } + else { + next(); + } + }, + + consume() { + cursor = 0; + + return buffer.shift()!; + }, + + flush(): T[] { + cursor = 0; + + return buffer.splice(0, buffer.length); + }, + + pop() { + cursor--; + + return buffer.pop(); + }, + + get done() { + return done && Math.max(0, buffer.length - 1) === cursor; + }, + + get top() { + const [key = undefined, value = undefined] = buffer.at(0) ?? []; + + return { key, value }; + }, + + get current() { + const [key = undefined, value = undefined] = buffer.at(cursor) ?? []; + + return { key, value }; + }, + + get entry() { + return [this.current.key, this.current.value] as const; + } + }; +}; + export interface filter { (subject: AsyncIterableIterator, predicate: (value: T) => value is S): AsyncGenerator; (subject: AsyncIterableIterator, predicate: (value: T) => unknown): AsyncGenerator;