improved diffing algorithm
This commit is contained in:
parent
6d1e011621
commit
0501a0a463
6 changed files with 156 additions and 19 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
7
examples/tiny/en-GB.json
Normal file
7
examples/tiny/en-GB.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"keyA": "Value a",
|
||||||
|
"keyB": "Value b",
|
||||||
|
"keyC": "Value c",
|
||||||
|
"keyD": "Value d",
|
||||||
|
"keyE": "Value e"
|
||||||
|
}
|
7
examples/tiny/nl-NL.json
Normal file
7
examples/tiny/nl-NL.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"keyA": "Waarde a",
|
||||||
|
"keyB": "Waarde b",
|
||||||
|
"keyC": "Waarde c",
|
||||||
|
"keyD": "Waarde d",
|
||||||
|
"keyE": "Waarde e"
|
||||||
|
}
|
|
@ -1,8 +1,9 @@
|
||||||
import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
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 { SelectionProvider, useSelection, selectable } from "../selectable";
|
||||||
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||||
import css from './grid.module.css';
|
import css from './grid.module.css';
|
||||||
|
import diff from "microdiff";
|
||||||
|
|
||||||
selectable // prevents removal of import
|
selectable // prevents removal of import
|
||||||
|
|
||||||
|
@ -18,6 +19,7 @@ export interface GridContextType {
|
||||||
readonly selection: Accessor<SelectionItem[]>;
|
readonly selection: Accessor<SelectionItem[]>;
|
||||||
mutate(prop: string, lang: string, value: string): void;
|
mutate(prop: string, lang: string, value: string): void;
|
||||||
remove(props: string[]): void;
|
remove(props: string[]): void;
|
||||||
|
insert(prop: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridApi {
|
export interface GridApi {
|
||||||
|
@ -27,6 +29,7 @@ export interface GridApi {
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
remove(keys: string[]): void;
|
remove(keys: string[]): void;
|
||||||
|
insert(prop: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const GridContext = createContext<GridContextType>();
|
const GridContext = createContext<GridContextType>();
|
||||||
|
@ -54,6 +57,10 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||||
setState('numberOfRows', Object.keys(state.rows).length);
|
setState('numberOfRows', Object.keys(state.rows).length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
createEffect(() => {
|
||||||
|
console.log(mutations());
|
||||||
|
});
|
||||||
|
|
||||||
const ctx: GridContextType = {
|
const ctx: GridContextType = {
|
||||||
rows,
|
rows,
|
||||||
mutations,
|
mutations,
|
||||||
|
@ -64,9 +71,6 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||||
},
|
},
|
||||||
|
|
||||||
remove(props: string[]) {
|
remove(props: string[]) {
|
||||||
console.log(props);
|
|
||||||
|
|
||||||
|
|
||||||
setState('rows', produce(rows => {
|
setState('rows', produce(rows => {
|
||||||
for (const prop of props) {
|
for (const prop of props) {
|
||||||
delete rows[prop];
|
delete rows[prop];
|
||||||
|
@ -74,7 +78,14 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||||
|
|
||||||
return rows;
|
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[]) {
|
remove(props: string[]) {
|
||||||
gridContext.remove(props);
|
gridContext.remove(props);
|
||||||
},
|
},
|
||||||
|
insert(prop: string) {
|
||||||
|
gridContext.insert(prop);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
|
|
@ -79,6 +79,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
return mutations.map(m => {
|
return mutations.map(m => {
|
||||||
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.'));
|
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.'));
|
||||||
|
|
||||||
|
console.log(m.key, key, lang, entries);
|
||||||
|
|
||||||
return { ...m, key, file: entries.get(key)?.[lang] };
|
return { ...m, key, file: entries.get(key)?.[lang] };
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
@ -195,10 +197,12 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
|
|
||||||
remove(Object.keys(selection()));
|
remove(Object.keys(selection()));
|
||||||
}, { key: 'delete', modifier: Modifier.None }),
|
}, { key: 'delete', modifier: Modifier.None }),
|
||||||
|
inserNewKey: createCommand('insert new key', () => {
|
||||||
|
api()?.insert('this.is.some.key');
|
||||||
|
}),
|
||||||
|
inserNewLanguage: noop.withLabel('insert new language'),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const commandCtx = useCommands();
|
|
||||||
|
|
||||||
return <div class={css.root}>
|
return <div class={css.root}>
|
||||||
<Command.Add commands={[commands.saveAs, commands.closeTab]} />
|
<Command.Add commands={[commands.saveAs, commands.closeTab]} />
|
||||||
|
|
||||||
|
@ -218,9 +222,9 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|
||||||
<Menu.Item label="edit">
|
<Menu.Item label="edit">
|
||||||
<Menu.Item command={noop.withLabel('insert new key')} />
|
<Menu.Item command={commands.inserNewKey} />
|
||||||
|
|
||||||
<Menu.Item command={noop.withLabel('insert new language')} />
|
<Menu.Item command={commands.inserNewLanguage} />
|
||||||
|
|
||||||
<Menu.Separator />
|
<Menu.Separator />
|
||||||
|
|
||||||
|
|
127
src/utilities.ts
127
src/utilities.ts
|
@ -58,7 +58,10 @@ export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, pa
|
||||||
return;
|
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) {
|
if (!keyA && !keyB) {
|
||||||
throw new Error('this code should not be reachable, there is a bug with an unhandled/unknown edge case');
|
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<T1 extends object, T2 extends object>(a: T1, b: T2, pa
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyA && !keyB) {
|
if (keyA && !keyB) {
|
||||||
// value was added
|
|
||||||
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete };
|
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete };
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
|
@ -113,20 +115,123 @@ const entriesOf = (subject: object): Iterable<readonly [string | number, any]> =
|
||||||
|
|
||||||
return Object.entries(subject);
|
return Object.entries(subject);
|
||||||
};
|
};
|
||||||
const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable<readonly [string | number, any]>): Generator<readonly [[string | number | undefined, any], [string | number | undefined, any]], void, unknown> {
|
const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable<readonly [string | number, any]>): Generator<readonly [readonly [string | number | undefined, any], readonly [string | number | undefined, any]], void, unknown> {
|
||||||
const iterA = Iterator.from(a);
|
const iterA = bufferredIterator(a);
|
||||||
const iterB = Iterator.from(b);
|
const iterB = bufferredIterator(b);
|
||||||
|
|
||||||
while (true) {
|
const EMPTY = [undefined, undefined] as [string | number | undefined, any];
|
||||||
const { done: doneA, value: entryA = [] } = iterA.next() ?? {};
|
|
||||||
const { done: doneB, value: entryB = [] } = iterB.next() ?? {};
|
|
||||||
|
|
||||||
if (doneA && doneB) {
|
while (!iterA.done || !iterB.done) {
|
||||||
break;
|
// 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 = <T extends readonly [string | number, any]>(subject: Iterable<T>) => {
|
||||||
|
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 {
|
export interface filter {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue