cursedbelt-core 2.4.0 → 2.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.
|
@@ -35,6 +35,21 @@ export interface FileTreeNode {
|
|
|
35
35
|
/** Stable across re-derivation — it is the React key, the selection value and the drag id. */
|
|
36
36
|
id: string;
|
|
37
37
|
name: string;
|
|
38
|
+
/**
|
|
39
|
+
* Drawn right after the name, a step quieter — a file's extension, `.md`.
|
|
40
|
+
*
|
|
41
|
+
* For a consumer whose extension is DERIVED rather than part of the stored name
|
|
42
|
+
* (`apps/desk` works a note's suffix out from its format): *"show the extensions in the
|
|
43
|
+
* file tree like vscode does. I need to be able to tell what type each file is"*. Quieter
|
|
44
|
+
* because the name is what you scan FOR and the suffix is what you scan BY.
|
|
45
|
+
*
|
|
46
|
+
* 🔴 It is part of the name everywhere a person types one: {@link matchTypeahead} and
|
|
47
|
+
* {@link filterFileTree} match `name + suffix`, because what is drawn and what can be
|
|
48
|
+
* typed are the same string or they are a bug (*"I typed .json but it said 'Nothing is
|
|
49
|
+
* called that'"*). A rename opens on `name + suffix` with only the name selected — VS
|
|
50
|
+
* Code's field — and hands back whatever was typed, suffix and all.
|
|
51
|
+
*/
|
|
52
|
+
suffix?: string;
|
|
38
53
|
kind: FileTreeNodeKind;
|
|
39
54
|
/** Folders only. A folder with `[]` is empty; a file's is ignored. */
|
|
40
55
|
children?: readonly FileTreeNode[];
|
|
@@ -300,6 +315,8 @@ export declare function accessMarkCopy(access: FileTreeAccess | undefined, copy?
|
|
|
300
315
|
* Typeahead is what makes a long tree usable without a search box — `bud` jumps to
|
|
301
316
|
* `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
|
|
302
317
|
*/
|
|
318
|
+
/** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
|
|
319
|
+
export declare function drawnName(node: FileTreeNode): string;
|
|
303
320
|
export declare function matchTypeahead(rows: readonly FileTreeRow[], query: string, fromIndex: number): number;
|
|
304
321
|
/**
|
|
305
322
|
* The tree narrowed to what matches, with the folders on the way to a match kept.
|
|
@@ -308,6 +308,10 @@ export function accessMarkCopy(access, copy) {
|
|
|
308
308
|
* Typeahead is what makes a long tree usable without a search box — `bud` jumps to
|
|
309
309
|
* `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
|
|
310
310
|
*/
|
|
311
|
+
/** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
|
|
312
|
+
export function drawnName(node) {
|
|
313
|
+
return node.suffix ? `${node.name}${node.suffix}` : node.name;
|
|
314
|
+
}
|
|
311
315
|
export function matchTypeahead(rows, query, fromIndex) {
|
|
312
316
|
if (!query)
|
|
313
317
|
return -1;
|
|
@@ -315,7 +319,7 @@ export function matchTypeahead(rows, query, fromIndex) {
|
|
|
315
319
|
for (let step = 1; step <= rows.length; step += 1) {
|
|
316
320
|
const index = (fromIndex + step + rows.length) % rows.length;
|
|
317
321
|
const row = rows[index];
|
|
318
|
-
if (row
|
|
322
|
+
if (row && drawnName(row.node).toLowerCase().startsWith(needle))
|
|
319
323
|
return index;
|
|
320
324
|
}
|
|
321
325
|
return -1;
|
|
@@ -332,7 +336,7 @@ export function filterFileTree(nodes, query) {
|
|
|
332
336
|
if (!needle)
|
|
333
337
|
return nodes;
|
|
334
338
|
const keep = (node) => {
|
|
335
|
-
const hit = node.
|
|
339
|
+
const hit = drawnName(node).toLowerCase().includes(needle);
|
|
336
340
|
if (hit)
|
|
337
341
|
return node;
|
|
338
342
|
const kids = childrenOf(node)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cursedbelt-core",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.5.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Framework-agnostic core of the cursedbelt split — isomorphic domain logic. cwip below it, cursedbelt-server and cursedbelt (react) above it.",
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
resolveDrop,
|
|
14
14
|
resolveKeyboardMove,
|
|
15
15
|
describeDrop,
|
|
16
|
+
drawnName,
|
|
16
17
|
} from './fileTreeModel.js';
|
|
17
18
|
|
|
18
19
|
/**
|
|
@@ -526,3 +527,25 @@ describe('accessMarkCopy', () => {
|
|
|
526
527
|
expect(accessMarkCopy(undefined)).toBe('');
|
|
527
528
|
});
|
|
528
529
|
});
|
|
530
|
+
|
|
531
|
+
/** `suffix` — a derived extension, drawn and matched as part of the name. */
|
|
532
|
+
describe('a suffix is part of the name a person types', () => {
|
|
533
|
+
const notes: FileTreeNode = { id: 'n', name: 'notes', suffix: '.md', kind: 'file' };
|
|
534
|
+
const nodes: FileTreeNode[] = [folder('docs', 'Docs', [notes]), { id: 'b', name: 'budget', kind: 'file' }];
|
|
535
|
+
|
|
536
|
+
test('drawnName carries it, and a node without one is its name', () => {
|
|
537
|
+
expect(drawnName(notes)).toBe('notes.md');
|
|
538
|
+
expect(drawnName({ id: 'b', name: 'budget', kind: 'file' })).toBe('budget');
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
test('typeahead matches through the dot', () => {
|
|
542
|
+
const rows = flattenFileTree(nodes, []);
|
|
543
|
+
expect(matchTypeahead(rows, 'notes.m', 0)).toBe(rows.findIndex((row) => row.node.id === 'n'));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('a filter for the extension finds the file', () => {
|
|
547
|
+
const kept = filterFileTree(nodes, '.md');
|
|
548
|
+
expect(kept.map((node) => node.id)).toEqual(['docs']);
|
|
549
|
+
expect((kept[0]?.children ?? []).map((node) => node.id)).toEqual(['n']);
|
|
550
|
+
});
|
|
551
|
+
});
|
|
@@ -38,6 +38,21 @@ export interface FileTreeNode {
|
|
|
38
38
|
/** Stable across re-derivation — it is the React key, the selection value and the drag id. */
|
|
39
39
|
id: string;
|
|
40
40
|
name: string;
|
|
41
|
+
/**
|
|
42
|
+
* Drawn right after the name, a step quieter — a file's extension, `.md`.
|
|
43
|
+
*
|
|
44
|
+
* For a consumer whose extension is DERIVED rather than part of the stored name
|
|
45
|
+
* (`apps/desk` works a note's suffix out from its format): *"show the extensions in the
|
|
46
|
+
* file tree like vscode does. I need to be able to tell what type each file is"*. Quieter
|
|
47
|
+
* because the name is what you scan FOR and the suffix is what you scan BY.
|
|
48
|
+
*
|
|
49
|
+
* 🔴 It is part of the name everywhere a person types one: {@link matchTypeahead} and
|
|
50
|
+
* {@link filterFileTree} match `name + suffix`, because what is drawn and what can be
|
|
51
|
+
* typed are the same string or they are a bug (*"I typed .json but it said 'Nothing is
|
|
52
|
+
* called that'"*). A rename opens on `name + suffix` with only the name selected — VS
|
|
53
|
+
* Code's field — and hands back whatever was typed, suffix and all.
|
|
54
|
+
*/
|
|
55
|
+
suffix?: string;
|
|
41
56
|
kind: FileTreeNodeKind;
|
|
42
57
|
/** Folders only. A folder with `[]` is empty; a file's is ignored. */
|
|
43
58
|
children?: readonly FileTreeNode[];
|
|
@@ -530,6 +545,11 @@ export function accessMarkCopy(
|
|
|
530
545
|
* Typeahead is what makes a long tree usable without a search box — `bud` jumps to
|
|
531
546
|
* `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
|
|
532
547
|
*/
|
|
548
|
+
/** The name as the row draws it — with its {@link FileTreeNode.suffix}. */
|
|
549
|
+
export function drawnName(node: FileTreeNode): string {
|
|
550
|
+
return node.suffix ? `${node.name}${node.suffix}` : node.name;
|
|
551
|
+
}
|
|
552
|
+
|
|
533
553
|
export function matchTypeahead(
|
|
534
554
|
rows: readonly FileTreeRow[],
|
|
535
555
|
query: string,
|
|
@@ -540,7 +560,7 @@ export function matchTypeahead(
|
|
|
540
560
|
for (let step = 1; step <= rows.length; step += 1) {
|
|
541
561
|
const index = (fromIndex + step + rows.length) % rows.length;
|
|
542
562
|
const row = rows[index];
|
|
543
|
-
if (row
|
|
563
|
+
if (row && drawnName(row.node).toLowerCase().startsWith(needle)) return index;
|
|
544
564
|
}
|
|
545
565
|
return -1;
|
|
546
566
|
}
|
|
@@ -559,7 +579,7 @@ export function filterFileTree(
|
|
|
559
579
|
const needle = query.trim().toLowerCase();
|
|
560
580
|
if (!needle) return nodes;
|
|
561
581
|
const keep = (node: FileTreeNode): FileTreeNode | null => {
|
|
562
|
-
const hit = node.
|
|
582
|
+
const hit = drawnName(node).toLowerCase().includes(needle);
|
|
563
583
|
if (hit) return node;
|
|
564
584
|
const kids = childrenOf(node)
|
|
565
585
|
.map(keep)
|