@tuidom/elements 0.6.0 → 0.6.1

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.
@@ -14,6 +14,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
14
14
  private selectedKeys;
15
15
  private cutKeys;
16
16
  private maxRowWidth;
17
+ /**
18
+ * Ширина метки в колонках по тексту — между пересборками плоского списка.
19
+ * `new DisplayLine` на каждую строку каждой пересборки был основной ценой
20
+ * раскрытия на больших деревьях. Поколенческий: пересборка переносит в
21
+ * новый кэш только встретившиеся метки, поэтому он не растёт без предела.
22
+ */
23
+ private labelWidths;
17
24
  /**
18
25
  * Отступ контента строк от левого края элемента. В отличие от внешнего
19
26
  * паддинг-контейнера, входит в саму строку, поэтому фон курсора/выделения
@@ -35,6 +42,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
35
42
  toggleExpand(element: T): Promise<void>;
36
43
  /** Раскрывает узел (идемпотентно, без прокрутки/выделения) и перестраивает список. */
37
44
  expand(element: T): Promise<void>;
45
+ /**
46
+ * Раскрывает несколько узлов разом (идемпотентно, без прокрутки/выделения)
47
+ * и перестраивает список один раз. Для «раскрыть все N» вместо N вызовов
48
+ * {@link expand}: каждый из них пересобирает весь плоский список, и на
49
+ * тысячах узлов это квадратичная цена.
50
+ */
51
+ expandElements(elements: readonly T[]): Promise<void>;
38
52
  /** Раскрывает узел (загружая детей в кеш), если он ещё не раскрыт. No-op для уже раскрытого. */
39
53
  private expandElement;
40
54
  /**
@@ -64,6 +78,13 @@ export declare class TreeViewElement<T> extends ScrollableElement {
64
78
  protected performDefaultAction(event: TUIEventBase): void;
65
79
  protected renderViewport(context: RenderContext, viewport: ScrollViewportInfo): void;
66
80
  private resolveRowColors;
81
+ /**
82
+ * Перечитывает корень и раскрытые поддеревья обходом от корня: раскрытый
83
+ * узел, до которого обход не дошёл (свёрнут предок или узла больше нет),
84
+ * в плоском списке всё равно не виден. Обход вместо поиска каждого
85
+ * раскрытого ключа по плоскому списку — иначе `refresh()` квадратичен по
86
+ * числу раскрытых узлов.
87
+ */
67
88
  private loadRootChildren;
68
89
  private reloadExpandedChildren;
69
90
  private invalidateSubtreeCache;
