@trebco/treb 29.5.4 → 29.7.5

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.
@@ -116,6 +116,7 @@ import type { ClipboardCellData } from './clipboard_data';
116
116
 
117
117
  import type { ExternalEditorConfig } from './external_editor_config';
118
118
  import { ExternalEditor } from '../editors/external_editor';
119
+ import type { ClipboardData, PasteOptions } from './clipboard_data2';
119
120
 
120
121
  interface DoubleClickData {
121
122
  timeout?: number;
@@ -416,6 +417,227 @@ export class Grid extends GridBase {
416
417
 
417
418
  }
418
419
 
420
+
421
+ // --- Copy/paste API methods ------------------------------------------------
422
+ //
423
+ // moving here with a view towards (eventually) merging with the UI/browser
424
+ // copy/paste routines (in grid)
425
+ //
426
+
427
+
428
+ /**
429
+ * internal composite for cut/copy. mostly identical except we
430
+ * read data as A1 for cut, so it will retain references. also
431
+ * cut clears the data.
432
+ *
433
+ * FIXME: merge with grid cut/copy/paste routines. we already
434
+ * handle recycling and relative addressing, the only thing missing
435
+ * is alternate formats.
436
+ */
437
+ public CopyArea(resolved: Area, semantics: 'cut'|'copy' = 'copy'): ClipboardData {
438
+
439
+ // resolve range so we can use it later -> Area
440
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.active_sheet) || this.active_sheet;
441
+
442
+ // get style data, !apply theme but do apply r/c styles
443
+ const style_data = sheet.GetCellStyle(resolved, false);
444
+
445
+ // flag we want R1C1 (copy)
446
+ const r1c1 = (semantics !== 'cut');
447
+
448
+ // NOTE: we're losing arrays here. need to fix. also think
449
+ // about merges? we'll reimplement what grid does (only in part)
450
+
451
+ const data: ClipboardData = [];
452
+
453
+ for (const { cell, row, column } of sheet.cells.IterateRC(resolved)) {
454
+
455
+ // raw value
456
+ let value = cell.value;
457
+
458
+ // seems like we're using a loop function unecessarily
459
+ if (r1c1 && value && cell.type === ValueType.formula) {
460
+ value = this.FormatR1C1(value, { row, column })[0][0];
461
+ }
462
+
463
+ const r = row - resolved.start.row;
464
+ const c = column - resolved.start.column;
465
+
466
+ if (!data[r]) {
467
+ data[r] = [];
468
+ }
469
+
470
+ let array_head: IArea|undefined;
471
+ if (cell.area) {
472
+
473
+ // scrubbing to just area (and unlinking)
474
+ array_head = {
475
+ start: {
476
+ row: cell.area.start.row - resolved.start.row,
477
+ column: cell.area.start.column - resolved.start.column,
478
+ },
479
+ end: {
480
+ row: cell.area.end.row - resolved.start.row,
481
+ column: cell.area.end.column - resolved.start.column,
482
+ },
483
+ };
484
+
485
+ }
486
+
487
+ data[r][c] = {
488
+ value,
489
+ calculated: cell.calculated,
490
+ style: style_data[r][c],
491
+ area: array_head,
492
+ };
493
+
494
+ }
495
+
496
+ // EmbeddedSpreadsheet.clipboard = structuredClone(data);
497
+
498
+ if (semantics === 'cut') {
499
+ this.SetRange(resolved, undefined, { recycle: true }); // clear
500
+ }
501
+
502
+ return data;
503
+
504
+ }
505
+
506
+ /**
507
+ * paste clipboard data into a target range. this method does not use
508
+ * the system clipboard; pass in clipboard data returned from the Cut or
509
+ * Copy method.
510
+ *
511
+ * @param target - the target to paste data into. this can be larger
512
+ * than the clipboard data, in which case values will be recycled in
513
+ * blocks. if the target is smaller than the source data, we will expand
514
+ * it to fit the data.
515
+ *
516
+ * @param data - clipboard data to paste.
517
+ *
518
+ * @privateRemarks LLM API
519
+ *
520
+ * @privateRemarks this was async when we were thinking of using the
521
+ * system clipboard, but that's pretty broken so we're not going to
522
+ * bother atm.
523
+ */
524
+ public PasteArea(resolved: Area, data: ClipboardData, options: PasteOptions = {}): void {
525
+
526
+ if (!data) {
527
+ throw new Error('no clipboad data');
528
+ }
529
+
530
+ // paste has some special semantics. if the target smaller than the
531
+ // source data, we write the full data irrespective of size (similar
532
+ // to "spill"). otherwise, we recycle in blocks.
533
+
534
+ // the setrange method will recycle, but we also need to recycle styles.
535
+
536
+ // start with data length
537
+
538
+ const rows = data.length;
539
+ const columns = data[0]?.length || 0;
540
+
541
+ // target -> block size
542
+
543
+ resolved.Resize(
544
+ Math.max(1, Math.floor(resolved.rows / rows)) * rows,
545
+ Math.max(1, Math.floor(resolved.columns / columns)) * columns );
546
+
547
+ const sheet = (resolved.start.sheet_id ? this.model.sheets.Find(resolved.start.sheet_id) : this.active_sheet) || this.active_sheet;
548
+
549
+ const values: CellValue[][] = [];
550
+
551
+ // optionally collect calculated values, instead of raw values
552
+
553
+ if (options.values) {
554
+ for (const [index, row] of data.entries()) {
555
+ values[index] = [];
556
+ for (const cell of row) {
557
+ values[index].push(typeof cell.calculated === 'undefined' ? cell.value : cell.calculated);
558
+ }
559
+ }
560
+ }
561
+
562
+ // this is to resolve the reference in the callback,
563
+ // but we should copy -- there's a possibility that
564
+ // this points to the static member, which could get
565
+ // overwritten. FIXME
566
+
567
+ const local = data;
568
+
569
+ // batch to limit events, sync up undo
570
+
571
+ const events = this.Batch(() => {
572
+
573
+ // this needs to change to support arrays (and potentially merges...)
574
+
575
+ // actually we could leave as is for just calculated values
576
+
577
+ if (options.values) {
578
+ this.SetRange(resolved, values, {
579
+ r1c1: true, recycle: true,
580
+ });
581
+ }
582
+ else {
583
+
584
+ // so this is for formulas only now
585
+
586
+ // start by clearing... (but leave styles as-is for now)
587
+
588
+ // probably a better way to do this
589
+ this.SetRange(resolved, undefined, { recycle: true });
590
+
591
+ for (const address of resolved) {
592
+ const r = (address.row - resolved.start.row) % rows;
593
+ const c = (address.column - resolved.start.column) % columns;
594
+
595
+ const cell_data = local[r][c];
596
+
597
+ if (cell_data.area) {
598
+ // only the head
599
+ if (cell_data.area.start.row === r && cell_data.area.start.column === c) {
600
+ const array_target = new Area(cell_data.area.start, cell_data.area.end);
601
+ array_target.Shift(resolved.start.row, resolved.start.column);
602
+ this.SetRange(array_target, cell_data.value, { r1c1: true, array: true });
603
+ }
604
+ }
605
+ else if (cell_data.value) {
606
+ this.SetRange(new Area(address), cell_data.value, { r1c1: true });
607
+ }
608
+
609
+ }
610
+
611
+ }
612
+
613
+ if (options.formatting === 'number-formats') {
614
+
615
+ // number format only, and apply delta
616
+
617
+ for (const address of resolved) {
618
+ const r = (address.row - resolved.start.row) % rows;
619
+ const c = (address.column - resolved.start.column) % columns;
620
+ const number_format = (local[r][c].style || {}).number_format;
621
+ sheet.UpdateCellStyle(address, { number_format }, true);
622
+ }
623
+ }
624
+ else if (options.formatting !== 'target') {
625
+
626
+ // use source formatting (default)
627
+ for (const address of resolved) {
628
+ const r = (address.row - resolved.start.row) % rows;
629
+ const c = (address.column - resolved.start.column) % columns;
630
+ sheet.UpdateCellStyle(address, local[r][c].style || {}, false);
631
+ }
632
+
633
+ }
634
+
635
+ }, true);
636
+
637
+ this.grid_events.Publish(events);
638
+
639
+ }
640
+
419
641
  // --- public methods --------------------------------------------------------
