From 0501a0a46378c97c088eae2517d42eb3cd48cd0a Mon Sep 17 00:00:00 2001 From: Chris Kruining Date: Wed, 30 Oct 2024 16:27:34 +0100 Subject: [PATCH] improved diffing algorithm --- bun.lockb | Bin 323764 -> 323764 bytes examples/tiny/en-GB.json | 7 ++ examples/tiny/nl-NL.json | 7 ++ src/features/file/grid.tsx | 22 ++++-- src/routes/(editor)/edit.tsx | 12 ++-- src/utilities.ts | 127 ++++++++++++++++++++++++++++++++--- 6 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 examples/tiny/en-GB.json create mode 100644 examples/tiny/nl-NL.json diff --git a/bun.lockb b/bun.lockb index a6fff260c04433c53b91aa817920f8fc5164aa9f..e9a557a81505bbcfc22c501ff8221b0e6b8456c8 100644 GIT binary patch delta 34 qcmdn;P; 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;