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.
Files changed (119) hide show
  1. package/dist/domain/posix-node.d.ts +29 -0
  2. package/dist/domain/posix-node.d.ts.map +1 -0
  3. package/dist/domain/posix-node.js +20 -0
  4. package/dist/domain/unix-path.d.ts +1 -1
  5. package/dist/domain/unix-path.js +6 -9
  6. package/dist/index.d.ts +9 -14
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +8 -13
  9. package/dist/tree-surfer/cursor.d.ts +24 -0
  10. package/dist/tree-surfer/cursor.d.ts.map +1 -0
  11. package/dist/tree-surfer/cursor.js +65 -0
  12. package/dist/tree-surfer/fold-node/fold-node.impl.d.ts +5 -0
  13. package/dist/tree-surfer/fold-node/fold-node.impl.d.ts.map +1 -0
  14. package/dist/tree-surfer/fold-node/fold-node.impl.js +143 -0
  15. package/dist/tree-surfer/fold-node/fold-node.types.d.ts +70 -0
  16. package/dist/tree-surfer/fold-node/fold-node.types.d.ts.map +1 -0
  17. package/dist/tree-surfer/fold-node/fold-node.types.js +1 -0
  18. package/dist/tree-surfer/index.d.ts +9 -0
  19. package/dist/tree-surfer/index.d.ts.map +1 -0
  20. package/dist/tree-surfer/index.js +8 -0
  21. package/dist/tree-surfer/nav-node/nav-node.impl.d.ts +6 -0
  22. package/dist/tree-surfer/nav-node/nav-node.impl.d.ts.map +1 -0
  23. package/dist/tree-surfer/nav-node/nav-node.impl.js +191 -0
  24. package/dist/tree-surfer/nav-node/nav-node.types.d.ts +42 -0
  25. package/dist/tree-surfer/nav-node/nav-node.types.d.ts.map +1 -0
  26. package/dist/tree-surfer/nav-node/nav-node.types.js +3 -0
  27. package/dist/tree-surfer/state.d.ts +19 -0
  28. package/dist/tree-surfer/state.d.ts.map +1 -0
  29. package/dist/tree-surfer/state.js +109 -0
  30. package/dist/tree-surfer/tree-node/tree-node.arb.d.ts +31 -0
  31. package/dist/tree-surfer/tree-node/tree-node.arb.d.ts.map +1 -0
  32. package/dist/tree-surfer/tree-node/tree-node.arb.js +80 -0
  33. package/dist/tree-surfer/tree-node/tree-node.impl.d.ts +13 -0
  34. package/dist/tree-surfer/tree-node/tree-node.impl.d.ts.map +1 -0
  35. package/dist/tree-surfer/tree-node/tree-node.impl.js +87 -0
  36. package/dist/tree-surfer/tree-node/tree-node.test.d.ts +2 -0
  37. package/dist/tree-surfer/tree-node/tree-node.test.d.ts.map +1 -0
  38. package/dist/tree-surfer/tree-node/tree-node.test.js +117 -0
  39. package/dist/tree-surfer/tree-node/tree-node.types.d.ts +184 -0
  40. package/dist/tree-surfer/tree-node/tree-node.types.d.ts.map +1 -0
  41. package/dist/tree-surfer/tree-node/tree-node.types.js +12 -0
  42. package/dist/tree-surfer-ui/application/index.d.ts +3 -0
  43. package/dist/tree-surfer-ui/application/index.d.ts.map +1 -0
  44. package/dist/tree-surfer-ui/application/index.js +2 -0
  45. package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.d.ts +2 -0
  46. package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.d.ts.map +1 -0
  47. package/dist/tree-surfer-ui/application/intent-to-effect-no-state.test.js +192 -0
  48. package/dist/tree-surfer-ui/application/intent-to-effect.d.ts +6 -0
  49. package/dist/tree-surfer-ui/application/intent-to-effect.d.ts.map +1 -0
  50. package/dist/tree-surfer-ui/application/intent-to-effect.js +137 -0
  51. package/dist/tree-surfer-ui/application/user-input-to-intent.d.ts +3 -0
  52. package/dist/tree-surfer-ui/application/user-input-to-intent.d.ts.map +1 -0
  53. package/dist/tree-surfer-ui/application/user-input-to-intent.js +98 -0
  54. package/dist/tree-surfer-ui/application/user-input-to-intent.test.d.ts +2 -0
  55. package/dist/tree-surfer-ui/application/user-input-to-intent.test.d.ts.map +1 -0
  56. package/dist/tree-surfer-ui/application/user-input-to-intent.test.js +118 -0
  57. package/dist/tree-surfer-ui/domain/effect.d.ts +36 -0
  58. package/dist/tree-surfer-ui/domain/effect.d.ts.map +1 -0
  59. package/dist/tree-surfer-ui/domain/index.d.ts +5 -0
  60. package/dist/tree-surfer-ui/domain/index.d.ts.map +1 -0
  61. package/dist/tree-surfer-ui/domain/index.js +4 -0
  62. package/dist/tree-surfer-ui/domain/input-state.d.ts +8 -0
  63. package/dist/tree-surfer-ui/domain/input-state.d.ts.map +1 -0
  64. package/dist/tree-surfer-ui/domain/intent.d.ts +25 -0
  65. package/dist/tree-surfer-ui/domain/intent.d.ts.map +1 -0
  66. package/dist/tree-surfer-ui/domain/user-input.d.ts +19 -0
  67. package/dist/tree-surfer-ui/domain/user-input.d.ts.map +1 -0
  68. package/package.json +3 -2
  69. package/dist/application/intent-to-effect-no-state.test.d.ts +0 -2
  70. package/dist/application/intent-to-effect-no-state.test.d.ts.map +0 -1
  71. package/dist/application/intent-to-effect-no-state.test.js +0 -178
  72. package/dist/application/intent-to-effect.d.ts +0 -8
  73. package/dist/application/intent-to-effect.d.ts.map +0 -1
  74. package/dist/application/intent-to-effect.js +0 -142
  75. package/dist/application/intent-to-effect.test-no-state.d.ts +0 -2
  76. package/dist/application/intent-to-effect.test-no-state.d.ts.map +0 -1
  77. package/dist/application/intent-to-effect.test-no-state.js +0 -178
  78. package/dist/application/user-input-to-intent.d.ts +0 -8
  79. package/dist/application/user-input-to-intent.d.ts.map +0 -1
  80. package/dist/application/user-input-to-intent.js +0 -98
  81. package/dist/application/user-input-to-intent.test.d.ts +0 -2
  82. package/dist/application/user-input-to-intent.test.d.ts.map +0 -1
  83. package/dist/application/user-input-to-intent.test.js +0 -125
  84. package/dist/domain/cursor.d.ts +0 -19
  85. package/dist/domain/cursor.d.ts.map +0 -1
  86. package/dist/domain/cursor.js +0 -46
  87. package/dist/domain/display-row.d.ts +0 -34
  88. package/dist/domain/display-row.d.ts.map +0 -1
  89. package/dist/domain/display-row.js +0 -33
  90. package/dist/domain/effect.d.ts +0 -48
  91. package/dist/domain/effect.d.ts.map +0 -1
  92. package/dist/domain/fold-node.d.ts +0 -25
  93. package/dist/domain/fold-node.d.ts.map +0 -1
  94. package/dist/domain/fold-node.js +0 -93
  95. package/dist/domain/input-state.d.ts +0 -10
  96. package/dist/domain/input-state.d.ts.map +0 -1
  97. package/dist/domain/intent.d.ts +0 -34
  98. package/dist/domain/intent.d.ts.map +0 -1
  99. package/dist/domain/navigation-node.d.ts +0 -58
  100. package/dist/domain/navigation-node.d.ts.map +0 -1
  101. package/dist/domain/navigation-node.js +0 -210
  102. package/dist/domain/state.d.ts +0 -12
  103. package/dist/domain/state.d.ts.map +0 -1
  104. package/dist/domain/state.js +0 -12
  105. package/dist/domain/unix-entry.d.ts +0 -44
  106. package/dist/domain/unix-entry.d.ts.map +0 -1
  107. package/dist/domain/unix-entry.js +0 -108
  108. package/dist/domain/user-input.d.ts +0 -27
  109. package/dist/domain/user-input.d.ts.map +0 -1
  110. package/dist/infrastructure/get-cwd.d.ts +0 -3
  111. package/dist/infrastructure/get-cwd.d.ts.map +0 -1
  112. package/dist/infrastructure/get-cwd.js +0 -15
  113. package/dist/infrastructure/get-dir-lazy-entries.d.ts +0 -6
  114. package/dist/infrastructure/get-dir-lazy-entries.d.ts.map +0 -1
  115. package/dist/infrastructure/get-dir-lazy-entries.js +0 -40
  116. /package/dist/{domain → tree-surfer-ui/domain}/effect.js +0 -0
  117. /package/dist/{domain → tree-surfer-ui/domain}/input-state.js +0 -0
  118. /package/dist/{domain → tree-surfer-ui/domain}/intent.js +0 -0
  119. /package/dist/{domain → tree-surfer-ui/domain}/user-input.js +0 -0