420
642
 
421
643
  /**
@@ -1883,23 +2105,19 @@ export class Grid extends GridBase {
1883
2105
  let convert = false;
1884
2106
 
1885
2107
  if (options.argument_separator === ',' && this.parser.argument_separator !== ArgumentSeparatorType.Comma) {
1886
- // this.parser.argument_separator = ArgumentSeparatorType.Comma;
1887
- // this.parser.decimal_mark = DecimalMarkType.Period;
1888
2108
  this.parser.SetLocaleSettings(DecimalMarkType.Period);
1889
-
1890
2109
  convert = true;
1891
2110
  }
1892
2111
 
1893
2112
  if (options.argument_separator === ';' && this.parser.argument_separator !== ArgumentSeparatorType.Semicolon) {
1894
- // this.parser.argument_separator = ArgumentSeparatorType.Semicolon;
1895
- // this.parser.decimal_mark = DecimalMarkType.Comma;
1896
2113
  this.parser.SetLocaleSettings(DecimalMarkType.Comma);
1897
-
1898
2114
  convert = true;
1899
2115
  }
1900
2116
 
1901
2117
  if (convert) {
1902
2118
 
2119
+ this.parser.flags.r1c1 = r1c1;
2120
+
1903
2121
  const Convert = (value: CellValue): CellValue => {
1904
2122
  if (typeof value === 'string' && value[0] === '=') {
1905
2123
  const result = this.parser.Parse(value);
@@ -1908,8 +2126,10 @@ export class Grid extends GridBase {
1908
2126
  missing: '',
1909
2127
  convert_decimal: current.decimal_mark,
1910
2128
  convert_argument_separator: current.argument_separator,
2129
+ pass_through_addresses: true,
1911
2130
  });
1912
2131
  }
2132
+ // console.info("CVT", this.parser.flags, result.expression, value);
1913
2133
  }
1914
2134
  return value;
1915
2135
  };
@@ -2176,21 +2396,40 @@ export class Grid extends GridBase {
2176
2396
  /**
2177
2397
  * batch updates. returns all the events that _would_ have been sent.
2178
2398
  * also does a paint (can disable).
2399
+ *
2400
+ * update for nesting/stacking. we won't return events until the last
2401
+ * batch is complete. paint will similarly toll until the last batch
2402
+ * is complete, and we'll carry forward any paint requirement from
2403
+ * inner batch funcs.
2404
+ *
2179
2405
  * @param func
2180
2406
  */
