@ashx-j/lunr-tui 0.2.26 → 0.2.28

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.
@@ -106,7 +106,7 @@ function segmentWithMarkers(text, baseSegmenter, families) {
106
106
  */
107
107
  export function wordWrapLine(line, maxWidth, preSegmented) {
108
108
  if (!line || maxWidth <= 0) {
109
- return [{ text: "", startIndex: 0, endIndex: 0 }];
109
+ return [{ text: "", startIndex: 0, endIndex: line.length }];
110
110
  }
111
111
  const lineWidth = visibleWidth(line);
112
112
  if (lineWidth <= maxWidth) {
@@ -148,11 +148,17 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
148
148
  wrapOppIndex = -1;
149
149
  }
150
150
  if (gWidth > maxWidth) {
151
- // Single atomic segment wider than maxWidth (e.g. paste marker
152
- // in a narrow terminal). Re-wrap it at grapheme granularity.
153
- // The segment remains logically atomic for cursor
154
- // movement / editing — the split is purely visual for word-wrap layout.
155
- const subChunks = wordWrapLine(grapheme, maxWidth);
151
+ const subSegments = [...graphemeSegmenter.segment(grapheme)];
152
+ if (subSegments.length === 1) {
153
+ // Keep the source range even when this grapheme cannot occupy its two cells.
154
+ chunks.push({ text: "�", startIndex: charIndex, endIndex: charIndex + grapheme.length });
155
+ chunkStart = charIndex + grapheme.length;
156
+ currentWidth = 0;
157
+ wrapOppIndex = -1;
158
+ continue;
159
+ }
160
+ // Split a multi-grapheme atomic marker visually, while keeping editing atomic.
161
+ const subChunks = wordWrapLine(grapheme, maxWidth, subSegments);
156
162
  for (let j = 0; j < subChunks.length - 1; j++) {
157
163
  const sc = subChunks[j];
158
164
  chunks.push({ text: sc.text, startIndex: charIndex + sc.startIndex, endIndex: charIndex + sc.endIndex });
@@ -184,7 +190,9 @@ export function wordWrapLine(line, maxWidth, preSegmented) {
184
190
  }
185
191
  }
186
192
  // Push final chunk.
187
- chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
193
+ if (chunkStart < line.length || chunks.length === 0) {
194
+ chunks.push({ text: line.slice(chunkStart), startIndex: chunkStart, endIndex: line.length });
195
+ }
188
196
  return chunks;
189
197
  }
190
198
  const SLASH_COMMAND_SELECT_LIST_LAYOUT = {
@@ -262,7 +270,11 @@ export class Editor {
262
270
  // to.
263
271
  snappedFromCursorCol = null;
264
272
  // Undo support
265
- undoStack = new UndoStack();
273
+ // Attachment maps are copied only when membership changes; snapshots share file metadata.
274
+ undoStack = new UndoStack((snapshot) => ({
275
+ state: structuredClone(snapshot.state),
276
+ images: snapshot.images,
277
+ }));
266
278
  onSubmit;
267
279
  onChange;
268
280
  disableSubmit = false;
@@ -400,6 +412,8 @@ export class Editor {
400
412
  // No cached state to invalidate currently
401
413
  }
402
414
  render(width) {
415
+ if (width <= 0)
416
+ return [];
403
417
  const maxPadding = Math.max(0, Math.floor((width - 1) / 2));
404
418
  const paddingX = Math.min(this.paddingX, maxPadding);
405
419
  const contentWidth = Math.max(1, width - paddingX * 2);
@@ -494,7 +508,7 @@ export class Editor {
494
508
  if (linesBelow > 0) {
495
509
  const indicator = `─── ↓ ${linesBelow} more `;
496
510
  const remaining = width - visibleWidth(indicator);
497
- result.push(this.borderColor(indicator + "─".repeat(Math.max(0, remaining))));
511
+ result.push(this.borderColor(truncateToWidth(indicator + "─".repeat(Math.max(0, remaining)), width, "")));
498
512
  }
499
513
  else {
500
514
  result.push(horizontal.repeat(width));
@@ -815,18 +829,14 @@ export class Editor {
815
829
  if (isLastChunk) {
816
830
  // Last chunk: cursor belongs here if >= startIndex
817
831
  hasCursorInChunk = cursorPos >= chunk.startIndex;
818
- adjustedCursorPos = cursorPos - chunk.startIndex;
832
+ adjustedCursorPos = Math.min(cursorPos - chunk.startIndex, chunk.text.length);
819
833
  }
820
834
  else {
821
835
  // Non-last chunk: cursor belongs here if in range [startIndex, endIndex)
822
836
  // But we need to handle the visual position in the trimmed text
823
837
  hasCursorInChunk = cursorPos >= chunk.startIndex && cursorPos < chunk.endIndex;
824
838
  if (hasCursorInChunk) {
825
- adjustedCursorPos = cursorPos - chunk.startIndex;
826
- // Clamp to text length (in case cursor was in trimmed whitespace)
827
- if (adjustedCursorPos > chunk.text.length) {
828
- adjustedCursorPos = chunk.text.length;
829
- }
839
+ adjustedCursorPos = Math.min(cursorPos - chunk.startIndex, chunk.text.length);
830
840
  }
831
841
  }
832
842
  }
@@ -846,6 +856,14 @@ export class Editor {
846
856
  }
847
857
  }
848
858
  }
859
+ // At one column there is no spare cell for an end cursor.
860
+ if (contentWidth === 1) {
861
+ const cursorLine = layoutLines.find((line) => line.hasCursor);
862
+ if (cursorLine?.text && cursorLine.cursorPos === cursorLine.text.length) {
863
+ cursorLine.hasCursor = false;
864
+ layoutLines.splice(layoutLines.indexOf(cursorLine) + 1, 0, { text: "", hasCursor: true, cursorPos: 0 });
865
+ }
866
+ }
849
867
  return layoutLines;
850
868
  }
851
869
  getText() {
@@ -876,16 +894,14 @@ export class Editor {
876
894
  this.cancelAutocomplete();
877
895
  this.lastAction = null;
878
896
  this.exitHistoryBrowsing();
879
- this.pastes.clear();
880
- this.pasteCounter = 0;
881
- this.images.clear();
882
- this.imageCounter = 0;
883
- // Keep submittedImages: submitValue() snapshots chips, then setText("") runs.
884
897
  const normalized = this.normalizeText(text);
885
- // Push undo snapshot if content differs (makes programmatic changes undoable)
886
898
  if (this.getText() !== normalized) {
887
899
  this.pushUndoSnapshot();
888
900
  }
901
+ this.pastes.clear();
902
+ this.pasteCounter = 0;
903
+ this.images = new Map();
904
+ // Keep submittedImages: submitValue() snapshots chips, then setText("") runs.
889
905
  this.setTextInternal(normalized);
890
906
  }
891
907
  /**
@@ -913,7 +929,9 @@ export class Editor {
913
929
  this.exitHistoryBrowsing();
914
930
  this.imageCounter++;
915
931
  const id = this.imageCounter;
916
- this.images.set(id, { id, path: attachment.path, mimeType: attachment.mimeType });
932
+ const images = new Map(this.images);
933
+ images.set(id, { id, path: attachment.path, mimeType: attachment.mimeType });
934
+ this.images = images;
917
935
  this.insertTextAtCursorInternal(formatImageMarker(id));
918
936
  return id;
919
937
  }
@@ -922,13 +940,14 @@ export class Editor {
922
940
  * Used by /edit so `[image_n]` stays a chip instead of becoming a path.
923
941
  */
924
942
  restoreImageMarkers(attachments) {
925
- this.images.clear();
943
+ const images = new Map();
926
944
  this.imageCounter = 0;
927
945
  for (const attachment of attachments) {
928
- this.images.set(attachment.id, { ...attachment });
946
+ images.set(attachment.id, { ...attachment });
929
947
  if (attachment.id > this.imageCounter)
930
948
  this.imageCounter = attachment.id;
931
949
  }
950
+ this.images = images;
932
951
  }
933
952
  /** Pending image chips still in the editor, in first-appearance order. */
934
953
  getPendingImages() {
@@ -945,7 +964,7 @@ export class Editor {
945
964
  takePendingImages() {
946
965
  const pending = this.submittedImages.length > 0 ? this.submittedImages : this.getPendingImages();
947
966
  this.submittedImages = [];
948
- this.images.clear();
967
+ this.images = new Map();
949
968
  this.imageCounter = 0;
950
969
  return pending;
951
970
  }
@@ -1148,7 +1167,7 @@ export class Editor {
1148
1167
  this.state = { lines: [""], cursorLine: 0, cursorCol: 0 };
1149
1168
  this.pastes.clear();
1150
1169
  this.pasteCounter = 0;
1151
- this.images.clear();
1170
+ this.images = new Map();
1152
1171
  this.imageCounter = 0;
1153
1172
  this.exitHistoryBrowsing();
1154
1173
  this.scrollOffset = 0;
@@ -1192,7 +1211,9 @@ export class Editor {
1192
1211
  }
1193
1212
  else if (isImageSegmented) {
1194
1213
  const targetId = Number(isImageSegmented[1]);
1195
- this.images.delete(targetId);
1214
+ const images = new Map(this.images);
1215
+ images.delete(targetId);
1216
+ this.images = images;
1196
1217
  }
1197
1218
  line = this.state.lines[this.state.cursorLine] || "";
1198
1219
  const before = line.slice(0, this.state.cursorCol - graphemeLength);
@@ -1501,7 +1522,9 @@ export class Editor {
1501
1522
  const graphemeLength = firstGrapheme ? firstGrapheme.segment.length : 1;
1502
1523
  const deletedImage = firstGrapheme ? IMAGE_MARKER_SINGLE.exec(firstGrapheme.segment) : null;
1503
1524
  if (deletedImage) {
1504
- this.images.delete(Number(deletedImage[1]));
1525
+ const images = new Map(this.images);
1526
+ images.delete(Number(deletedImage[1]));
1527
+ this.images = images;
1505
1528
  }
1506
1529
  const before = currentLine.slice(0, this.state.cursorCol);
1507
1530
  const after = currentLine.slice(this.state.cursorCol + graphemeLength);
@@ -1784,14 +1807,17 @@ export class Editor {
1784
1807
  }
1785
1808
  }
1786
1809
  pushUndoSnapshot() {
1787
- this.undoStack.push(this.state);
1810
+ this.undoStack.push({ state: this.state, images: this.images });
1788
1811
  }
1789
1812
  undo() {
1790
1813
  this.exitHistoryBrowsing();
1791
1814
  const snapshot = this.undoStack.pop();
1792
1815
  if (!snapshot)
1793
1816
  return;
1794
- Object.assign(this.state, snapshot);
1817
+ Object.assign(this.state, snapshot.state);
1818
+ this.images = snapshot.images;
1819
+ for (const id of this.images.keys())
1820
+ this.imageCounter = Math.max(this.imageCounter, id);
1795
1821
  this.lastAction = null;
1796
1822
  this.preferredVisualCol = null;
1797
1823
  if (this.onChange) {