dirvi-lib 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/domain/posix-node.d.ts +29 -0
- package/dist/domain/posix-node.d.ts.map +1 -0
- package/dist/domain/posix-node.js +20 -0
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -8
- package/dist/tree-surfer/cursor.d.ts +24 -0
- package/dist/tree-surfer/cursor.d.ts.map +1 -0
- package/dist/tree-surfer/cursor.js +65 -0
- package/dist/tree-surfer/fold-node/fold-node.impl.d.ts +5 -0
- package/dist/tree-surfer/fold-node/fold-node.impl.d.ts.map +1 -0
- package/dist/tree-surfer/fold-node/fold-node.impl.js +143 -0
- package/dist/tree-surfer/fold-node/fold-node.types.d.ts +70 -0
- package/dist/tree-surfer/fold-node/fold-node.types.d.ts.map +1 -0
- package/dist/tree-surfer/fold-node/fold-node.types.js +1 -0
- package/dist/tree-surfer/index.d.ts +9 -0
- package/dist/tree-surfer/index.d.ts.map +1 -0
- package/dist/tree-surfer/index.js +8 -0
- package/dist/tree-surfer/nav-node/nav-node.impl.d.ts +6 -0
- package/dist/tree-surfer/nav-node/nav-node.impl.d.ts.map +1 -0
- package/dist/tree-surfer/nav-node/nav-node.impl.js +191 -0
- package/dist/tree-surfer/nav-node/nav-node.types.d.ts +42 -0
- package/dist/tree-surfer/nav-node/nav-node.types.d.ts.map +1 -0
- package/dist/tree-surfer/nav-node/nav-node.types.js +3 -0
- package/dist/tree-surfer/state.d.ts +19 -0
- package/dist/tree-surfer/state.d.ts.map +1 -0
- package/dist/tree-surfer/state.js +109 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.d.ts +31 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.arb.js +80 -0
- package/dist/tree-surfer/tree-node/tree-node.impl.d.ts +13 -0
- package/dist/tree-surfer/tree-node/tree-node.impl.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.impl.js +87 -0
- package/dist/tree-surfer/tree-node/tree-node.test.d.ts +2 -0
- package/dist/tree-surfer/tree-node/tree-node.test.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.test.js +117 -0
- package/dist/tree-surfer/tree-node/tree-node.types.d.ts +184 -0
- package/dist/tree-surfer/tree-node/tree-node.types.d.ts.map +1 -0
- package/dist/tree-surfer/tree-node/tree-node.types.js +12 -0
- package/dist/tree-surfer-ui/application/index.d.ts +3 -0
- package/dist/tree-surfer-ui/application/index.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/index.js +2 -0
- package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.d.ts +2 -0
- package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.js +192 -0
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts +6 -0
- package/dist/tree-surfer-ui/application/intent-to-effect.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/intent-to-effect.js +137 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.d.ts +3 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.js +98 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.test.d.ts +2 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.test.d.ts.map +1 -0
- package/dist/tree-surfer-ui/application/user-input-to-intent.test.js +118 -0
- package/dist/tree-surfer-ui/domain/effect.d.ts +36 -0
- package/dist/tree-surfer-ui/domain/effect.d.ts.map +1 -0
- package/dist/tree-surfer-ui/domain/effect.js +1 -0
- package/dist/tree-surfer-ui/domain/index.d.ts +5 -0
- package/dist/tree-surfer-ui/domain/index.d.ts.map +1 -0
- package/dist/tree-surfer-ui/domain/index.js +4 -0
- package/dist/tree-surfer-ui/domain/input-state.d.ts +8 -0
- package/dist/tree-surfer-ui/domain/input-state.d.ts.map +1 -0
- package/dist/tree-surfer-ui/domain/input-state.js +1 -0
- package/dist/tree-surfer-ui/domain/intent.d.ts +25 -0
- package/dist/tree-surfer-ui/domain/intent.d.ts.map +1 -0
- package/dist/tree-surfer-ui/domain/intent.js +1 -0
- package/dist/tree-surfer-ui/domain/user-input.d.ts +19 -0
- package/dist/tree-surfer-ui/domain/user-input.d.ts.map +1 -0
- package/dist/tree-surfer-ui/domain/user-input.js +1 -0
- package/package.json +6 -3
- package/dist/domain/cursor.d.ts +0 -17
- package/dist/domain/cursor.d.ts.map +0 -1
- package/dist/domain/cursor.js +0 -44
- package/dist/domain/display-row.d.ts +0 -29
- package/dist/domain/display-row.d.ts.map +0 -1
- package/dist/domain/display-row.js +0 -29
- package/dist/domain/fold-node.d.ts +0 -17
- package/dist/domain/fold-node.d.ts.map +0 -1
- package/dist/domain/fold-node.js +0 -81
- package/dist/domain/fold.d.ts +0 -10
- package/dist/domain/fold.d.ts.map +0 -1
- package/dist/domain/fold.js +0 -24
- package/dist/domain/navigation-entry.d.ts +0 -23
- package/dist/domain/navigation-entry.d.ts.map +0 -1
- package/dist/domain/navigation-entry.js +0 -52
- package/dist/domain/navigation-node.d.ts +0 -43
- package/dist/domain/navigation-node.d.ts.map +0 -1
- package/dist/domain/navigation-node.js +0 -198
- package/dist/domain/state.d.ts +0 -12
- package/dist/domain/state.d.ts.map +0 -1
- package/dist/domain/state.js +0 -12
- package/dist/domain/unix-entry.d.ts +0 -28
- package/dist/domain/unix-entry.d.ts.map +0 -1
- package/dist/domain/unix-entry.js +0 -96
- package/dist/domain/view.d.ts +0 -15
- package/dist/domain/view.d.ts.map +0 -1
- package/dist/domain/view.js +0 -12
- package/dist/infrastructure/get-cwd.d.ts +0 -3
- package/dist/infrastructure/get-cwd.d.ts.map +0 -1
- package/dist/infrastructure/get-cwd.js +0 -13
- package/dist/infrastructure/get-dir-lazy-entries.d.ts +0 -4
- package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +0 -1
- package/dist/infrastructure/get-dir-lazy-entries.js +0 -34
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { LeafTreeNode, SerializableKey, TreeNode } from '../tree-node/tree-node.types.js';
|
|
2
|
+
import { FoldNodeRoot } from '../fold-node/fold-node.types.js';
|
|
3
|
+
import { Cursor } from '../cursor.js';
|
|
4
|
+
/**
|
|
5
|
+
* A navigation entry preserves the original buffer-node properties, but
|
|
6
|
+
* replaces a branch's buffer children with a NavNode.
|
|
7
|
+
*/
|
|
8
|
+
export type NavEntry<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = NavLeaf<Id, BufferNode> | NavBranch<Id, BufferNode>;
|
|
9
|
+
export type NavLeaf<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = BufferNode & LeafTreeNode<Id>;
|
|
10
|
+
type ReplaceChildren<Node, Children> = Node extends {
|
|
11
|
+
children: unknown;
|
|
12
|
+
} ? Omit<Node, 'children'> & {
|
|
13
|
+
children: Children;
|
|
14
|
+
} : never;
|
|
15
|
+
export type NavBranch<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = ReplaceChildren<BufferNode, NavNode<Id, BufferNode> | null>;
|
|
16
|
+
export declare function isNavBranch<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(entry: NavEntry<Id, BufferNode>): entry is NavBranch<Id, BufferNode>;
|
|
17
|
+
/**
|
|
18
|
+
* The derived tree as navigated: list of visible entries and folded entries.
|
|
19
|
+
*/
|
|
20
|
+
export type NavNode<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
|
|
21
|
+
/**
|
|
22
|
+
* Currently visible entries.
|
|
23
|
+
*/
|
|
24
|
+
entries: NavEntry<Id, BufferNode>[];
|
|
25
|
+
/**
|
|
26
|
+
* Currently loaded entries hidden by this directory's fold.
|
|
27
|
+
*/
|
|
28
|
+
foldedEntries: NavEntry<Id, BufferNode>[];
|
|
29
|
+
};
|
|
30
|
+
export type NavNodeApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
|
|
31
|
+
from(entries: BufferNode[], foldNode: FoldNodeRoot<Id>): NavNode<Id, BufferNode>;
|
|
32
|
+
getNodeAtPath(navigation: NavNode<Id, BufferNode>, path: Id[]): NavNode<Id, BufferNode> | undefined;
|
|
33
|
+
getEntryAtPath(navigation: NavNode<Id, BufferNode>, path: Id[]): NavEntry<Id, BufferNode> | undefined;
|
|
34
|
+
nextCursor(rootNode: NavNode<Id, BufferNode>, cursor: Cursor<Id>): Cursor<Id> | undefined;
|
|
35
|
+
previousCursor(rootNode: NavNode<Id, BufferNode>, cursor: Cursor<Id>): Cursor<Id> | undefined;
|
|
36
|
+
cursorAfterFold(rootNode: NavNode<Id, BufferNode>, cursor: Cursor<Id>): Cursor<Id> | undefined;
|
|
37
|
+
parentCursor(navigation: NavNode<Id, BufferNode>, cursor: Cursor<Id>): Cursor<Id> | undefined;
|
|
38
|
+
cursors(navigation: NavNode<Id, BufferNode>, parentPath?: Id[]): Cursor<Id>[];
|
|
39
|
+
visibleLeavesPaths(navigation: NavNode<Id, BufferNode>, parentPath?: Id[]): Id[][];
|
|
40
|
+
};
|
|
41
|
+
export {};
|
|
42
|
+
//# sourceMappingURL=nav-node.types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nav-node.types.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/nav-node/nav-node.types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Cursor, CursorApi } from './cursor.js';
|
|
2
|
+
import { SerializableKey, TreeNode, TreeNodeApi } from './tree-node/tree-node.types.js';
|
|
3
|
+
import { FoldNodeApi, FoldNodeRoot } from './fold-node/fold-node.types.js';
|
|
4
|
+
import { NavNodeApi } from './nav-node/nav-node.types.js';
|
|
5
|
+
export type State<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
|
|
6
|
+
buffer: BufferNode[];
|
|
7
|
+
foldNode: FoldNodeRoot<Id>;
|
|
8
|
+
cursor: Cursor<Id>;
|
|
9
|
+
};
|
|
10
|
+
export type StateApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = {
|
|
11
|
+
getNodeAtCursor(state: State<Id, BufferNode>): BufferNode | undefined;
|
|
12
|
+
/**
|
|
13
|
+
* Reloads the root and all descendant branches that were loaded in the
|
|
14
|
+
* previous state.
|
|
15
|
+
*/
|
|
16
|
+
resync(oldState: State<Id, BufferNode>, loadBranches: (path: Id[]) => Promise<BufferNode[]>): Promise<State<Id, BufferNode>>;
|
|
17
|
+
};
|
|
18
|
+
export declare function createStateApi<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(treeNodeApi: TreeNodeApi<Id, BufferNode>, foldNodeApi: FoldNodeApi<Id>, cursorApi: CursorApi<Id>, navNodeApi: NavNodeApi<Id, BufferNode>): StateApi<Id, BufferNode>;
|
|
19
|
+
//# sourceMappingURL=state.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state.d.ts","sourceRoot":"","sources":["../../src/tree-surfer/state.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,SAAS,EAAc,MAAM,aAAa,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AACxF,OAAO,EAAY,WAAW,EAAE,YAAY,EAAE,MAAM,gCAAgC,CAAC;AACrF,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAE1D,MAAM,MAAM,KAAK,CACf,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC;IACF,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,QAAQ,EAAE,YAAY,CAAC,EAAE,CAAC,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,QAAQ,CAClB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC;IACF,eAAe,CAAC,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,GAAG,UAAU,GAAG,SAAS,CAAC;IAEtE;;;OAGG;IACH,MAAM,CACJ,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,EAC/B,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,UAAU,EAAE,CAAC,GAClD,OAAO,CAAC,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,CAAC,CAAC;CACnC,CAAC;AAEF,wBAAgB,cAAc,CAC5B,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAE3C,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,EACxC,WAAW,EAAE,WAAW,CAAC,EAAE,CAAC,EAC5B,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,EACxB,UAAU,EAAE,UAAU,CAAC,EAAE,EAAE,UAAU,CAAC,GACrC,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,CAoK1B"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { CursorKind } from './cursor.js';
|
|
2
|
+
export function createStateApi(treeNodeApi, foldNodeApi, cursorApi, navNodeApi) {
|
|
3
|
+
// Helpers
|
|
4
|
+
/**
|
|
5
|
+
* Create a new buffer and fold node at this level.
|
|
6
|
+
*
|
|
7
|
+
* Works on fold root and child level through the generic.
|
|
8
|
+
*/
|
|
9
|
+
async function reload(oldBuffer, oldFoldNode, // Can be FoldNode too
|
|
10
|
+
parentPath, loadBranches) {
|
|
11
|
+
let newBufferAtRoot = await loadBranches(parentPath);
|
|
12
|
+
let newFoldNode = oldFoldNode; // Start with assumption of no changes
|
|
13
|
+
/**
|
|
14
|
+
* Reload descendants that were already loaded in the old buffer.
|
|
15
|
+
*
|
|
16
|
+
* Fold membership is represented by IDs in `folds`, so it remains
|
|
17
|
+
* valid across a reload without rebuilding an entry array.
|
|
18
|
+
*/
|
|
19
|
+
for (const oldEntry of oldBuffer) {
|
|
20
|
+
// If it wasn't an open branch, then there's no need to reload descendants.
|
|
21
|
+
// Either the new entry is a leaf or an unopen branch.
|
|
22
|
+
if (!treeNodeApi.isBranch(oldEntry) ||
|
|
23
|
+
!treeNodeApi.isOpenBranch(oldEntry)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
// Get the entry being updated, in the new buffer
|
|
27
|
+
const newEntry = treeNodeApi.getAtPath(newBufferAtRoot, [oldEntry.id], (entry) => entry);
|
|
28
|
+
// If it doesn't exist (an open branch was deleted) then move on
|
|
29
|
+
if (newEntry === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// The old entry may have changed into a leaf.
|
|
33
|
+
// A newly loaded branch is normally closed here because loadBranches
|
|
34
|
+
// returns flat entries, and the descendants are to be attached below.
|
|
35
|
+
if (!treeNodeApi.isBranch(newEntry)) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const oldChildFoldNode = oldFoldNode === undefined
|
|
39
|
+
? undefined
|
|
40
|
+
: foldNodeApi.getChildById(oldFoldNode, oldEntry.id);
|
|
41
|
+
const entryPath = [...parentPath, oldEntry.id];
|
|
42
|
+
const reloadedChild = await reload([...treeNodeApi.getChildren(oldEntry)], oldChildFoldNode, entryPath, loadBranches);
|
|
43
|
+
const updatedBuffer = treeNodeApi.modifyAtPath(newBufferAtRoot, [oldEntry.id], (entry) => {
|
|
44
|
+
// The entry may be closed: loadBranches normally returns branches
|
|
45
|
+
// with `branches: null`. We are turning it back into an open branch.
|
|
46
|
+
if (!treeNodeApi.isBranch(entry)) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
...entry,
|
|
51
|
+
branches: reloadedChild.buffer,
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
if (updatedBuffer !== undefined) {
|
|
55
|
+
newBufferAtRoot = updatedBuffer;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
buffer: newBufferAtRoot,
|
|
60
|
+
foldNode: newFoldNode,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Currently the cursor just goes to the first entry at root if
|
|
65
|
+
* not all root entries are folded, otherwise the fold.
|
|
66
|
+
*
|
|
67
|
+
* TODO: Cursor policy:
|
|
68
|
+
* 1. Keep the exact cursor if it survives,
|
|
69
|
+
* 2. Otherwise, choose the deepest surviving ancestor entry,
|
|
70
|
+
* 3. Otherwise, choose the nearest preceding old cursor that still survives,
|
|
71
|
+
* 4. Otherwise, choose the first new cursor.
|
|
72
|
+
* 5. If no cursor exists, use the root fold cursor.
|
|
73
|
+
*/
|
|
74
|
+
function resyncCursor(oldState, newBuffer, newFoldNode) {
|
|
75
|
+
const navigation = navNodeApi.from(newBuffer, newFoldNode);
|
|
76
|
+
return navNodeApi.cursors(navigation)[0];
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
getNodeAtCursor(state) {
|
|
80
|
+
const path = cursorApi.getPath(state.cursor);
|
|
81
|
+
if (path === undefined) {
|
|
82
|
+
// The cursor is positioned on a fold rather than an entry.
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return treeNodeApi.getAtPath(state.buffer, path, (node) => node);
|
|
86
|
+
},
|
|
87
|
+
async resync(oldState, loadBranches) {
|
|
88
|
+
const reloaded = await reload(oldState.buffer, oldState.foldNode, [], loadBranches);
|
|
89
|
+
/*
|
|
90
|
+
* `oldState.foldNode` is always present. At the root reload starts with
|
|
91
|
+
* that node, so `reloaded.foldNode` should also always be present.
|
|
92
|
+
*
|
|
93
|
+
* The fallback expresses the State invariant while satisfying the
|
|
94
|
+
* optional return type needed by recursive child reloads.
|
|
95
|
+
*/
|
|
96
|
+
const foldNode = reloaded.foldNode ?? oldState.foldNode;
|
|
97
|
+
const cursor = resyncCursor(oldState, reloaded.buffer, foldNode) ?? {
|
|
98
|
+
kind: CursorKind.Fold,
|
|
99
|
+
parentPath: [],
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
...oldState,
|
|
103
|
+
buffer: reloaded.buffer,
|
|
104
|
+
foldNode,
|
|
105
|
+
cursor,
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fc from 'fast-check';
|
|
2
|
+
import type { TreeNode } from './tree-node.types.js';
|
|
3
|
+
export type StringNode = TreeNode<string, StringNode>;
|
|
4
|
+
export type PathEntry = {
|
|
5
|
+
path: string[];
|
|
6
|
+
node: StringNode;
|
|
7
|
+
};
|
|
8
|
+
export type GeneratedNode = {
|
|
9
|
+
node: StringNode;
|
|
10
|
+
pathEntries: PathEntry[];
|
|
11
|
+
};
|
|
12
|
+
export declare const idArb: fc.Arbitrary<string>;
|
|
13
|
+
export declare const generatedNodeArb: fc.Arbitrary<GeneratedNode>;
|
|
14
|
+
export declare const generatedNodesArrayArb: fc.Arbitrary<GeneratedNode[]>;
|
|
15
|
+
export declare const forestAndPathsArb: fc.Arbitrary<{
|
|
16
|
+
entries: any[];
|
|
17
|
+
pathEntries: PathEntry[];
|
|
18
|
+
}>;
|
|
19
|
+
export declare const forestAndPathAndExpectedArb: fc.Arbitrary<{
|
|
20
|
+
entries: any[];
|
|
21
|
+
path: string[];
|
|
22
|
+
expected: any;
|
|
23
|
+
}>;
|
|
24
|
+
export declare const nodesArrayAndBranchPathArb: fc.Arbitrary<{
|
|
25
|
+
entries: any[];
|
|
26
|
+
path: string[];
|
|
27
|
+
expected: any;
|
|
28
|
+
}>;
|
|
29
|
+
export declare const newBranchesArb: fc.Arbitrary<any[]>;
|
|
30
|
+
export declare const newBranchesOrNullArb: fc.Arbitrary<any[] | null>;
|
|
31
|
+
//# sourceMappingURL=tree-node.arb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.arb.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/tree-node/tree-node.arb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,YAAY,CAAC;AAEjC,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAGrD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;AAEtD,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,SAAS,EAAE,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,KAAK,sBAGhB,CAAC;AAEH,eAAO,MAAM,gBAAgB,EAAE,EAAE,CAAC,SAAS,CAAC,aAAa,CA4DjD,CAAC;AAET,eAAO,MAAM,sBAAsB,+BAIjC,CAAC;AAEH,eAAO,MAAM,iBAAiB;;;EAK7B,CAAC;AAEF,eAAO,MAAM,2BAA2B;;;;EAQrC,CAAC;AAEJ,eAAO,MAAM,0BAA0B;;;;EAYpC,CAAC;AAEJ,eAAO,MAAM,cAAc,qBAE1B,CAAC;AAEF,eAAO,MAAM,oBAAoB,4BAA8C,CAAC"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as fc from 'fast-check';
|
|
2
|
+
export const idArb = fc.string({
|
|
3
|
+
minLength: 1,
|
|
4
|
+
maxLength: 8,
|
|
5
|
+
});
|
|
6
|
+
export const generatedNodeArb = fc.letrec((tie) => ({
|
|
7
|
+
node: fc.oneof(idArb.map((id) => {
|
|
8
|
+
const node = { id: id };
|
|
9
|
+
return {
|
|
10
|
+
node,
|
|
11
|
+
pathEntries: [
|
|
12
|
+
{
|
|
13
|
+
path: [id],
|
|
14
|
+
node,
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
}), fc
|
|
19
|
+
.tuple(idArb, fc.oneof(fc.constant(null), fc.uniqueArray(tie('node'), {
|
|
20
|
+
minLength: 0,
|
|
21
|
+
size: 'medium',
|
|
22
|
+
// @ts-ignore
|
|
23
|
+
selector: (generated) => generated.node.id,
|
|
24
|
+
})))
|
|
25
|
+
.map(([id, children]) => {
|
|
26
|
+
const node = {
|
|
27
|
+
id,
|
|
28
|
+
children:
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
children === null ? null : children.map((child) => child.node),
|
|
31
|
+
};
|
|
32
|
+
const descendantPathEntries = children === null
|
|
33
|
+
? []
|
|
34
|
+
: children.flatMap((child) =>
|
|
35
|
+
// @ts-ignore
|
|
36
|
+
child.pathEntries.map(({ path, node }) => ({
|
|
37
|
+
path: [id, ...path],
|
|
38
|
+
node,
|
|
39
|
+
})));
|
|
40
|
+
return {
|
|
41
|
+
node,
|
|
42
|
+
pathEntries: [
|
|
43
|
+
{
|
|
44
|
+
path: [id],
|
|
45
|
+
node,
|
|
46
|
+
},
|
|
47
|
+
...descendantPathEntries,
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
})),
|
|
51
|
+
})).node;
|
|
52
|
+
export const generatedNodesArrayArb = fc.uniqueArray(generatedNodeArb, {
|
|
53
|
+
minLength: 0,
|
|
54
|
+
size: 'small',
|
|
55
|
+
selector: (generated) => generated.node.id,
|
|
56
|
+
});
|
|
57
|
+
export const forestAndPathsArb = generatedNodesArrayArb.map((generatedNodes) => ({
|
|
58
|
+
entries: generatedNodes.map((generated) => generated.node),
|
|
59
|
+
pathEntries: generatedNodes.flatMap((generated) => generated.pathEntries),
|
|
60
|
+
}));
|
|
61
|
+
export const forestAndPathAndExpectedArb = forestAndPathsArb
|
|
62
|
+
.filter(({ pathEntries }) => pathEntries.length > 0)
|
|
63
|
+
.chain(({ entries, pathEntries }) => fc.constantFrom(...pathEntries).map(({ path, node }) => ({
|
|
64
|
+
entries,
|
|
65
|
+
path,
|
|
66
|
+
expected: node,
|
|
67
|
+
})));
|
|
68
|
+
export const nodesArrayAndBranchPathArb = forestAndPathsArb
|
|
69
|
+
.map(({ entries, pathEntries }) => ({
|
|
70
|
+
entries,
|
|
71
|
+
branchPathEntries: pathEntries.filter(({ node }) => 'children' in node),
|
|
72
|
+
}))
|
|
73
|
+
.filter(({ branchPathEntries }) => branchPathEntries.length > 0)
|
|
74
|
+
.chain(({ entries, branchPathEntries }) => fc.constantFrom(...branchPathEntries).map(({ path, node }) => ({
|
|
75
|
+
entries,
|
|
76
|
+
path,
|
|
77
|
+
expected: node,
|
|
78
|
+
})));
|
|
79
|
+
export const newBranchesArb = generatedNodesArrayArb.map((generatedNodes) => generatedNodes.map(({ node }) => node));
|
|
80
|
+
export const newBranchesOrNullArb = fc.oneof(newBranchesArb, fc.constant(null));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SerializableKey, TreeNode, TreeNodeApi } from './tree-node.types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Creates an API for inspecting and immutably updating tree nodes.
|
|
4
|
+
*
|
|
5
|
+
* Tree updates do not mutate the supplied forest or its nodes. Updated
|
|
6
|
+
* arrays and ancestor nodes are created as needed, while unrelated nodes
|
|
7
|
+
* retain their original object identity.
|
|
8
|
+
*
|
|
9
|
+
* Node IDs are compared using JavaScript value equality. Sibling IDs
|
|
10
|
+
* should be unique.
|
|
11
|
+
*/
|
|
12
|
+
export declare function createTreeNodeApi<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>>(): TreeNodeApi<Id, ChildNode>;
|
|
13
|
+
//# sourceMappingURL=tree-node.impl.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.impl.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/tree-node/tree-node.impl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIU,eAAe,EACnC,QAAQ,EACR,WAAW,EACZ,MAAM,sBAAsB,CAAC;AAE9B;;;;;;;;;GASG;AACH,wBAAgB,iBAAiB,CAC/B,EAAE,SAAS,eAAe,EAC1B,SAAS,SAAS,QAAQ,CAAC,EAAE,EAAE,SAAS,CAAC,KACtC,WAAW,CAAC,EAAE,EAAE,SAAS,CAAC,CAwG9B"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates an API for inspecting and immutably updating tree nodes.
|
|
3
|
+
*
|
|
4
|
+
* Tree updates do not mutate the supplied forest or its nodes. Updated
|
|
5
|
+
* arrays and ancestor nodes are created as needed, while unrelated nodes
|
|
6
|
+
* retain their original object identity.
|
|
7
|
+
*
|
|
8
|
+
* Node IDs are compared using JavaScript value equality. Sibling IDs
|
|
9
|
+
* should be unique.
|
|
10
|
+
*/
|
|
11
|
+
export function createTreeNodeApi() {
|
|
12
|
+
const treeNodeApi = {
|
|
13
|
+
isLeaf(node) {
|
|
14
|
+
return !('children' in node);
|
|
15
|
+
},
|
|
16
|
+
isBranch(node) {
|
|
17
|
+
return 'children' in node;
|
|
18
|
+
},
|
|
19
|
+
isClosedBranch(node) {
|
|
20
|
+
return node.children === null;
|
|
21
|
+
},
|
|
22
|
+
isOpenBranch(node) {
|
|
23
|
+
return node.children !== null;
|
|
24
|
+
},
|
|
25
|
+
getChildren(node) {
|
|
26
|
+
return node.children;
|
|
27
|
+
},
|
|
28
|
+
getChildById(node, id) {
|
|
29
|
+
return node.children.find((child) => child.id === id);
|
|
30
|
+
},
|
|
31
|
+
getAtPath(entries, path, selector) {
|
|
32
|
+
if (path.length === 0) {
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
// Accumulator
|
|
36
|
+
let node = entries.find((candidate) => candidate.id === path[0]);
|
|
37
|
+
for (let index = 1; node !== undefined && index < path.length; index += 1) {
|
|
38
|
+
if (treeNodeApi.isOpenBranch(node)) {
|
|
39
|
+
node = treeNodeApi.getChildById(node, path[index]);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
node = undefined;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return node === undefined ? undefined : selector(node);
|
|
46
|
+
},
|
|
47
|
+
modifyAtPath(entries, path, modifier) {
|
|
48
|
+
if (path.length === 0) {
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
// Only cloned arrays/nodes are modified below.
|
|
52
|
+
const updatedEntries = [...entries];
|
|
53
|
+
let currentEntries = updatedEntries;
|
|
54
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
55
|
+
const id = path[index];
|
|
56
|
+
const childIndex = currentEntries.findIndex((candidate) => candidate.id === id);
|
|
57
|
+
if (childIndex === -1) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
const child = currentEntries[childIndex];
|
|
61
|
+
if (index === path.length - 1) {
|
|
62
|
+
const replacement = modifier(child);
|
|
63
|
+
if (replacement === undefined) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
currentEntries[childIndex] = replacement;
|
|
67
|
+
return updatedEntries;
|
|
68
|
+
}
|
|
69
|
+
if (!treeNodeApi.isBranch(child) || !treeNodeApi.isOpenBranch(child)) {
|
|
70
|
+
// The path cannot continue through a leaf or closed branch.
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
// Clone the child array before modifying it.
|
|
74
|
+
const updatedChildren = [...treeNodeApi.getChildren(child)];
|
|
75
|
+
// Clone the ancestor node and attach the cloned child array.
|
|
76
|
+
currentEntries[childIndex] = {
|
|
77
|
+
...child,
|
|
78
|
+
children: updatedChildren,
|
|
79
|
+
};
|
|
80
|
+
// Continue traversal through the cloned children.
|
|
81
|
+
currentEntries = updatedChildren;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
return treeNodeApi;
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tree-node.test.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/tree-node/tree-node.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import * as fc from 'fast-check';
|
|
3
|
+
import { createTreeNodeApi } from './tree-node.impl.js';
|
|
4
|
+
import { forestAndPathAndExpectedArb, newBranchesOrNullArb, nodesArrayAndBranchPathArb, } from './tree-node.arb.js';
|
|
5
|
+
const stringNodeApi = createTreeNodeApi();
|
|
6
|
+
describe('tree node API', () => {
|
|
7
|
+
it('getAtPath returns the node at the generated path', () => {
|
|
8
|
+
fc.assert(fc.property(forestAndPathAndExpectedArb, ({ entries, path, expected }) => {
|
|
9
|
+
expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
10
|
+
}));
|
|
11
|
+
});
|
|
12
|
+
it('getAtPath applies the selector', () => {
|
|
13
|
+
const entries = [
|
|
14
|
+
{
|
|
15
|
+
id: 'root',
|
|
16
|
+
children: [{ id: 'child' }],
|
|
17
|
+
},
|
|
18
|
+
];
|
|
19
|
+
expect(stringNodeApi.getAtPath(entries, ['root', 'child'], (node) => node.id)).toBe('child');
|
|
20
|
+
});
|
|
21
|
+
it('getAtPath returns undefined for an empty path', () => {
|
|
22
|
+
const entries = [{ id: 'root' }];
|
|
23
|
+
expect(stringNodeApi.getAtPath(entries, [], (node) => node)).toBeUndefined();
|
|
24
|
+
});
|
|
25
|
+
it('getAtPath returns undefined through a closed branch', () => {
|
|
26
|
+
const entries = [
|
|
27
|
+
{
|
|
28
|
+
id: 'root',
|
|
29
|
+
children: null,
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
expect(stringNodeApi.getAtPath(entries, ['root', 'child'], (node) => node)).toBeUndefined();
|
|
33
|
+
});
|
|
34
|
+
it('getAtPath returns undefined when the selector returns undefined', () => {
|
|
35
|
+
const entries = [{ id: 'root' }];
|
|
36
|
+
expect(stringNodeApi.getAtPath(entries, ['root'], () => undefined)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
it('getChildById returns the matching child', () => {
|
|
39
|
+
const child = { id: 'child' };
|
|
40
|
+
const root = {
|
|
41
|
+
id: 'root',
|
|
42
|
+
children: [child],
|
|
43
|
+
};
|
|
44
|
+
expect(stringNodeApi.isOpenBranch(root)).toBe(true);
|
|
45
|
+
if (!stringNodeApi.isOpenBranch(root)) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
expect(stringNodeApi.getChildById(root, 'child')).toBe(child);
|
|
49
|
+
expect(stringNodeApi.getChildById(root, 'missing')).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
it('modifyAtPath replaces the generated node', () => {
|
|
52
|
+
fc.assert(fc.property(forestAndPathAndExpectedArb, ({ entries, path, expected }) => {
|
|
53
|
+
const replacement = {
|
|
54
|
+
id: expected.id,
|
|
55
|
+
};
|
|
56
|
+
const updatedForest = stringNodeApi.modifyAtPath(entries, path, () => replacement);
|
|
57
|
+
expect(updatedForest).not.toBeUndefined();
|
|
58
|
+
if (updatedForest === undefined) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
expect(stringNodeApi.getAtPath(updatedForest, path, (node) => node)).toBe(replacement);
|
|
62
|
+
expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
it('modifyAtPath replaces children at a branch path', () => {
|
|
66
|
+
fc.assert(fc.property(nodesArrayAndBranchPathArb, newBranchesOrNullArb, ({ entries, path, expected }, newBranches) => {
|
|
67
|
+
expect(stringNodeApi.isBranch(expected)).toBe(true);
|
|
68
|
+
const updatedForest = stringNodeApi.modifyAtPath(entries, path, (node) => ({
|
|
69
|
+
...node,
|
|
70
|
+
children: newBranches,
|
|
71
|
+
}));
|
|
72
|
+
expect(updatedForest).not.toBeUndefined();
|
|
73
|
+
if (updatedForest === undefined) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const updatedNode = stringNodeApi.getAtPath(updatedForest, path, (node) => node);
|
|
77
|
+
expect(updatedNode).not.toBeUndefined();
|
|
78
|
+
expect(updatedNode).not.toBe(expected);
|
|
79
|
+
if (updatedNode === undefined ||
|
|
80
|
+
!stringNodeApi.isBranch(updatedNode) ||
|
|
81
|
+
(!stringNodeApi.isOpenBranch(updatedNode) && newBranches !== null)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (stringNodeApi.isOpenBranch(updatedNode)) {
|
|
85
|
+
expect([...stringNodeApi.getChildren(updatedNode)]).toStrictEqual(newBranches);
|
|
86
|
+
}
|
|
87
|
+
expect(stringNodeApi.getAtPath(entries, path, (node) => node)).toBe(expected);
|
|
88
|
+
}));
|
|
89
|
+
});
|
|
90
|
+
it('modifyAtPath returns undefined when the modifier aborts', () => {
|
|
91
|
+
const entries = [{ id: 'root' }];
|
|
92
|
+
expect(stringNodeApi.modifyAtPath(entries, ['root'], () => undefined)).toBeUndefined();
|
|
93
|
+
});
|
|
94
|
+
it('modifyAtPath returns undefined for an empty path', () => {
|
|
95
|
+
const entries = [{ id: 'root' }];
|
|
96
|
+
expect(stringNodeApi.modifyAtPath(entries, [], (node) => node)).toBeUndefined();
|
|
97
|
+
});
|
|
98
|
+
it('modifyAtPath returns undefined for an invalid path', () => {
|
|
99
|
+
const entries = [
|
|
100
|
+
{
|
|
101
|
+
id: 'root',
|
|
102
|
+
children: [],
|
|
103
|
+
},
|
|
104
|
+
];
|
|
105
|
+
expect(stringNodeApi.modifyAtPath(entries, ['missing'], (node) => node)).toBeUndefined();
|
|
106
|
+
expect(stringNodeApi.modifyAtPath(entries, ['root', 'missing'], (node) => node)).toBeUndefined();
|
|
107
|
+
});
|
|
108
|
+
it('modifyAtPath cannot traverse a closed branch', () => {
|
|
109
|
+
const entries = [
|
|
110
|
+
{
|
|
111
|
+
id: 'root',
|
|
112
|
+
children: null,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
expect(stringNodeApi.modifyAtPath(entries, ['root', 'child'], (node) => node)).toBeUndefined();
|
|
116
|
+
});
|
|
117
|
+
});
|