2181
2407
  public Batch(func: () => void, paint = true): GridEvent[] {
2182
2408
 
2183
- this.batch = true;
2409
+ if (this.batch === 0) {
2410
+ this.batch_paint = paint; // clear any old setting
2411
+ }
2412
+ else {
2413
+ this.batch_paint = this.batch_paint || paint; // ensure we honor it at the end
2414
+ }
2415
+
2416
+ this.batch++;
2184
2417
  func();
2185
- this.batch = false;
2186
- const events = this.batch_events.slice(0);
2187
- this.batch_events = [];
2418
+ this.batch--;
2419
+
2420
+ if (this.batch === 0) {
2421
+ const events = this.batch_events.slice(0);
2422
+ this.batch_events = [];
2188
2423
 
2189
- if (paint) {
2190
- this.DelayedRender(false);
2424
+ if (this.batch_paint) {
2425
+ this.DelayedRender(false);
2426
+ }
2427
+
2428
+ return events;
2191
2429
  }
2192
2430
 
2193
- return events;
2431
+ return []; // either nothing happened or we're not done
2432
+
2194
2433
  }
2195
2434
 
2196
2435
 
@@ -109,7 +109,19 @@ export class GridBase {
109
109
 
110
110
  // --- protected members -----------------------------------------------------
111
111
 
112
- protected batch = false;
112
+ /**
113
+ * switching to a stack, in case batching is nested. we don't need
114
+ * actual data so (atm) just count the depth.
115
+ */
116
+ protected batch = 0; // false;
117
+
118
+ /**
119
+ * if any batch method along the way requests a paint update, we
120
+ * want to toll it until the last batch call is complete, but we don't
121
+ * want to lose it. just remember to reset. [FIXME: isn't tolling this
122
+ * paint implicit, since it's async? ...]
123
+ */
124
+ protected batch_paint = false;
113
125
 
114
126
  protected batch_events: GridEvent[] = [];
115
127
 
@@ -4474,7 +4486,7 @@ export class GridBase {
4474
4486
  });
4475
4487
  }
4476
4488
 