@@ -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
+ });
@@ -0,0 +1,184 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Runtime schema for IDs that can be safely represented as serializable
4
+ * primitive values.
5
+ *
6
+ * Strings are accepted as-is. Numbers must be finite, so `NaN`, `Infinity`,
7
+ * and `-Infinity` are rejected.
8
+ *
9
+ * Applications should derive a more specific schema when those constraints
10
+ * are needed.
11
+ */
12
+ export declare const serializableKeySchema: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
13
+ /**
14
+ * A serializable primitive suitable for use as a stable tree ID.
15
+ *
16
+ * The runtime schema guarantees that numeric values are finite. TypeScript
17
+ * cannot represent the exclusion of `NaN` and infinities from `number`, so
18
+ * values from untrusted sources should be validated with
19
+ * `serializableKeySchema` before being used as IDs.
20
+ *
21
+ * For application-specific IDs, define a narrower branded schema and derive
22
+ * its type with `z.output`.
23
+ *
24
+ * @example
25
+ *
26
+ * ```ts
27
+ * const menuIdSchema = z
28
+ * .string()
29
+ * .min(1)
30
+ * .brand<'MenuId'>();
31
+ *
32
+ * type MenuId = z.output<typeof menuIdSchema>;
33
+ *
34
+ * const menuId = menuIdSchema.parse(input);
35
+ * // menuId: MenuId
36
+ *
37
+ * type MenuNode = TreeNode<MenuId, MenuNode>;
38
+ * ```
39
+ */
40
+ export type SerializableKey = string | number;
41
+ /**
42
+ * A node with no children.
43
+ */
44
+ export type LeafTreeNode<Id extends SerializableKey> = {
45
+ id: Id;
46
+ children?: never;
47
+ };
48
+ /**
49
+ * A branch whose children have not been loaded.
50
+ */
51
+ export type ClosedBranchTreeNode<Id extends SerializableKey> = {
52
+ id: Id;
53
+ children: null;
54
+ };
55
+ /**
56
+ * A branch whose children have been loaded.
57
+ *
58
+ * TODO Later: Add a dictionary by ID for faster lookup.
59
+ * Then children will just be an array for keeping order.
60
+ */
61
+ export type OpenBranchTreeNode<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>> = {
62
+ id: Id;
63
+ children: ChildNode[];
64
+ };
65
+ /**
66
+ * A tree node that has children, either loaded or unloaded.
67
+ */
68
+ export type BranchTreeNode<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>> = OpenBranchTreeNode<Id, ChildNode> | ClosedBranchTreeNode<Id>;
69
+ /**
70
+ * A node in a tree.
71
+ *
72
+ * `ChildNode` is the concrete application-specific node type. It may contain
73
+ * additional properties, provided that it has the shape of either a leaf or a
74
+ * branch.
75
+ */
76
+ export type TreeNode<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>> = LeafTreeNode<Id> | BranchTreeNode<Id, ChildNode>;
77
+ /**
78
+ * Selects a value from a node reached by a path.
79
+ *
80
+ * The selector is called only when the complete path exists and identifies
81
+ * a node. Returning `undefined` from the selector is allowed, but is
82
+ * indistinguishable from an unsuccessful lookup in the return value.
83
+ */
84
+ export type TreeNodeSelector<ChildNode, Result> = (node: ChildNode) => Result;
85
+ /**
86
+ * Produces a replacement for a node reached by a path.
87
+ *
88
+ * Returning `undefined` aborts the modification and causes `modifyAtPath`
89
+ * to return `undefined`.
90
+ */
91
+ export type TreeNodeModifier<ChildNode> = (node: ChildNode) => ChildNode | undefined;
92
+ /**
93
+ * Fundamental operations for inspecting and immutably updating a tree forest.
94
+ *
95
+ * A forest is represented by an array of root-level nodes. A path is an array
96
+ * of IDs beginning at a root node and continuing through its descendants.
97
+ *
98
+ * For example, given tree with IDs:
99
+ *
100
+ * root
101
+ * └── child
102
+ *
103
+ * the path to `child` is `['root', 'child']`.
104
+ *
105
+ * Path traversal and child lookup are performed by this API so that callers
106
+ * do not depend on the internal child-storage representation or need to
107
+ * iterate over children themselves.
108
+ *
109
+ * An empty path does not identify a node and is invalid for both lookup and
110
+ * modification operations.
111
+ */
112
+ export type TreeNodeApi<Id extends SerializableKey, ChildNode extends TreeNode<Id, ChildNode>> = {
113
+ isLeaf(node: ChildNode): node is ChildNode & LeafTreeNode<Id>;
114
+ isBranch(node: ChildNode): node is ChildNode & BranchTreeNode<Id, ChildNode>;
115
+ isClosedBranch(node: BranchTreeNode<Id, ChildNode>): node is ClosedBranchTreeNode<Id>;
116
+ isOpenBranch(node: BranchTreeNode<Id, ChildNode>): node is OpenBranchTreeNode<Id, ChildNode>;
117
+ /**
118
+ * Returns the loaded children of an open branch.
119
+ *
120
+ * The returned iterable preserves the branch's sibling order. The method
121
+ * can only be called after narrowing the branch with `isOpenBranch`.
122
+ *
123
+ * The returned children are the existing child nodes; they are not cloned.
124
+ */
125
+ getChildren(node: OpenBranchTreeNode<Id, ChildNode>): Iterable<ChildNode>;
126
+ /**
127
+ * Returns the child with the supplied ID.
128
+ *
129
+ * Returns `undefined` when the branch has no child with that ID.
130
+ * The returned child is the existing node; it is not cloned.
131
+ */
132
+ getChildById(node: OpenBranchTreeNode<Id, ChildNode>, id: Id): ChildNode | undefined;
133
+ /**
134
+ * Applies `selector` to the node at `path`.
135
+ *
136
+ * The path must:
137
+ *
138
+ * - be non-empty;
139
+ * - begin at one of the supplied forest entries;
140
+ * - contain only IDs that identify existing entries;
141
+ * - not traverse through a leaf; and
142
+ * - not traverse through a closed branch.
143
+ *
144
+ * A branch at the end of the path is valid even when it is closed.
145
+ *
146
+ * The selector is called only after the complete path has been resolved.
147
+ * This method does not clone the tree or the selected node.
148
+ *
149
+ * Returns `undefined` when the path cannot be resolved, or when the
150
+ * selector returns `undefined`.
151
+ */
152
+ getAtPath<Result>(entries: ChildNode[], path: Id[], selector: TreeNodeSelector<ChildNode, Result>): Result | undefined;
153
+ /**
154
+ * Immutably modifies the node at `path`.
155
+ *
156
+ * The modifier is called only after the complete path has been resolved.
157
+ * Its return value replaces the target node. Returning `undefined` aborts
158
+ * the modification.
159
+ *
160
+ * The operation creates new arrays and ancestor nodes along the path.
161
+ * Nodes and arrays unrelated to the path retain their original object
162
+ * identity. The input forest and its nodes (as objects) are never modified.
163
+ *
164
+ * The path must:
165
+ *
166
+ * - be non-empty;
167
+ * - begin at one of the supplied forest entries;
168
+ * - contain only IDs that identify existing entries;
169
+ * - not traverse through a leaf; and
170
+ * - not traverse through a closed branch.
171
+ *
172
+ * A leaf or branch, including a closed branch, may be the final target.
173
+ *
174
+ * Returns `undefined` when:
175
+ *
176
+ * - `path` is empty;
177
+ * - the path does not exist;
178
+ * - an intermediate node is a leaf;
179
+ * - an intermediate branch is closed; or
180
+ * - the modifier returns `undefined`.
181
+ */
182
+ modifyAtPath(entries: ChildNode[], path: Id[], modifier: TreeNodeModifier<ChildNode>): ChildNode[] | undefined;
183
+ };
184
+ //# sourceMappingURL=tree-node.types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tree-node.types.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer/tree-node/tree-node.types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ import { z } from 'zod';
2
+ /**
3
+ * Runtime schema for IDs that can be safely represented as serializable
4
+ * primitive values.
5
+ *
6
+ * Strings are accepted as-is. Numbers must be finite, so `NaN`, `Infinity`,
7
+ * and `-Infinity` are rejected.
8
+ *
9
+ * Applications should derive a more specific schema when those constraints
10
+ * are needed.
11
+ */
12
+ export const serializableKeySchema = z.union([z.string(), z.number().finite()]);
@@ -0,0 +1,3 @@
1
+ export * from './intent-to-effect.js';
2
+ export * from './user-input-to-intent.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer-ui/application/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,2BAA2B,CAAC"}
@@ -0,0 +1,2 @@
1
+ export * from './intent-to-effect.js';
2
+ export * from './user-input-to-intent.js';
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=intent-to-effect-no-state.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent-to-effect-no-state.test.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer-ui/application/intent-to-effect-no-state.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,192 @@
1
+ export {};
2
+ // import { describe, expect, it } from 'vitest';
3
+ //
4
+ // import type { PosixState } from '../../domain/posix-node.js';
5
+ // import { intentToEffect } from './intent-to-effect.js';
6
+ //
7
+ // const state = {} as PosixState;
8
+ //
9
+ // describe('intentToEffectNoState', () => {
10
+ // describe('normal mode buffer', () => {
11
+ // it('stores z as a possible command prefix', () => {
12
+ // expect(
13
+ // intentToEffect(
14
+ // {
15
+ // intentType: 'setNormalBuffer',
16
+ // normalBuffer: 'z',
17
+ // },
18
+ // state,
19
+ // ),
20
+ // ).toEqual({
21
+ // effectType: 'setInputState',
22
+ // inputState: {
23
+ // inputMode: 'normal',
24
+ // normalBuffer: 'z',
25
+ // },
26
+ // });
27
+ // });
28
+ //
29
+ // it.each([
30
+ // ['za', 'toggleFold'],
31
+ // ['zc', 'fold'],
32
+ // ['zo', 'unfold'],
33
+ // ] as const)(
34
+ // 'converts %s into a %s effect action',
35
+ // (normalBuffer, effectActionType) => {
36
+ // expect(
37
+ // intentToEffect(
38
+ // {
39
+ // intentType: 'setNormalBuffer',
40
+ // normalBuffer,
41
+ // },
42
+ // state,
43
+ // ),
44
+ // ).toEqual({
45
+ // effectType: 'dispatchEffectAction',
46
+ // action: {
47
+ // effectActionType,
48
+ // },
49
+ // });
50
+ // },
51
+ // );
52
+ //
53
+ // it('clears the normal buffer when the input is not a known command', () => {
54
+ // expect(
55
+ // intentToEffect(
56
+ // {
57
+ // intentType: 'setNormalBuffer',
58
+ // normalBuffer: 'x',
59
+ // },
60
+ // state,
61
+ // ),
62
+ // ).toEqual({
63
+ // effectType: 'setInputState',
64
+ // inputState: {
65
+ // inputMode: 'normal',
66
+ // normalBuffer: '',
67
+ // },
68
+ // });
69
+ // });
70
+ // });
71
+ //
72
+ // describe('normal mode navigation', () => {
73
+ // it('moves to the next entry', () => {
74
+ // expect(
75
+ // intentToEffect(
76
+ // {
77
+ // intentType: 'normalDown',
78
+ // },
79
+ // state,
80
+ // ),
81
+ // ).toEqual({
82
+ // effectType: 'dispatchEffectAction',
83
+ // action: {
84
+ // effectActionType: 'nextEntry',
85
+ // },
86
+ // });
87
+ // });
88
+ //
89
+ // it('moves to the previous entry', () => {
90
+ // expect(
91
+ // intentToEffect(
92
+ // {
93
+ // intentType: 'normalUp',
94
+ // },
95
+ // state,
96
+ // ),
97
+ // ).toEqual({
98
+ // effectType: 'dispatchEffectAction',
99
+ // action: {
100
+ // effectActionType: 'prevEntry',
101
+ // },
102
+ // });
103
+ // });
104
+ // });
105
+ //
106
+ // describe('command-line mode', () => {
107
+ // it('enters command-line mode', () => {
108
+ // expect(
109
+ // intentToEffect(
110
+ // {
111
+ // intentType: 'enterCommandLineMode',
112
+ // },
113
+ // state,
114
+ // ),
115
+ // ).toEqual({
116
+ // effectType: 'setInputState',
117
+ // inputState: {
118
+ // inputMode: 'command',
119
+ // commandLine: ':',
120
+ // },
121
+ // });
122
+ // });
123
+ //
124
+ // it('updates the command line', () => {
125
+ // expect(
126
+ // intentToEffect(
127
+ // {
128
+ // intentType: 'setCommandLine',
129
+ // commandLine: ':w',
130
+ // },
131
+ // state,
132
+ // ),
133
+ // ).toEqual({
134
+ // effectType: 'setInputState',
135
+ // inputState: {
136
+ // inputMode: 'command',
137
+ // commandLine: ':w',
138
+ // },
139
+ // });
140
+ // });
141
+ //
142
+ // it('exits command-line mode', () => {
143
+ // expect(
144
+ // intentToEffect(
145
+ // {
146
+ // intentType: 'exitCommandLineMode',
147
+ // },
148
+ // state,
149
+ // ),
150
+ // ).toEqual({
151
+ // effectType: 'setInputState',
152
+ // inputState: {
153
+ // inputMode: 'normal',
154
+ // normalBuffer: '',
155
+ // },
156
+ // });
157
+ // });
158
+ //
159
+ // it('returns to normal mode after executing a gibberish command line', () => {
160
+ // expect(
161
+ // intentToEffect(
162
+ // {
163
+ // intentType: 'executeCommandLine',
164
+ // commandLine: ':sijbas',
165
+ // },
166
+ // state,
167
+ // ),
168
+ // ).toEqual({
169
+ // effectType: 'setInputState',
170
+ // inputState: {
171
+ // inputMode: 'normal',
172
+ // normalBuffer: '',
173
+ // },
174
+ // });
175
+ // });
176
+ //
177
+ // it('quits when the :q command is executed', () => {
178
+ // expect(
179
+ // intentToEffect(
180
+ // {
181
+ // intentType: 'executeCommandLine',
182
+ // commandLine: ':q',
183
+ // },
184
+ // state,
185
+ // ),
186
+ // ).toEqual({
187
+ // effectType: 'quit',
188
+ // exitMessage: '',
189
+ // });
190
+ // });
191
+ // });
192
+ // });
@@ -0,0 +1,6 @@
1
+ import { SerializableKey, TreeNode, TreeNodeApi } from '../../tree-surfer/tree-node/tree-node.types.js';
2
+ import { Effect, Intent } from '../domain/index.js';
3
+ import { CursorApi, State, StateApi } from '../../tree-surfer/index.js';
4
+ export type IntentToEffect<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>> = (intent: Intent, state: State<Id, BufferNode>) => Effect<Id, BufferNode> | undefined;
5
+ export declare function createIntentToEffect<Id extends SerializableKey, BufferNode extends TreeNode<Id, BufferNode>>(stateApi: StateApi<Id, BufferNode>, cursorApi: CursorApi<Id>, treeNodeApi: TreeNodeApi<Id, BufferNode>): IntentToEffect<Id, BufferNode>;
6
+ //# sourceMappingURL=intent-to-effect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"intent-to-effect.d.ts","sourceRoot":"","sources":["../../../src/tree-surfer-ui/application/intent-to-effect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,gDAAgD,CAAC;AACxG,OAAO,EAAE,MAAM,EAAgB,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAClE,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,4BAA4B,CAAC;AAGxE,MAAM,MAAM,cAAc,CACxB,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,IACzC,CACF,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,KAAK,CAAC,EAAE,EAAE,UAAU,CAAC,KACzB,MAAM,CAAC,EAAE,EAAE,UAAU,CAAC,GAAG,SAAS,CAAC;AAGxC,wBAAgB,oBAAoB,CAClC,EAAE,SAAS,eAAe,EAC1B,UAAU,SAAS,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAE3C,QAAQ,EAAE,QAAQ,CAAC,EAAE,EAAE,UAAU,CAAC,EAClC,SAAS,EAAE,SAAS,CAAC,EAAE,CAAC,EACxB,WAAW,EAAE,WAAW,CAAC,EAAE,EAAE,UAAU,CAAC,GACvC,cAAc,CAAC,EAAE,EAAE,UAAU,CAAC,CAyEhC"}
@@ -0,0 +1,137 @@
1
+ // Factory method, since Apis are needed to derive intent
2
+ export function createIntentToEffect(stateApi, cursorApi, treeNodeApi) {
3
+ return (intent, state) => {
4
+ switch (intent.intentType) {
5
+ case 'setNormalBuffer':
6
+ return normalBufferToEffectResult(intent.normalBuffer);
7
+ case 'normalRight':
8
+ return normalInteractRightToEffect(state, stateApi, cursorApi, treeNodeApi);
9
+ case 'normalLeft':
10
+ return normalInteractLeftToEffect(state);
11
+ case 'normalDown':
12
+ return dispatchAction({
13
+ effectActionType: 'nextEntry',
14
+ });
15
+ case 'normalUp':
16
+ return dispatchAction({
17
+ effectActionType: 'prevEntry',
18
+ });
19
+ case 'enterCommandLineMode':
20
+ return {
21
+ effectType: 'setInputState',
22
+ inputState: {
23
+ inputMode: 'command',
24
+ commandLine: ':',
25
+ },
26
+ };
27
+ case 'setCommandLine':
28
+ return {
29
+ effectType: 'setInputState',
30
+ inputState: {
31
+ inputMode: 'command',
32
+ commandLine: intent.commandLine,
33
+ },
34
+ };
35
+ case 'exitCommandLineMode':
36
+ return {
37
+ effectType: 'setInputState',
38
+ inputState: {
39
+ inputMode: 'normal',
40
+ normalBuffer: '',
41
+ },
42
+ };
43
+ case 'executeCommandLine':
44
+ if (intent.commandLine === ':q') {
45
+ return {
46
+ effectType: 'quit',
47
+ exitMessage: '',
48
+ };
49
+ }
50
+ return {
51
+ effectType: 'setInputState',
52
+ inputState: {
53
+ inputMode: 'normal',
54
+ normalBuffer: '',
55
+ },
56
+ };
57
+ }
58
+ };
59
+ }
60
+ function dispatchAction(action) {
61
+ return {
62
+ effectType: 'dispatchEffectAction',
63
+ action,
64
+ };
65
+ }
66
+ // Sets the buffer. Three possibilities:
67
+ // - New command buffer is recognized as a motion, this dispatches the
68
+ // "effect action". Consumer will know to reset the input state.
69
+ // - Prefix of a possible motion, updates normal buffer.
70
+ // - Not a prefix, clears the buffer.
71
+ function normalBufferToEffectResult(updatedNormalBuffer) {
72
+ switch (updatedNormalBuffer) {
73
+ case 'z':
74
+ return {
75
+ effectType: 'setInputState',
76
+ inputState: {
77
+ inputMode: 'normal',
78
+ normalBuffer: updatedNormalBuffer,
79
+ },
80
+ };
81
+ case 'za':
82
+ return dispatchAction({
83
+ effectActionType: 'toggleFold',
84
+ });
85
+ case 'zc':
86
+ return dispatchAction({
87
+ effectActionType: 'fold',
88
+ });
89
+ case 'zo':
90
+ return dispatchAction({
91
+ effectActionType: 'unfold',
92
+ });
93
+ default:
94
+ return {
95
+ effectType: 'setInputState',
96
+ inputState: {
97
+ inputMode: 'normal',
98
+ normalBuffer: '',
99
+ },
100
+ };
101
+ }
102
+ }
103
+ function normalInteractRightToEffect(state, stateApi, cursorApi, treeNodeApi) {
104
+ const currentEntry = stateApi.getNodeAtCursor(state);
105
+ if (currentEntry === undefined) {
106
+ return undefined;
107
+ }
108
+ const path = cursorApi.getPath(state.cursor);
109
+ if (path === undefined) {
110
+ return undefined;
111
+ }
112
+ if (treeNodeApi.isBranch(currentEntry)) {
113
+ if (currentEntry.children === null) {
114
+ return {
115
+ effectType: 'loadBranchEntries',
116
+ path,
117
+ };
118
+ }
119
+ return dispatchAction({
120
+ effectActionType: 'setBranchEntries',
121
+ path,
122
+ entries: null,
123
+ });
124
+ }
125
+ return {
126
+ effectType: 'emitPath',
127
+ path,
128
+ };
129
+ }
130
+ function normalInteractLeftToEffect(state) {
131
+ if (state.cursor.parentPath.length === 0) {
132
+ return undefined;
133
+ }
134
+ return dispatchAction({
135
+ effectActionType: 'navigateToParent',
136
+ });
137
+ }