dirvi-lib 0.4.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/domain/unix-path.d.ts +1 -1
- package/dist/domain/unix-path.js +6 -9
- package/dist/index.d.ts +9 -14
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -13
- 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/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/intent.d.ts +25 -0
- package/dist/tree-surfer-ui/domain/intent.d.ts.map +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/package.json +3 -2
- package/dist/application/intent-to-effect-no-state.test.d.ts +0 -2
- package/dist/application/intent-to-effect-no-state.test.d.ts.map +0 -1
- package/dist/application/intent-to-effect-no-state.test.js +0 -178
- package/dist/application/intent-to-effect.d.ts +0 -8
- package/dist/application/intent-to-effect.d.ts.map +0 -1
- package/dist/application/intent-to-effect.js +0 -142
- package/dist/application/intent-to-effect.test-no-state.d.ts +0 -2
- package/dist/application/intent-to-effect.test-no-state.d.ts.map +0 -1
- package/dist/application/intent-to-effect.test-no-state.js +0 -178
- package/dist/application/user-input-to-intent.d.ts +0 -8
- package/dist/application/user-input-to-intent.d.ts.map +0 -1
- package/dist/application/user-input-to-intent.js +0 -98
- package/dist/application/user-input-to-intent.test.d.ts +0 -2
- package/dist/application/user-input-to-intent.test.d.ts.map +0 -1
- package/dist/application/user-input-to-intent.test.js +0 -125
- package/dist/domain/cursor.d.ts +0 -19
- package/dist/domain/cursor.d.ts.map +0 -1
- package/dist/domain/cursor.js +0 -46
- package/dist/domain/display-row.d.ts +0 -34
- package/dist/domain/display-row.d.ts.map +0 -1
- package/dist/domain/display-row.js +0 -33
- package/dist/domain/effect.d.ts +0 -48
- package/dist/domain/effect.d.ts.map +0 -1
- package/dist/domain/fold-node.d.ts +0 -25
- package/dist/domain/fold-node.d.ts.map +0 -1
- package/dist/domain/fold-node.js +0 -93
- package/dist/domain/input-state.d.ts +0 -10
- package/dist/domain/input-state.d.ts.map +0 -1
- package/dist/domain/intent.d.ts +0 -34
- package/dist/domain/intent.d.ts.map +0 -1
- package/dist/domain/navigation-node.d.ts +0 -58
- package/dist/domain/navigation-node.d.ts.map +0 -1
- package/dist/domain/navigation-node.js +0 -210
- 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 -44
- package/dist/domain/unix-entry.d.ts.map +0 -1
- package/dist/domain/unix-entry.js +0 -108
- package/dist/domain/user-input.d.ts +0 -27
- package/dist/domain/user-input.d.ts.map +0 -1
- 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 -15
- package/dist/infrastructure/get-dir-lazy-entries.d.ts +0 -6
- package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +0 -1
- package/dist/infrastructure/get-dir-lazy-entries.js +0 -40
- /package/dist/{domain → tree-surfer-ui/domain}/effect.js +0 -0
- /package/dist/{domain → tree-surfer-ui/domain}/input-state.js +0 -0
- /package/dist/{domain → tree-surfer-ui/domain}/intent.js +0 -0
- /package/dist/{domain → tree-surfer-ui/domain}/user-input.js +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { CursorKind } from '../cursor.js';
|
|
2
|
+
import { isNavBranch } from './nav-node.types.js';
|
|
3
|
+
export function createNavNodeApi(treeNodeApi, foldNodeApi, cursorApi) {
|
|
4
|
+
const navNodeApi = {
|
|
5
|
+
// Return a nav node from the BufferNode tree and FoldNode tree.
|
|
6
|
+
from(entries, foldNode) {
|
|
7
|
+
const visibleEntriesSoFar = [];
|
|
8
|
+
const foldedEntriesSoFar = [];
|
|
9
|
+
for (const entry of entries) {
|
|
10
|
+
// Find the node
|
|
11
|
+
let navigationEntry;
|
|
12
|
+
if (treeNodeApi.isBranch(entry)) {
|
|
13
|
+
// If fold node does not have children (recursively no folds),
|
|
14
|
+
// then just use empty.
|
|
15
|
+
const childFoldNode = foldNodeApi.getChildById(foldNode, entry.id) ??
|
|
16
|
+
createEmptyFoldNode();
|
|
17
|
+
navigationEntry = {
|
|
18
|
+
...entry,
|
|
19
|
+
children: entry.children === null
|
|
20
|
+
? null
|
|
21
|
+
: navNodeApi.from(entry.children, childFoldNode),
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
else if (treeNodeApi.isLeaf(entry)) {
|
|
25
|
+
navigationEntry = entry;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
// This should be unreachable if BufferNode correctly extends TreeNode.
|
|
29
|
+
throw new Error('Unsupported tree node');
|
|
30
|
+
}
|
|
31
|
+
if (foldNode.folds.has(entry.id)) {
|
|
32
|
+
foldedEntriesSoFar.push(navigationEntry);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
visibleEntriesSoFar.push(navigationEntry);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
entries: visibleEntriesSoFar,
|
|
40
|
+
foldedEntries: foldedEntriesSoFar,
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
getNodeAtPath(navigation, path) {
|
|
44
|
+
const [currentId, ...remainingPath] = path;
|
|
45
|
+
// An empty path identifies the current node.
|
|
46
|
+
if (currentId === undefined) {
|
|
47
|
+
return navigation;
|
|
48
|
+
}
|
|
49
|
+
const entry = [...navigation.entries, ...navigation.foldedEntries].find((candidate) => candidate.id === currentId);
|
|
50
|
+
if (entry === undefined) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (!isNavBranch(entry)) {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
// The directory has not been loaded/opened.
|
|
57
|
+
if (entry.children === null) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
return navNodeApi.getNodeAtPath(entry.children, remainingPath);
|
|
61
|
+
},
|
|
62
|
+
getEntryAtPath(navigation, path) {
|
|
63
|
+
const [currentId, ...remainingPath] = path;
|
|
64
|
+
if (currentId === undefined) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const entry = [...navigation.entries, ...navigation.foldedEntries].find((candidate) => candidate.id === currentId);
|
|
68
|
+
if (entry === undefined) {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
if (remainingPath.length === 0) {
|
|
72
|
+
return entry;
|
|
73
|
+
}
|
|
74
|
+
if (!isNavBranch(entry) || entry.children === null) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
return navNodeApi.getEntryAtPath(entry.children, remainingPath);
|
|
78
|
+
},
|
|
79
|
+
nextCursor(rootNode, cursor) {
|
|
80
|
+
const cursors = cursorsInNode(rootNode, []);
|
|
81
|
+
const currentIndex = cursors.findIndex((candidate) => cursorApi.equal(candidate, cursor));
|
|
82
|
+
if (currentIndex === -1 || currentIndex === cursors.length - 1) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return cursors[currentIndex + 1];
|
|
86
|
+
},
|
|
87
|
+
previousCursor(rootNode, cursor) {
|
|
88
|
+
const cursors = cursorsInNode(rootNode, []);
|
|
89
|
+
const currentIndex = cursors.findIndex((candidate) => cursorApi.equal(candidate, cursor));
|
|
90
|
+
if (currentIndex <= 0) {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
return cursors[currentIndex - 1];
|
|
94
|
+
},
|
|
95
|
+
cursorAfterFold(rootNode, cursor) {
|
|
96
|
+
if (cursorApi.isFold(cursor)) {
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
const cursors = cursorsInNode(rootNode, []);
|
|
100
|
+
const currentIndex = cursors.findIndex((candidate) => cursorApi.equal(candidate, cursor));
|
|
101
|
+
if (currentIndex === -1) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const foldedPath = [...cursor.parentPath, cursor.entryId];
|
|
105
|
+
for (let index = currentIndex + 1; index < cursors.length; index += 1) {
|
|
106
|
+
const candidate = cursors[index];
|
|
107
|
+
if (candidate === undefined) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (cursorApi.cursorBelongsToSubtree(candidate, foldedPath)) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
return candidate;
|
|
114
|
+
}
|
|
115
|
+
// The fold row will be created at the end of this directory.
|
|
116
|
+
return {
|
|
117
|
+
kind: CursorKind.Fold,
|
|
118
|
+
parentPath: cursor.parentPath,
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
parentCursor(navigation, cursor) {
|
|
122
|
+
const parentPath = cursor.parentPath;
|
|
123
|
+
// The cursor is already in the root directory.
|
|
124
|
+
if (parentPath.length === 0) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const entryId = parentPath.at(-1);
|
|
128
|
+
if (entryId === undefined) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const containingPath = parentPath.slice(0, -1);
|
|
132
|
+
const entry = navNodeApi.getEntryAtPath(navigation, [...parentPath]);
|
|
133
|
+
// The parent path must identify a navigable branch.
|
|
134
|
+
if (entry === undefined || !isNavBranch(entry)) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
kind: CursorKind.Entry,
|
|
139
|
+
parentPath: containingPath,
|
|
140
|
+
entryId: entryId,
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
cursors(navigation, parentPath = []) {
|
|
144
|
+
return cursorsInNode(navigation, parentPath);
|
|
145
|
+
},
|
|
146
|
+
visibleLeavesPaths(navigation, parentPath = []) {
|
|
147
|
+
const paths = [];
|
|
148
|
+
for (const entry of navigation.entries) {
|
|
149
|
+
const entryPath = [...parentPath, entry.id];
|
|
150
|
+
if (isNavBranch(entry)) {
|
|
151
|
+
// A null children value means that the directory has not been
|
|
152
|
+
// loaded/opened, so there are no visible descendant files.
|
|
153
|
+
if (entry.children !== null) {
|
|
154
|
+
paths.push(...navNodeApi.visibleLeavesPaths(entry.children, entryPath));
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
paths.push(entryPath);
|
|
159
|
+
}
|
|
160
|
+
return paths;
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
return navNodeApi;
|
|
164
|
+
}
|
|
165
|
+
function createEmptyFoldNode() {
|
|
166
|
+
return {
|
|
167
|
+
children: [],
|
|
168
|
+
folds: new Set(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function cursorsInNode(node, parentPath) {
|
|
172
|
+
const cursors = [];
|
|
173
|
+
for (const entry of node.entries) {
|
|
174
|
+
cursors.push({
|
|
175
|
+
kind: CursorKind.Entry,
|
|
176
|
+
parentPath,
|
|
177
|
+
entryId: entry.id,
|
|
178
|
+
});
|
|
179
|
+
if (!isNavBranch(entry) || entry.children === null) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
cursors.push(...cursorsInNode(entry.children, [...parentPath, entry.id]));
|
|
183
|
+
}
|
|
184
|
+
if (node.foldedEntries.length > 0) {
|
|
185
|
+
cursors.push({
|
|
186
|
+
kind: CursorKind.Fold,
|
|
187
|
+
parentPath,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
return cursors;
|
|
191
|
+
}
|
|
@@ -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":""}
|