4477
- if (this.batch) {
4489
+ if (this.batch > 0) {
4478
4490
  this.batch_events.push(...events);
4479
4491
  }
4480
4492
  else {
@@ -411,6 +411,9 @@ export interface RenderOptions {
411
411
  /** base for offsetting relative R1C1 addresses */
412
412
  r1c1_base?: UnitAddress;
413
413
 
414
+ /** if we're just translating, don't have to render addresses */
415
+ pass_through_addresses?: boolean;
416
+
414
417
  missing: string;
415
418
  convert_decimal: DecimalMarkType;
416
419
  convert_argument_separator: ArgumentSeparatorType;
@@ -34,6 +34,7 @@ import type {
34
34
  ParserFlags,
35
35
  UnitStructuredReference,
36
36
  RenderOptions,
37
+ BaseExpressionUnit,
37
38
  } from './parser-types';
38
39
  import {
39
40
  ArgumentSeparatorType,
@@ -334,6 +335,77 @@ export class Parser {
334
335
  }
335
336
  }
336
337
 
338
+ /**
339
+ * recursive tree walk that allows substitution. this should be
340
+ * a drop-in replacement for the original Walk function but I'm
341
+ * keeping it separate temporarily just in case it breaks something.
342
+ *
343
+ * @param func - in this version function can return `true` (continue
344
+ * walking subtree), `false` (don't walk subtree), or an ExpressionUnit.
345
+ * in the last case, we'll replace the original unit with the substitution.
346
+ * obviously in that case we don't recurse.
347
+ */
348
+ public Walk2(unit: ExpressionUnit, func: (unit: ExpressionUnit) => boolean|ExpressionUnit|undefined): ExpressionUnit {
349
+
350
+ const result = func(unit);
351
+ if (typeof result === 'object') {
352
+ return result;
353
+ }
354
+
355
+ switch (unit.type) {
356
+ case 'address':
357
+ case 'missing':
358
+ case 'literal':
359
+ case 'complex':
360
+ case 'identifier':
361
+ case 'operator':
362
+ case 'structured-reference':
363
+ break;
364
+
365
+ case 'dimensioned':
366
+ if (result) {
367
+ unit.expression = this.Walk2(unit.expression, func) as BaseExpressionUnit; // could be an issue
368
+ unit.unit = this.Walk2(unit.unit, func) as UnitIdentifier; // could be an issue
369
+ }
370
+ break;
371
+
372
+ case 'range':
373
+ if (func(unit)) {
374
+ unit.start = this.Walk2(unit.start, func) as UnitAddress; // could be an issue
375
+ unit.end = this.Walk2(unit.end, func) as UnitAddress; // could be an issue
376
+ }
377
+ break;
378
+
379
+ case 'binary':
380
+ if (func(unit)) {
381
+ unit.left = this.Walk2(unit.left, func);
382
+ unit.right = this.Walk2(unit.right, func);
383
+ }
384
+ break;
385
+
386
+ case 'unary':
387
+ if (func(unit)) {
388
+ unit.operand = this.Walk2(unit.operand, func);
389
+ }
390
+ break;
391
+
392
+ case 'group':
393
+ if (func(unit)) {
394
+ unit.elements = unit.elements.map(source => this.Walk2(source, func));
395
+ }
396
+ break;
397
+
398
+ case 'call':
399
+ if (func(unit)) {
400
+ unit.args = unit.args.map(source => this.Walk2(source, func));
401
+ }
402
+ break;
403
+ }
404
+
405
+ return unit;
406
+
407
+ }
408
+
337
409
  /**
338
410
  * recursive tree walk.
339
411
  *
@@ -382,13 +454,19 @@ export class Parser {
382
454
 
383
455
  case 'group':
384
456
  if (func(unit)) {
385
- unit.elements.forEach((element) => this.Walk(element, func));
457
+ // unit.elements.forEach((element) => this.Walk(element, func));
458
+ for (const element of unit.elements) {
459
+ this.Walk(element, func);
460
+ }
386
461
  }
387
462
  return;
388
463
 
389
464
  case 'call':
390
465
  if (func(unit)) {
391
- unit.args.forEach((arg) => this.Walk(arg, func));
466
+ for (const arg of unit.args) {
467
+ this.Walk(arg, func);
468
+ }
469
+ // unit.args.forEach((arg) => this.Walk(arg, func));
392
470
  }
393
471
  }
394
472
  }
@@ -489,9 +567,15 @@ export class Parser {
489
567
 
490
568
  switch (unit.type) {
491
569
  case 'address':
570
+ if (options.pass_through_addresses) {
571
+ return unit.label;
572
+ }
492
573
  return options.r1c1 ? this.R1C1Label(unit, options.r1c1_base) : this.AddressLabel(unit, offset);
493
574
 
494
575
  case 'range':
576
+ if (options.pass_through_addresses) {
577
+ return unit.label;
578
+ }
495
579
  return options.r1c1 ?
496
580
  this.R1C1Label(unit.start, options.r1c1_base) + ':' + this.R1C1Label(unit.end, options.r1c1_base) :
497
581
  this.AddressLabel(unit.start, offset) + ':' + this.AddressLabel(unit.end, offset);