@@ -71,7 +92,6 @@ export declare class TreeViewElement<T> extends ScrollableElement {
71
92
  private appendChildren;
72
93
  /** Колонка (в координатах контента), с которой начинается строка узла данной глубины. */
73
94
  private rowIndentX;
74
- private calculateRowWidth;
75
95
  private formatRow;
76
96
  private getSelectedKey;
77
97
  private restoreSelection;
@@ -87,7 +107,6 @@ export declare class TreeViewElement<T> extends ScrollableElement {
87
107
  private toggleSelectionAt;
88
108
  private applyCursor;
89
109
  private ensureVisible;
90
- private findElementByKey;
91
110
  private handleKeypress;
92
111
  /**
93
112
  * Быстрый поиск по набору: печатаешь символы — курсор перепрыгивает к первому узлу,
@@ -19,6 +19,13 @@ export class TreeViewElement extends ScrollableElement {
19
19
  selectedKeys = new Set();
20
20
  cutKeys = new Set();
21
21
  maxRowWidth = 0;
22
+ /**
23
+ * Ширина метки в колонках по тексту — между пересборками плоского списка.
24
+ * `new DisplayLine` на каждую строку каждой пересборки был основной ценой
25
+ * раскрытия на больших деревьях. Поколенческий: пересборка переносит в
26
+ * новый кэш только встретившиеся метки, поэтому он не растёт без предела.
27
+ */
28
+ labelWidths = new Map();
22
29
  /**
23
30
  * Отступ контента строк от левого края элемента. В отличие от внешнего
24
31
  * паддинг-контейнера, входит в саму строку, поэтому фон курсора/выделения
@@ -52,7 +59,7 @@ export class TreeViewElement extends ScrollableElement {
52
59
  if (element !== undefined) {
53
60
  const key = this.provider.getKey(element);
54
61
  this.invalidateSubtreeCache(key);
55
- await this.reloadExpandedChildren(key);
62
+ await this.reloadExpandedChildren(element);
56
63
  }
57
64
  else {
58
65
  this.childrenCache.clear();
@@ -81,9 +88,24 @@ export class TreeViewElement extends ScrollableElement {
81
88
  }
82
89
  /** Раскрывает узел (идемпотентно, без прокрутки/выделения) и перестраивает список. */
83
90
  async expand(element) {
84
- if (this.expandedKeys.has(this.provider.getKey(element)))
91
+ await this.expandElements([element]);
92
+ }
93
+ /**
94
+ * Раскрывает несколько узлов разом (идемпотентно, без прокрутки/выделения)
95
+ * и перестраивает список один раз. Для «раскрыть все N» вместо N вызовов
96
+ * {@link expand}: каждый из них пересобирает весь плоский список, и на
97
+ * тысячах узлов это квадратичная цена.
98
+ */
99
+ async expandElements(elements) {
100
+ let changed = false;
101
+ for (const element of elements) {
102
+ if (this.expandedKeys.has(this.provider.getKey(element)))
103
+ continue;
104
+ await this.expandElement(element);
105
+ changed = true;
106
+ }
107
+ if (!changed)
85
108
  return;
86
- await this.expandElement(element);
87
109
  this.rebuildFlatList();
88
110
  this.markDirty();
89
111
  }
@@ -318,28 +340,28 @@ export class TreeViewElement extends ScrollableElement {
318
340
  return { bg: resolved.bg, fg: resolved.fg };
319
341
  }
320
342
  // ─── Private: data loading ───
343
+ /**
344
+ * Перечитывает корень и раскрытые поддеревья обходом от корня: раскрытый
345
+ * узел, до которого обход не дошёл (свёрнут предок или узла больше нет),
346
+ * в плоском списке всё равно не виден. Обход вместо поиска каждого
347
+ * раскрытого ключа по плоскому списку — иначе `refresh()` квадратичен по
348
+ * числу раскрытых узлов.
349
+ */
321
350
  async loadRootChildren() {
322
351
  const rootChildren = await this.provider.getChildren();
323
352
  this.childrenCache.set("__root__", rootChildren);
324
- for (const key of this.expandedKeys) {
325
- await this.reloadExpandedChildren(key);
353
+ for (const child of rootChildren) {
354
+ await this.reloadExpandedChildren(child);
326
355
  }
327
356
  }
328
- async reloadExpandedChildren(key) {
357
+ async reloadExpandedChildren(element) {
358
+ const key = this.provider.getKey(element);
329
359
  if (!this.expandedKeys.has(key))
330
360
  return;
331
- const node = this.findElementByKey(key);
332
- /* v8 ignore start -- unreachable: findElementByKey always resolves an expanded key (see its own v8-ignore note) */
333
- if (node === null)
334
- return;
335
- /* v8 ignore stop */
336
- const children = await this.provider.getChildren(node);
361
+ const children = await this.provider.getChildren(element);
337
362
  this.childrenCache.set(key, children);
338
363
  for (const child of children) {
339
- const childKey = this.provider.getKey(child);
340
- if (this.expandedKeys.has(childKey)) {
341
- await this.reloadExpandedChildren(childKey);
342
- }
364
+ await this.reloadExpandedChildren(child);
343
365
  }
344
366
  }
345
367
  invalidateSubtreeCache(key) {
@@ -359,14 +381,23 @@ export class TreeViewElement extends ScrollableElement {
359
381
  const rootChildren = this.childrenCache.get("__root__");
360
382
  if (!rootChildren)
361
383
  return;
362
- this.appendChildren(rootChildren, 0, null);
384
+ const previousWidths = this.labelWidths;
385
+ const labelWidths = new Map();
386
+ this.appendChildren(rootChildren, 0, null, previousWidths, labelWidths);
387
+ this.labelWidths = labelWidths;
363
388
  }
364
- appendChildren(children, depth, parentKey) {
389
+ appendChildren(children, depth, parentKey, previousWidths, labelWidths) {
365
390
  for (const element of children) {
366
391
  const key = this.provider.getKey(element);
367
392
  const item = this.provider.getTreeItem(element);
368
393
  this.flatNodes.push({ element, depth, item, parentKey });
369
- const rowWidth = this.calculateRowWidth(depth, item);
394
+ let labelWidth = labelWidths.get(item.label);
395
+ if (labelWidth === undefined) {
396
+ labelWidth = previousWidths.get(item.label) ?? new DisplayLine(item.label).displayWidth;
397
+ labelWidths.set(item.label, labelWidth);
398
+ }
399
+ // indent + expandIcon + space + icon + space + label
400
+ const rowWidth = this.rowIndentX(depth) + 2 + (item.icon ? 2 : 0) + labelWidth;
370
401
  if (rowWidth > this.maxRowWidth) {
371
402
  this.maxRowWidth = rowWidth;
372
403
  }
@@ -374,7 +405,7 @@ export class TreeViewElement extends ScrollableElement {
374
405
  const cachedChildren = this.childrenCache.get(key);
375
406
  /* v8 ignore start -- defensive: an expanded key always has its children cached (cache and expandedKeys stay in sync) */
376
407
  if (cachedChildren) {
377
- this.appendChildren(cachedChildren, depth + 1, key);
408
+ this.appendChildren(cachedChildren, depth + 1, key, previousWidths, labelWidths);
378
409
  }
379
410
  /* v8 ignore stop */
380
411
  }
@@ -384,10 +415,6 @@ export class TreeViewElement extends ScrollableElement {
384
415
  rowIndentX(depth) {
385
416
  return this.leftPadding + depth * INDENT_SIZE;
386
417
  }
387
- calculateRowWidth(depth, item) {
388
- // indent + expandIcon + space + icon + space + label
389
- return this.rowIndentX(depth) + 2 + (item.icon ? 2 : 0) + new DisplayLine(item.label).displayWidth;
390
- }
391
418
  formatRow(node) {
392
419
  const indent = " ".repeat(this.rowIndentX(node.depth));
393
420
  const expandIcon = node.item.collapsible
@@ -507,25 +534,6 @@ export class TreeViewElement extends ScrollableElement {
507
534
  this.scrollTo(this.scrollLeft, index - viewportHeight + 1);
508
535
  }
509
536
  }
510
- // ─── Private: key helpers ───
511
- findElementByKey(key) {
512
- for (const node of this.flatNodes) {
513
- if (this.provider.getKey(node.element) === key) {
514
- return node.element;
515
- }
516
- }
517
- /* v8 ignore start -- unreachable: findElementByKey is only called with keys of currently-flattened (visible) nodes, which the loop above always finds */
518
- // Search in cache values too
519
- for (const children of this.childrenCache.values()) {
520
- for (const child of children) {
521
- if (this.provider.getKey(child) === key) {
522
- return child;
523
- }
524
- }
525
- }
526
- return null;
527
- /* v8 ignore stop */
528
- }
529
537
  // ─── Private: input handlers ───
530
538
  handleKeypress(event) {
531
539
  switch (event.key) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tuidom/elements",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "type": "module",
5
5
  "description": "Experimental terminal UI DOM — widgets: lists, trees, menus, inputs, quick pick. API is unstable.",
6
6
  "license": "MIT",
@@ -28,6 +28,6 @@
28
28
  "./*": "./dist/*.js"
29
29
  },
30
30
  "dependencies": {
31
- "@tuidom/core": "0.6.0"
31
+ "@tuidom/core": "0.6.1"
32
32
  }
33
33
  }