@sunub/obsidian-mcp-server 0.3.23 → 0.3.24

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 (135) hide show
  1. package/build/cli/App.d.ts +4 -1
  2. package/build/cli/App.js +158 -104
  3. package/build/cli/App.js.map +1 -1
  4. package/build/cli/AppContainer.js +8 -2
  5. package/build/cli/AppContainer.js.map +1 -1
  6. package/build/cli/config/mcpServersConfig.js +19 -2
  7. package/build/cli/config/mcpServersConfig.js.map +1 -1
  8. package/build/cli/context/InputContext.d.ts +1 -1
  9. package/build/cli/context/KeypressContext.js +0 -5
  10. package/build/cli/context/KeypressContext.js.map +1 -1
  11. package/build/cli/context/KeypressContext.util.js +1 -2
  12. package/build/cli/context/KeypressContext.util.js.map +1 -1
  13. package/build/cli/context/UIStateContext.d.ts +16 -0
  14. package/build/cli/context/UIStateContext.js +10 -0
  15. package/build/cli/context/UIStateContext.js.map +1 -0
  16. package/build/cli/hooks/useCommandCompletion.d.ts +1 -1
  17. package/build/cli/hooks/useCommandCompletion.js +1 -1
  18. package/build/cli/hooks/useCommandCompletion.js.map +1 -1
  19. package/build/cli/hooks/useHistoryManager.d.ts +1 -0
  20. package/build/cli/hooks/useHistoryManager.js +19 -1
  21. package/build/cli/hooks/useHistoryManager.js.map +1 -1
  22. package/build/cli/hooks/useInputHistory.d.ts +2 -2
  23. package/build/cli/hooks/useInputHistory.js +2 -2
  24. package/build/cli/hooks/useInputHistory.js.map +1 -1
  25. package/build/cli/hooks/useKeyMatchers.d.ts +0 -9
  26. package/build/cli/hooks/useKeyMatchers.js +0 -9
  27. package/build/cli/hooks/useKeyMatchers.js.map +1 -1
  28. package/build/cli/hooks/useKeypress.d.ts +0 -13
  29. package/build/cli/hooks/useKeypress.js +0 -13
  30. package/build/cli/hooks/useKeypress.js.map +1 -1
  31. package/build/cli/hooks/useLlmStream/useLlmStream.d.ts +1 -1
  32. package/build/cli/hooks/useLlmStream/useLlmStream.js +9 -3
  33. package/build/cli/hooks/useLlmStream/useLlmStream.js.map +1 -1
  34. package/build/cli/hooks/useMcpManager.d.ts +0 -10
  35. package/build/cli/hooks/useMcpManager.js +6 -0
  36. package/build/cli/hooks/useMcpManager.js.map +1 -1
  37. package/build/cli/hooks/usePromptCompletion.d.ts +1 -2
  38. package/build/cli/hooks/usePromptCompletion.js +0 -1
  39. package/build/cli/hooks/usePromptCompletion.js.map +1 -1
  40. package/build/cli/hooks/useTransientMessage.d.ts +7 -0
  41. package/build/cli/hooks/useTransientMessage.js +34 -0
  42. package/build/cli/hooks/useTransientMessage.js.map +1 -0
  43. package/build/cli/index.js +7 -3
  44. package/build/cli/index.js.map +1 -1
  45. package/build/cli/key/keyBindings.d.ts +0 -19
  46. package/build/cli/key/keyBindings.js +0 -14
  47. package/build/cli/key/keyBindings.js.map +1 -1
  48. package/build/cli/key/keyMatchers.d.ts +0 -9
  49. package/build/cli/key/keyMatchers.js +0 -8
  50. package/build/cli/key/keyMatchers.js.map +1 -1
  51. package/build/cli/key/keybindingUtils.d.ts +3 -0
  52. package/build/cli/key/keybindingUtils.js +74 -0
  53. package/build/cli/key/keybindingUtils.js.map +1 -0
  54. package/build/cli/key/textBuffer/constants.d.ts +1 -0
  55. package/build/cli/key/textBuffer/constants.js +2 -0
  56. package/build/cli/key/textBuffer/constants.js.map +1 -0
  57. package/build/cli/key/textBuffer/index.d.ts +3 -0
  58. package/build/cli/key/textBuffer/index.js +3 -0
  59. package/build/cli/key/textBuffer/index.js.map +1 -0
  60. package/build/cli/key/textBuffer/textBuffer.d.ts +73 -0
  61. package/build/cli/key/textBuffer/textBuffer.js +1154 -0
  62. package/build/cli/key/textBuffer/textBuffer.js.map +1 -0
  63. package/build/cli/key/textBuffer/types.d.ts +106 -0
  64. package/build/cli/key/textBuffer/types.js +2 -0
  65. package/build/cli/key/textBuffer/types.js.map +1 -0
  66. package/build/cli/services/InputOffloadService.d.ts +20 -0
  67. package/build/cli/services/InputOffloadService.js +105 -0
  68. package/build/cli/services/InputOffloadService.js.map +1 -0
  69. package/build/cli/services/McpClientService.js +5 -3
  70. package/build/cli/services/McpClientService.js.map +1 -1
  71. package/build/cli/services/McpManager.js +4 -2
  72. package/build/cli/services/McpManager.js.map +1 -1
  73. package/build/cli/theme/builtin/tokyonight-dark.js +1 -0
  74. package/build/cli/theme/builtin/tokyonight-dark.js.map +1 -1
  75. package/build/cli/theme/semantic-tokens.js +8 -8
  76. package/build/cli/theme/semantic-tokens.js.map +1 -1
  77. package/build/cli/theme/theme-manager.js +4 -2
  78. package/build/cli/theme/theme-manager.js.map +1 -1
  79. package/build/cli/theme/theme.d.ts +1 -23
  80. package/build/cli/theme/theme.js +11 -31
  81. package/build/cli/theme/theme.js.map +1 -1
  82. package/build/cli/types.d.ts +1 -2
  83. package/build/cli/ui/CLILogo.d.ts +1 -0
  84. package/build/cli/ui/CLILogo.js +33 -0
  85. package/build/cli/ui/CLILogo.js.map +1 -0
  86. package/build/cli/ui/HistoryItemDisplay.js +36 -1
  87. package/build/cli/ui/HistoryItemDisplay.js.map +1 -1
  88. package/build/cli/ui/InputPrompt.d.ts +8 -1
  89. package/build/cli/ui/InputPrompt.js +142 -24
  90. package/build/cli/ui/InputPrompt.js.map +1 -1
  91. package/build/cli/ui/LLMHealthChecker.js +1 -1
  92. package/build/cli/ui/LLMStatusLoader.d.ts +10 -0
  93. package/build/cli/ui/LLMStatusLoader.js +35 -0
  94. package/build/cli/ui/LLMStatusLoader.js.map +1 -0
  95. package/build/cli/ui/MainContent.js +2 -7
  96. package/build/cli/ui/MainContent.js.map +1 -1
  97. package/build/cli/ui/SystemInfoSummaryBox.js +3 -2
  98. package/build/cli/ui/SystemInfoSummaryBox.js.map +1 -1
  99. package/build/cli/ui/ThinkingIndicator.d.ts +3 -2
  100. package/build/cli/ui/ThinkingIndicator.js +3 -3
  101. package/build/cli/ui/ThinkingIndicator.js.map +1 -1
  102. package/build/cli/ui/ToastDisplay.d.ts +2 -0
  103. package/build/cli/ui/ToastDisplay.js +19 -0
  104. package/build/cli/ui/ToastDisplay.js.map +1 -0
  105. package/build/cli/ui/shared/ExpandableText.d.ts +0 -3
  106. package/build/cli/ui/shared/ExpandableText.js +0 -3
  107. package/build/cli/ui/shared/ExpandableText.js.map +1 -1
  108. package/build/cli/utils/bootLogo.d.ts +8 -0
  109. package/build/cli/utils/bootLogo.js +68 -0
  110. package/build/cli/utils/bootLogo.js.map +1 -0
  111. package/build/cli/utils/events.d.ts +28 -0
  112. package/build/cli/utils/events.js +18 -0
  113. package/build/cli/utils/events.js.map +1 -0
  114. package/build/cli/utils/highlights.d.ts +8 -0
  115. package/build/cli/utils/highlights.js +117 -0
  116. package/build/cli/utils/highlights.js.map +1 -0
  117. package/build/cli/utils/textUtil.d.ts +5 -0
  118. package/build/cli/utils/textUtil.js +32 -0
  119. package/build/cli/utils/textUtil.js.map +1 -1
  120. package/build/shared/utils/debugLogger.d.ts +4 -0
  121. package/build/shared/utils/debugLogger.js +12 -0
  122. package/build/shared/utils/debugLogger.js.map +1 -1
  123. package/build/utils/cleanup.d.ts +30 -0
  124. package/build/utils/cleanup.js +137 -0
  125. package/build/utils/cleanup.js.map +1 -0
  126. package/package.json +4 -2
  127. package/build/cli/key/text-buffer.d.ts +0 -156
  128. package/build/cli/key/text-buffer.js +0 -937
  129. package/build/cli/key/text-buffer.js.map +0 -1
  130. package/build/cli/tmp.d.ts +0 -1
  131. package/build/cli/tmp.js +0 -27
  132. package/build/cli/tmp.js.map +0 -1
  133. package/build/cli/ui/layout/DefaultAppLayout.d.ts +0 -2
  134. package/build/cli/ui/layout/DefaultAppLayout.js +0 -6
  135. package/build/cli/ui/layout/DefaultAppLayout.js.map +0 -1
@@ -0,0 +1,1154 @@
1
+ import { LRU_BUFFER_PERF_CACHE_LIMIT } from "../../constants.js";
2
+ import { cpLen, cpSlice, getCachedStringWidth, shiftExpandedRegions, stripUnsafeCharacters, toCodePoints, } from "../../utils/textUtil.js";
3
+ import { LRUCache } from "mnemonist";
4
+ import { useCallback, useEffect, useMemo, useReducer } from "react";
5
+ import { LARGE_PASTE_CHAR_THRESHOLD, LARGE_PASTE_LINE_THRESHOLD, } from "../../../cli/ui/InputPrompt.js";
6
+ import { PASTED_TEXT_PLACEHOLDER_REGEX } from "./constants.js";
7
+ const lineLayoutCache = new LRUCache(LRU_BUFFER_PERF_CACHE_LIMIT);
8
+ const historyLimit = 100;
9
+ function calculateInitialCursorPosition(initialLines, offset) {
10
+ let remainingChars = offset;
11
+ let row = 0;
12
+ while (row < initialLines.length) {
13
+ const lineLength = cpLen(initialLines[row]);
14
+ // Add 1 for the newline character (except for the last line)
15
+ const totalCharsInLineAndNewline = lineLength + (row < initialLines.length - 1 ? 1 : 0);
16
+ if (remainingChars <= lineLength) {
17
+ // Cursor is on this line
18
+ return [row, remainingChars];
19
+ }
20
+ remainingChars -= totalCharsInLineAndNewline;
21
+ row++;
22
+ }
23
+ // Offset is beyond the text, place cursor at the end of the last line
24
+ if (initialLines.length > 0) {
25
+ const lastRow = initialLines.length - 1;
26
+ return [lastRow, cpLen(initialLines[lastRow])];
27
+ }
28
+ return [0, 0]; // Default for empty text
29
+ }
30
+ export const replaceRangeInternal = (state, startRow, startCol, endRow, endCol, text) => {
31
+ const currentLine = (row) => state.lines[row] || "";
32
+ const currentLineLen = (row) => cpLen(currentLine(row));
33
+ const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
34
+ if (startRow > endRow ||
35
+ (startRow === endRow && startCol > endCol) ||
36
+ startRow < 0 ||
37
+ startCol < 0 ||
38
+ endRow >= state.lines.length ||
39
+ (endRow < state.lines.length && endCol > currentLineLen(endRow))) {
40
+ return state; // Invalid range
41
+ }
42
+ const newLines = [...state.lines];
43
+ const sCol = clamp(startCol, 0, currentLineLen(startRow));
44
+ const eCol = clamp(endCol, 0, currentLineLen(endRow));
45
+ const prefix = cpSlice(currentLine(startRow), 0, sCol);
46
+ const suffix = cpSlice(currentLine(endRow), eCol);
47
+ const normalisedReplacement = text
48
+ .replace(/\r\n/g, "\n")
49
+ .replace(/\r/g, "\n");
50
+ const replacementParts = normalisedReplacement.split("\n");
51
+ // The combined first line of the new text
52
+ const firstLine = prefix + replacementParts[0];
53
+ if (replacementParts.length === 1) {
54
+ // No newlines in replacement: combine prefix, replacement, and suffix on one line.
55
+ newLines.splice(startRow, endRow - startRow + 1, firstLine + suffix);
56
+ }
57
+ else {
58
+ // Newlines in replacement: create new lines.
59
+ const lastLine = replacementParts[replacementParts.length - 1] + suffix;
60
+ const middleLines = replacementParts.slice(1, -1);
61
+ newLines.splice(startRow, endRow - startRow + 1, firstLine, ...middleLines, lastLine);
62
+ }
63
+ const finalCursorRow = startRow + replacementParts.length - 1;
64
+ const finalCursorCol = (replacementParts.length > 1 ? 0 : sCol) +
65
+ cpLen(replacementParts[replacementParts.length - 1]);
66
+ return {
67
+ ...state,
68
+ lines: newLines,
69
+ cursorRow: Math.min(Math.max(finalCursorRow, 0), newLines.length - 1),
70
+ cursorCol: Math.max(0, Math.min(finalCursorCol, cpLen(newLines[finalCursorRow] || ""))),
71
+ preferredCol: null,
72
+ };
73
+ };
74
+ export function getExpandedPasteAtLine(lineIndex, expandedPaste) {
75
+ if (expandedPaste &&
76
+ lineIndex >= expandedPaste.startLine &&
77
+ lineIndex < expandedPaste.startLine + expandedPaste.lineCount) {
78
+ return expandedPaste.id;
79
+ }
80
+ return null;
81
+ }
82
+ export function detachExpandedPaste(state) {
83
+ const expandedId = getExpandedPasteAtLine(state.cursorRow, state.expandedPaste);
84
+ if (!expandedId)
85
+ return state;
86
+ const { [expandedId]: _, ...newPastedContent } = state.pastedContent;
87
+ return {
88
+ ...state,
89
+ expandedPaste: null,
90
+ pastedContent: newPastedContent,
91
+ };
92
+ }
93
+ export const pushUndo = (currentState) => {
94
+ const snapshot = {
95
+ lines: [...currentState.lines],
96
+ cursorRow: currentState.cursorRow,
97
+ cursorCol: currentState.cursorCol,
98
+ pastedContent: { ...currentState.pastedContent },
99
+ expandedPaste: currentState.expandedPaste
100
+ ? { ...currentState.expandedPaste }
101
+ : null,
102
+ };
103
+ const newStack = [...currentState.undoStack, snapshot];
104
+ if (newStack.length > historyLimit) {
105
+ newStack.shift();
106
+ }
107
+ return { ...currentState, undoStack: newStack, redoStack: [] };
108
+ };
109
+ function generatePastedTextId(content, lineCount, pastedContent) {
110
+ const base = lineCount > LARGE_PASTE_LINE_THRESHOLD
111
+ ? `[Pasted Text: ${lineCount} lines]`
112
+ : `[Pasted Text: ${content.length} chars]`;
113
+ let id = base;
114
+ let suffix = 2;
115
+ while (pastedContent[id]) {
116
+ id = base.replace("]", ` #${suffix}]`);
117
+ suffix++;
118
+ }
119
+ return id;
120
+ }
121
+ function normalizePasteContent(content) {
122
+ return stripUnsafeCharacters(content.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
123
+ }
124
+ export function buildExpandedPasteInfo(line, row, transformation, pastedContent) {
125
+ if (transformation.type !== "paste" || !transformation.id) {
126
+ return null;
127
+ }
128
+ const pastedText = pastedContent[transformation.id];
129
+ if (!pastedText) {
130
+ return null;
131
+ }
132
+ const normalizedContent = normalizePasteContent(pastedText);
133
+ const lineCount = normalizedContent.split("\n").length;
134
+ return {
135
+ id: transformation.id,
136
+ startLine: row,
137
+ lineCount,
138
+ prefix: cpSlice(line, 0, transformation.logStart),
139
+ suffix: cpSlice(line, transformation.logEnd),
140
+ };
141
+ }
142
+ function getLineLayoutCacheKey(line, viewportWidth, isCursorOnLine, cursorCol) {
143
+ if (!isCursorOnLine) {
144
+ return `${viewportWidth}:N:${line}`;
145
+ }
146
+ return `${viewportWidth}:C:${cursorCol}:${line}`;
147
+ }
148
+ function calculateVisualCursorFromLayout(layout, logicalCursor) {
149
+ const { logicalToVisualMap, visualLines, transformedToLogicalMaps } = layout;
150
+ const [logicalRow, logicalCol] = logicalCursor;
151
+ const segmentsForLogicalLine = logicalToVisualMap[logicalRow];
152
+ if (!segmentsForLogicalLine || segmentsForLogicalLine.length === 0) {
153
+ return [0, 0, 0];
154
+ }
155
+ let targetSegmentIndex = segmentsForLogicalLine.findIndex(([, startColInLogical], index) => {
156
+ const nextStartColInLogical = index + 1 < segmentsForLogicalLine.length
157
+ ? segmentsForLogicalLine[index + 1][1]
158
+ : Infinity;
159
+ return (logicalCol >= startColInLogical && logicalCol < nextStartColInLogical);
160
+ });
161
+ if (targetSegmentIndex === -1) {
162
+ if (logicalCol === 0) {
163
+ targetSegmentIndex = 0;
164
+ }
165
+ else {
166
+ targetSegmentIndex = segmentsForLogicalLine.length - 1;
167
+ }
168
+ }
169
+ const [visualRow, startColInLogical] = segmentsForLogicalLine[targetSegmentIndex];
170
+ // Find the coordinates in transformed space in order to conver to visual
171
+ const transformedToLogicalMap = transformedToLogicalMaps[logicalRow] ?? [];
172
+ let transformedCol = 0;
173
+ for (let i = 0; i < transformedToLogicalMap.length; i++) {
174
+ if (transformedToLogicalMap[i] > logicalCol) {
175
+ transformedCol = Math.max(0, i - 1);
176
+ break;
177
+ }
178
+ if (i === transformedToLogicalMap.length - 1) {
179
+ transformedCol = transformedToLogicalMap.length - 1;
180
+ }
181
+ }
182
+ let startColInTransformed = 0;
183
+ while (startColInTransformed < transformedToLogicalMap.length &&
184
+ transformedToLogicalMap[startColInTransformed] < startColInLogical) {
185
+ startColInTransformed++;
186
+ }
187
+ const clampedTransformedCol = Math.min(transformedCol, Math.max(0, transformedToLogicalMap.length - 1));
188
+ const visualColIndex = clampedTransformedCol - startColInTransformed;
189
+ const currentVisualLineText = visualLines[visualRow] ?? "";
190
+ let visualColWidth = 0;
191
+ const codePoints = toCodePoints(currentVisualLineText);
192
+ for (let i = 0; i < Math.min(visualColIndex, codePoints.length); i++) {
193
+ visualColWidth += getCachedStringWidth(codePoints[i]);
194
+ }
195
+ const clampedVisualColIndex = Math.min(Math.max(visualColIndex, 0), codePoints.length);
196
+ return [visualRow, clampedVisualColIndex, visualColWidth];
197
+ }
198
+ /**
199
+ * Helper: Converts logical row/col position to absolute text offset
200
+ */
201
+ export function logicalPosToOffset(lines, row, col) {
202
+ let offset = 0;
203
+ const actualRow = Math.min(row, lines.length - 1);
204
+ for (let i = 0; i < actualRow; i++) {
205
+ offset += cpLen(lines[i]) + 1; // +1 for newline
206
+ }
207
+ if (actualRow >= 0 && actualRow < lines.length) {
208
+ offset += Math.min(col, cpLen(lines[actualRow]));
209
+ }
210
+ return offset;
211
+ }
212
+ /**
213
+ * Helper: Converts absolute text offset to logical row/col position
214
+ */
215
+ export function offsetToLogicalPos(text, offset) {
216
+ const lines = text.split("\n");
217
+ let currentOffset = 0;
218
+ for (let i = 0; i < lines.length; i++) {
219
+ const lineLen = cpLen(lines[i]);
220
+ const lineLenWithNewline = lineLen + (i < lines.length - 1 ? 1 : 0);
221
+ if (offset <= currentOffset + lineLen) {
222
+ return [i, offset - currentOffset];
223
+ }
224
+ if (offset < currentOffset + lineLenWithNewline) {
225
+ return [i, lineLen];
226
+ }
227
+ currentOffset += lineLenWithNewline;
228
+ }
229
+ const lastRow = Math.max(0, lines.length - 1);
230
+ return [lastRow, cpLen(lines[lastRow] || "")];
231
+ }
232
+ function bufferReducerLogic(state, action) {
233
+ const { lines, cursorRow, cursorCol } = state;
234
+ const currentLineText = lines[cursorRow] || "";
235
+ const lineCount = lines.length;
236
+ const pushUndoLocal = pushUndo;
237
+ const currentLine = (r) => state.lines[r] ?? "";
238
+ switch (action.type) {
239
+ case "SET_VIEWPORT":
240
+ return {
241
+ ...state,
242
+ viewportWidth: action.payload.width,
243
+ viewportHeight: action.payload.height,
244
+ };
245
+ case "SET_TEXT": {
246
+ let nextState = state;
247
+ if (action.pushToUndo !== false) {
248
+ nextState = pushUndoLocal(state);
249
+ }
250
+ const newContentLines = action.payload
251
+ .replace(/\r\n?/g, "\n")
252
+ .split("\n");
253
+ const lines = newContentLines.length === 0 ? [""] : newContentLines;
254
+ let newCursorRow;
255
+ let newCursorCol;
256
+ if (typeof action.cursorPosition === "number") {
257
+ [newCursorRow, newCursorCol] = offsetToLogicalPos(action.payload, action.cursorPosition);
258
+ }
259
+ else if (action.cursorPosition === "start") {
260
+ newCursorRow = 0;
261
+ newCursorCol = 0;
262
+ }
263
+ else {
264
+ // Default to 'end'
265
+ newCursorRow = lines.length - 1;
266
+ newCursorCol = cpLen(lines[newCursorRow] ?? "");
267
+ }
268
+ return {
269
+ ...nextState,
270
+ lines,
271
+ cursorRow: newCursorRow,
272
+ cursorCol: newCursorCol,
273
+ preferredCol: null,
274
+ pastedContent: action.payload === "" ? {} : nextState.pastedContent,
275
+ };
276
+ }
277
+ case "INSERT": {
278
+ const nextState = detachExpandedPaste(pushUndoLocal(state));
279
+ const newLines = [...nextState.lines];
280
+ let newCursorRow = nextState.cursorRow;
281
+ let newCursorCol = nextState.cursorCol;
282
+ let payload = action.payload;
283
+ let newPastedContent = nextState.pastedContent;
284
+ if (action.isPaste) {
285
+ payload = payload.replace(/\r\n|\r/g, "\n");
286
+ const lineCount = payload.split("\n").length;
287
+ if (lineCount > LARGE_PASTE_LINE_THRESHOLD ||
288
+ payload.length > LARGE_PASTE_CHAR_THRESHOLD) {
289
+ const id = generatePastedTextId(payload, lineCount, newPastedContent);
290
+ newPastedContent = {
291
+ ...newPastedContent,
292
+ [id]: payload,
293
+ };
294
+ payload = id;
295
+ }
296
+ }
297
+ if (payload.length === 0) {
298
+ return state;
299
+ }
300
+ const str = stripUnsafeCharacters(payload.replace(/\r\n/g, "\n").replace(/\r/g, "\n"));
301
+ const parts = str.split("\n");
302
+ const lineContent = currentLine(newCursorRow);
303
+ const before = cpSlice(lineContent, 0, newCursorCol);
304
+ const after = cpSlice(lineContent, newCursorCol);
305
+ let lineDelta = 0;
306
+ if (parts.length > 1) {
307
+ newLines[newCursorRow] = before + parts[0];
308
+ const remainingParts = parts.slice(1);
309
+ const lastPartOriginal = remainingParts.pop() ?? "";
310
+ newLines.splice(newCursorRow + 1, 0, ...remainingParts);
311
+ newLines.splice(newCursorRow + parts.length - 1, 0, lastPartOriginal + after);
312
+ lineDelta = parts.length - 1;
313
+ newCursorRow = newCursorRow + parts.length - 1;
314
+ newCursorCol = cpLen(lastPartOriginal);
315
+ }
316
+ else {
317
+ newLines[newCursorRow] = before + parts[0] + after;
318
+ newCursorCol = cpLen(before) + cpLen(parts[0]);
319
+ }
320
+ const { newInfo: newExpandedPaste, isDetached } = shiftExpandedRegions(nextState.expandedPaste, nextState.cursorRow, lineDelta);
321
+ if (isDetached && newExpandedPaste === null && nextState.expandedPaste) {
322
+ delete newPastedContent[nextState.expandedPaste.id];
323
+ }
324
+ return {
325
+ ...nextState,
326
+ lines: newLines,
327
+ cursorRow: newCursorRow,
328
+ cursorCol: newCursorCol,
329
+ preferredCol: null,
330
+ pastedContent: newPastedContent,
331
+ expandedPaste: newExpandedPaste,
332
+ };
333
+ }
334
+ case "TOGGLE_PASTE_EXPANSION": {
335
+ const { id, row, col } = action.payload;
336
+ if (state.expandedPaste?.id === id) {
337
+ return {
338
+ ...state,
339
+ expandedPaste: null,
340
+ };
341
+ }
342
+ const transform = getTransformUnderCursor(row, col, state.transformationsByLine, { includeEdge: true });
343
+ if (!transform || transform.type !== "paste" || transform.id !== id) {
344
+ return state;
345
+ }
346
+ const expandedPaste = buildExpandedPasteInfo(state.lines[row] ?? "", row, transform, state.pastedContent);
347
+ if (!expandedPaste) {
348
+ return state;
349
+ }
350
+ return {
351
+ ...state,
352
+ expandedPaste,
353
+ };
354
+ }
355
+ case "NEWLINE": {
356
+ const nextState = pushUndoLocal(state);
357
+ const newLines = [...nextState.lines];
358
+ const before = cpSlice(currentLineText, 0, cursorCol);
359
+ const after = cpSlice(currentLineText, cursorCol);
360
+ newLines[cursorRow] = before;
361
+ newLines.splice(cursorRow + 1, 0, after);
362
+ return {
363
+ ...state,
364
+ lines: newLines,
365
+ cursorRow: cursorRow + 1,
366
+ cursorCol: 0,
367
+ preferredCol: null,
368
+ redoStack: [],
369
+ };
370
+ }
371
+ case "BACKSPACE": {
372
+ if (cursorCol === 0 && cursorRow === 0) {
373
+ return state;
374
+ }
375
+ const nextState = pushUndoLocal(state);
376
+ const newLines = [...nextState.lines];
377
+ if (cursorCol > 0) {
378
+ newLines[cursorRow] =
379
+ cpSlice(currentLineText, 0, cursorCol - 1) +
380
+ cpSlice(currentLineText, cursorCol);
381
+ return {
382
+ ...nextState,
383
+ lines: newLines,
384
+ cursorCol: cursorCol - 1,
385
+ preferredCol: null,
386
+ redoStack: [],
387
+ };
388
+ }
389
+ else {
390
+ const prevLine = lines[cursorRow - 1];
391
+ const prevLen = cpLen(prevLine);
392
+ newLines[cursorRow - 1] = prevLine + currentLineText;
393
+ newLines.splice(cursorRow, 1);
394
+ return {
395
+ ...nextState,
396
+ lines: newLines,
397
+ cursorRow: cursorRow - 1,
398
+ cursorCol: prevLen,
399
+ preferredCol: null,
400
+ redoStack: [],
401
+ };
402
+ }
403
+ }
404
+ case "DELETE": {
405
+ if (cursorCol === cpLen(currentLineText) && cursorRow === lineCount - 1)
406
+ return state;
407
+ const nextState = pushUndoLocal(state);
408
+ const newLines = [...nextState.lines];
409
+ if (cursorCol < cpLen(currentLineText)) {
410
+ newLines[cursorRow] =
411
+ cpSlice(currentLineText, 0, cursorCol) +
412
+ cpSlice(currentLineText, cursorCol + 1);
413
+ }
414
+ else {
415
+ newLines[cursorRow] = currentLineText + lines[cursorRow + 1];
416
+ newLines.splice(cursorRow + 1, 1);
417
+ }
418
+ return {
419
+ ...nextState,
420
+ lines: newLines,
421
+ preferredCol: null,
422
+ redoStack: [],
423
+ };
424
+ }
425
+ case "KILL_LINE_RIGHT": {
426
+ const nextState = pushUndoLocal(state);
427
+ const newLines = [...nextState.lines];
428
+ newLines[cursorRow] = cpSlice(currentLineText, 0, cursorCol);
429
+ return {
430
+ ...nextState,
431
+ lines: newLines,
432
+ preferredCol: null,
433
+ redoStack: [],
434
+ };
435
+ }
436
+ case "KILL_LINE_LEFT": {
437
+ const nextState = pushUndoLocal(state);
438
+ const newLines = [...nextState.lines];
439
+ newLines[cursorRow] = cpSlice(currentLineText, cursorCol);
440
+ return {
441
+ ...nextState,
442
+ lines: newLines,
443
+ cursorCol: 0,
444
+ preferredCol: null,
445
+ redoStack: [],
446
+ };
447
+ }
448
+ case "DELETE_WORD_LEFT": {
449
+ if (cursorCol === 0)
450
+ return state;
451
+ const before = cpSlice(currentLineText, 0, cursorCol);
452
+ const after = cpSlice(currentLineText, cursorCol);
453
+ // Simple word boundary: skip whitespace then skip non-whitespace
454
+ let i = before.length - 1;
455
+ while (i >= 0 && before[i] === " ")
456
+ i--;
457
+ while (i >= 0 && before[i] !== " ")
458
+ i--;
459
+ const nextState = pushUndoLocal(state);
460
+ const newBefore = before.slice(0, i + 1);
461
+ const newLines = [...nextState.lines];
462
+ newLines[cursorRow] = newBefore + after;
463
+ return {
464
+ ...nextState,
465
+ lines: newLines,
466
+ cursorCol: cpLen(newBefore),
467
+ preferredCol: null,
468
+ redoStack: [],
469
+ };
470
+ }
471
+ case "DELETE_WORD_RIGHT": {
472
+ const before = cpSlice(currentLineText, 0, cursorCol);
473
+ const after = cpSlice(currentLineText, cursorCol);
474
+ if (after.length === 0)
475
+ return state;
476
+ let i = 0;
477
+ while (i < after.length && after[i] === " ")
478
+ i++;
479
+ while (i < after.length && after[i] !== " ")
480
+ i++;
481
+ const nextState = pushUndoLocal(state);
482
+ const newAfter = after.slice(i);
483
+ const newLines = [...nextState.lines];
484
+ newLines[cursorRow] = before + newAfter;
485
+ return {
486
+ ...nextState,
487
+ lines: newLines,
488
+ preferredCol: null,
489
+ redoStack: [],
490
+ };
491
+ }
492
+ case "MOVE": {
493
+ const { dir } = action;
494
+ const { visualLayout, preferredCol } = state;
495
+ const [vRow, , vWidth] = calculateVisualCursorFromLayout(visualLayout, [
496
+ cursorRow,
497
+ cursorCol,
498
+ ]);
499
+ if (dir === "left" ||
500
+ dir === "right" ||
501
+ dir === "home" ||
502
+ dir === "end") {
503
+ let r = cursorRow;
504
+ let c = cursorCol;
505
+ if (dir === "left") {
506
+ if (c > 0)
507
+ c--;
508
+ else if (r > 0) {
509
+ r--;
510
+ c = cpLen(lines[r]);
511
+ }
512
+ }
513
+ else if (dir === "right") {
514
+ if (c < cpLen(currentLineText))
515
+ c++;
516
+ else if (r < lineCount - 1) {
517
+ r++;
518
+ c = 0;
519
+ }
520
+ }
521
+ else if (dir === "home")
522
+ c = 0;
523
+ else if (dir === "end")
524
+ c = cpLen(currentLineText);
525
+ return { ...state, cursorRow: r, cursorCol: c, preferredCol: null };
526
+ }
527
+ let newVisRow = vRow;
528
+ const targetWidth = preferredCol !== null ? preferredCol : vWidth;
529
+ if (dir === "up" && newVisRow > 0) {
530
+ newVisRow--;
531
+ }
532
+ else if (dir === "down" &&
533
+ newVisRow < visualLayout.visualLines.length - 1) {
534
+ newVisRow++;
535
+ }
536
+ else {
537
+ return state;
538
+ }
539
+ const newLineText = visualLayout.visualLines[newVisRow] ?? "";
540
+ const codePoints = toCodePoints(newLineText);
541
+ let currentWidth = 0;
542
+ let newVisColIdx = 0;
543
+ for (let i = 0; i < codePoints.length; i++) {
544
+ const charWidth = getCachedStringWidth(codePoints[i]);
545
+ if (currentWidth + charWidth > targetWidth) {
546
+ break;
547
+ }
548
+ currentWidth += charWidth;
549
+ newVisColIdx = i + 1;
550
+ }
551
+ const mapping = visualLayout.visualToLogicalMap[newVisRow];
552
+ if (mapping) {
553
+ return {
554
+ ...state,
555
+ cursorRow: mapping[0],
556
+ cursorCol: mapping[1] + newVisColIdx,
557
+ preferredCol: targetWidth,
558
+ };
559
+ }
560
+ return state;
561
+ }
562
+ case "REPLACE_RANGE": {
563
+ const { startRow, startCol, endRow, endCol, text } = action.payload;
564
+ const nextState = pushUndoLocal(state);
565
+ const newState = replaceRangeInternal(nextState, startRow, startCol, endRow, endCol, text);
566
+ const oldLineCount = endRow - startRow + 1;
567
+ const newLineCount = newState.lines.length - (nextState.lines.length - oldLineCount);
568
+ const lineDelta = newLineCount - oldLineCount;
569
+ const { newInfo: newExpandedPaste, isDetached } = shiftExpandedRegions(nextState.expandedPaste, startRow, lineDelta, endRow);
570
+ const newPastedContent = { ...newState.pastedContent };
571
+ if (isDetached && nextState.expandedPaste) {
572
+ delete newPastedContent[nextState.expandedPaste.id];
573
+ }
574
+ return {
575
+ ...newState,
576
+ pastedContent: newPastedContent,
577
+ expandedPaste: newExpandedPaste,
578
+ };
579
+ }
580
+ case "UNDO": {
581
+ const stateToRestore = state.undoStack[state.undoStack.length - 1];
582
+ if (!stateToRestore)
583
+ return state;
584
+ const currentSnapshot = {
585
+ lines: [...state.lines],
586
+ cursorRow: state.cursorRow,
587
+ cursorCol: state.cursorCol,
588
+ pastedContent: { ...state.pastedContent },
589
+ expandedPaste: state.expandedPaste ? { ...state.expandedPaste } : null,
590
+ };
591
+ return {
592
+ ...state,
593
+ ...stateToRestore,
594
+ undoStack: state.undoStack.slice(0, -1),
595
+ redoStack: [...state.redoStack, currentSnapshot],
596
+ };
597
+ }
598
+ case "REDO": {
599
+ const stateToRestore = state.redoStack[state.redoStack.length - 1];
600
+ if (!stateToRestore)
601
+ return state;
602
+ const currentSnapshot = {
603
+ lines: [...state.lines],
604
+ cursorRow: state.cursorRow,
605
+ cursorCol: state.cursorCol,
606
+ pastedContent: { ...state.pastedContent },
607
+ expandedPaste: state.expandedPaste ? { ...state.expandedPaste } : null,
608
+ };
609
+ return {
610
+ ...state,
611
+ ...stateToRestore,
612
+ redoStack: state.redoStack.slice(0, -1),
613
+ undoStack: [...state.undoStack, currentSnapshot],
614
+ };
615
+ }
616
+ default:
617
+ return state;
618
+ }
619
+ }
620
+ const transformationsCache = new LRUCache(LRU_BUFFER_PERF_CACHE_LIMIT);
621
+ export function calculateTransformationsForLine(line, pastedContent = {}, expandedPaste = null, lineIndex) {
622
+ const hasPastePlaceholder = line.includes("[Pasted Text:");
623
+ const shouldBypassCache = hasPastePlaceholder &&
624
+ expandedPaste !== null &&
625
+ lineIndex === expandedPaste.startLine;
626
+ const cached = !shouldBypassCache
627
+ ? transformationsCache.get(line)
628
+ : undefined;
629
+ if (cached !== undefined) {
630
+ return cached;
631
+ }
632
+ const transformations = [];
633
+ const pasteRegex = new RegExp(PASTED_TEXT_PLACEHOLDER_REGEX.source, "g");
634
+ let match;
635
+ match = pasteRegex.exec(line);
636
+ while (match !== null) {
637
+ const logicalText = match[0];
638
+ const logStart = cpLen(line.substring(0, match.index));
639
+ const logEnd = logStart + cpLen(logicalText);
640
+ const isExpanded = expandedPaste?.id === logicalText &&
641
+ lineIndex === expandedPaste.startLine;
642
+ transformations.push({
643
+ logStart,
644
+ logEnd,
645
+ logicalText,
646
+ collapsedText: logicalText,
647
+ expandedText: isExpanded
648
+ ? normalizePasteContent(pastedContent[logicalText] ?? logicalText)
649
+ : undefined,
650
+ type: "paste",
651
+ id: logicalText,
652
+ });
653
+ match = pasteRegex.exec(line);
654
+ }
655
+ transformations.sort((a, b) => a.logStart - b.logStart);
656
+ if (!shouldBypassCache) {
657
+ transformationsCache.set(line, transformations);
658
+ }
659
+ return transformations;
660
+ }
661
+ export function calculateTransformedLine(logLine, logIndex, logicalCursor, transformations, expandedPaste = null) {
662
+ let transformedLine = "";
663
+ const transformedToLogMap = [];
664
+ let lastLogPos = 0;
665
+ const cursorIsOnThisLine = logIndex === logicalCursor[0];
666
+ const cursorCol = logicalCursor[1];
667
+ for (const transform of transformations) {
668
+ const textBeforeTransformation = cpSlice(logLine, lastLogPos, transform.logStart);
669
+ transformedLine += textBeforeTransformation;
670
+ for (let i = 0; i < cpLen(textBeforeTransformation); i++) {
671
+ transformedToLogMap.push(lastLogPos + i);
672
+ }
673
+ const isExpanded = (transform.type === "image" &&
674
+ cursorIsOnThisLine &&
675
+ cursorCol >= transform.logStart &&
676
+ cursorCol <= transform.logEnd) ||
677
+ (transform.type === "paste" &&
678
+ transform.id === expandedPaste?.id &&
679
+ logIndex === expandedPaste?.startLine);
680
+ const transformedText = isExpanded
681
+ ? (transform.expandedText ?? transform.logicalText)
682
+ : transform.collapsedText;
683
+ transformedLine += transformedText;
684
+ const transformedCodePoints = toCodePoints(transformedText);
685
+ const transformedLen = transformedCodePoints.length;
686
+ if (isExpanded) {
687
+ const logicalLength = Math.max(0, transform.logEnd - transform.logStart);
688
+ const visibleCodePointCount = transformedCodePoints.filter((codePoint) => codePoint !== "\n").length;
689
+ let visibleCodePointIndex = 0;
690
+ for (const codePoint of transformedCodePoints) {
691
+ if (codePoint === "\n") {
692
+ transformedToLogMap.push(transform.logEnd);
693
+ continue;
694
+ }
695
+ const transformationToLogicalOffset = visibleCodePointCount === 0 || logicalLength === 0
696
+ ? 0
697
+ : Math.floor((visibleCodePointIndex * logicalLength) / visibleCodePointCount);
698
+ transformedToLogMap.push(transform.logStart +
699
+ Math.min(transformationToLogicalOffset, Math.max(logicalLength - 1, 0)));
700
+ visibleCodePointIndex++;
701
+ }
702
+ }
703
+ else {
704
+ const logicalLength = Math.max(0, transform.logEnd - transform.logStart);
705
+ for (let i = 0; i < transformedLen; i++) {
706
+ const transformationToLogicalOffset = logicalLength === 0
707
+ ? 0
708
+ : Math.floor((i * logicalLength) / transformedLen);
709
+ const transformationToLogicalIndex = transform.logStart +
710
+ Math.min(transformationToLogicalOffset, Math.max(logicalLength - 1, 0));
711
+ transformedToLogMap.push(transformationToLogicalIndex);
712
+ }
713
+ }
714
+ lastLogPos = transform.logEnd;
715
+ }
716
+ // Append text after last transform
717
+ const remainingUntransformedText = cpSlice(logLine, lastLogPos);
718
+ transformedLine += remainingUntransformedText;
719
+ for (let i = 0; i < cpLen(remainingUntransformedText); i++) {
720
+ transformedToLogMap.push(lastLogPos + i);
721
+ }
722
+ // For a cursor at the very end of the transformed line
723
+ transformedToLogMap.push(cpLen(logLine));
724
+ return { transformedLine, transformedToLogMap };
725
+ }
726
+ function layoutWrappedLine(codePointsInLine, transformedToLogMap, viewportWidth, logIndex) {
727
+ const lineVisualLines = [];
728
+ const lineLogicalToVisualMap = [];
729
+ const lineVisualToLogicalMap = [];
730
+ const lineVisualToTransformedMap = [];
731
+ if (codePointsInLine.length === 0) {
732
+ lineLogicalToVisualMap.push([0, transformedToLogMap[0] ?? 0]);
733
+ lineVisualToLogicalMap.push([logIndex, transformedToLogMap[0] ?? 0]);
734
+ lineVisualToTransformedMap.push(0);
735
+ lineVisualLines.push("");
736
+ return {
737
+ visualLines: lineVisualLines,
738
+ logicalToVisualMap: lineLogicalToVisualMap,
739
+ visualToLogicalMap: lineVisualToLogicalMap,
740
+ visualToTransformedMap: lineVisualToTransformedMap,
741
+ };
742
+ }
743
+ let currentPosInLine = 0;
744
+ while (currentPosInLine < codePointsInLine.length) {
745
+ let currentChunk = "";
746
+ let currentChunkVisualWidth = 0;
747
+ let numCodePointsInChunk = 0;
748
+ let lastWordBreakPoint = -1;
749
+ let numCodePointsAtLastWordBreak = 0;
750
+ for (let i = currentPosInLine; i < codePointsInLine.length; i++) {
751
+ const char = codePointsInLine[i];
752
+ const charVisualWidth = getCachedStringWidth(char);
753
+ if (currentChunkVisualWidth + charVisualWidth > viewportWidth) {
754
+ if (lastWordBreakPoint !== -1 &&
755
+ numCodePointsAtLastWordBreak > 0 &&
756
+ currentPosInLine + numCodePointsAtLastWordBreak < i) {
757
+ currentChunk = codePointsInLine
758
+ .slice(currentPosInLine, currentPosInLine + numCodePointsAtLastWordBreak)
759
+ .join("");
760
+ numCodePointsInChunk = numCodePointsAtLastWordBreak;
761
+ }
762
+ else if (numCodePointsInChunk === 0 &&
763
+ charVisualWidth > viewportWidth) {
764
+ currentChunk = char;
765
+ numCodePointsInChunk = 1;
766
+ }
767
+ break;
768
+ }
769
+ currentChunk += char;
770
+ currentChunkVisualWidth += charVisualWidth;
771
+ numCodePointsInChunk++;
772
+ if (char === " ") {
773
+ lastWordBreakPoint = i;
774
+ numCodePointsAtLastWordBreak = numCodePointsInChunk - 1;
775
+ }
776
+ }
777
+ if (numCodePointsInChunk === 0 &&
778
+ currentPosInLine < codePointsInLine.length) {
779
+ currentChunk = codePointsInLine[currentPosInLine] ?? "";
780
+ numCodePointsInChunk = 1;
781
+ }
782
+ const logicalStartCol = transformedToLogMap[currentPosInLine] ?? 0;
783
+ lineLogicalToVisualMap.push([lineVisualLines.length, logicalStartCol]);
784
+ lineVisualToLogicalMap.push([logIndex, logicalStartCol]);
785
+ lineVisualToTransformedMap.push(currentPosInLine);
786
+ lineVisualLines.push(currentChunk);
787
+ const logicalStartOfThisChunk = currentPosInLine;
788
+ currentPosInLine += numCodePointsInChunk;
789
+ if (logicalStartOfThisChunk + numCodePointsInChunk <
790
+ codePointsInLine.length &&
791
+ currentPosInLine < codePointsInLine.length &&
792
+ codePointsInLine[currentPosInLine] === " ") {
793
+ currentPosInLine++;
794
+ }
795
+ }
796
+ return {
797
+ visualLines: lineVisualLines,
798
+ logicalToVisualMap: lineLogicalToVisualMap,
799
+ visualToLogicalMap: lineVisualToLogicalMap,
800
+ visualToTransformedMap: lineVisualToTransformedMap,
801
+ };
802
+ }
803
+ function calculateLayout(logicalLines, viewportWidth, logicalCursor, pastedContent, expandedPaste) {
804
+ const visualLines = [];
805
+ const logicalToVisualMap = [];
806
+ const visualToLogicalMap = [];
807
+ const transformedToLogicalMaps = [];
808
+ const visualToTransformedMap = [];
809
+ logicalLines.forEach((logLine, logIndex) => {
810
+ logicalToVisualMap[logIndex] = [];
811
+ const isCursorOnLine = logIndex === logicalCursor[0];
812
+ const isExpandedLine = expandedPaste?.startLine === logIndex;
813
+ const cacheKey = getLineLayoutCacheKey(logLine, viewportWidth, isCursorOnLine, logicalCursor[1]);
814
+ const cached = isExpandedLine ? undefined : lineLayoutCache.get(cacheKey);
815
+ if (cached) {
816
+ const visualLineOffset = visualLines.length;
817
+ visualLines.push(...cached.visualLines);
818
+ cached.logicalToVisualMap.forEach(([relVisualIdx, logCol]) => {
819
+ logicalToVisualMap[logIndex].push([
820
+ visualLineOffset + relVisualIdx,
821
+ logCol,
822
+ ]);
823
+ });
824
+ cached.visualToLogicalMap.forEach(([, logCol]) => {
825
+ visualToLogicalMap.push([logIndex, logCol]);
826
+ });
827
+ transformedToLogicalMaps[logIndex] = cached.transformedToLogMap;
828
+ visualToTransformedMap.push(...cached.visualToTransformedMap);
829
+ return;
830
+ }
831
+ const transformations = calculateTransformationsForLine(logLine, pastedContent, expandedPaste, logIndex);
832
+ const { transformedLine, transformedToLogMap } = calculateTransformedLine(logLine, logIndex, logicalCursor, transformations, expandedPaste);
833
+ const lineVisualLines = [];
834
+ const lineLogicalToVisualMap = [];
835
+ const lineVisualToLogicalMap = [];
836
+ const lineVisualToTransformedMap = [];
837
+ if (transformedLine.length === 0) {
838
+ lineLogicalToVisualMap.push([0, 0]);
839
+ lineVisualToLogicalMap.push([logIndex, 0]);
840
+ lineVisualToTransformedMap.push(0);
841
+ lineVisualLines.push("");
842
+ }
843
+ else {
844
+ const transformedCodePoints = toCodePoints(transformedLine);
845
+ let segmentStart = 0;
846
+ for (let i = 0; i <= transformedCodePoints.length; i++) {
847
+ const isLineBreak = i === transformedCodePoints.length ||
848
+ transformedCodePoints[i] === "\n";
849
+ if (!isLineBreak) {
850
+ continue;
851
+ }
852
+ const subLineCodePoints = transformedCodePoints.slice(segmentStart, i);
853
+ const subLineMap = transformedToLogMap.slice(segmentStart, i + 1);
854
+ const segmentLayout = layoutWrappedLine(subLineCodePoints, subLineMap, viewportWidth, logIndex);
855
+ segmentLayout.logicalToVisualMap.forEach(([visualIndex, logicalCol]) => {
856
+ lineLogicalToVisualMap.push([
857
+ lineVisualLines.length + visualIndex,
858
+ logicalCol,
859
+ ]);
860
+ });
861
+ lineVisualToLogicalMap.push(...segmentLayout.visualToLogicalMap);
862
+ segmentLayout.visualToTransformedMap.forEach((transformedIndex) => {
863
+ lineVisualToTransformedMap.push(segmentStart + transformedIndex);
864
+ });
865
+ lineVisualLines.push(...segmentLayout.visualLines);
866
+ segmentStart = i + 1;
867
+ }
868
+ }
869
+ if (!isExpandedLine) {
870
+ lineLayoutCache.set(cacheKey, {
871
+ visualLines: lineVisualLines,
872
+ logicalToVisualMap: lineLogicalToVisualMap,
873
+ visualToLogicalMap: lineVisualToLogicalMap,
874
+ transformedToLogMap,
875
+ visualToTransformedMap: lineVisualToTransformedMap,
876
+ });
877
+ }
878
+ const visualLineOffset = visualLines.length;
879
+ visualLines.push(...lineVisualLines);
880
+ lineLogicalToVisualMap.forEach(([relVisualIdx, logCol]) => {
881
+ logicalToVisualMap[logIndex].push([
882
+ visualLineOffset + relVisualIdx,
883
+ logCol,
884
+ ]);
885
+ });
886
+ lineVisualToLogicalMap.forEach(([, logCol]) => {
887
+ visualToLogicalMap.push([logIndex, logCol]);
888
+ });
889
+ transformedToLogicalMaps[logIndex] = transformedToLogMap;
890
+ visualToTransformedMap.push(...lineVisualToTransformedMap);
891
+ });
892
+ // If the entire logical text was empty, ensure there's one empty visual line.
893
+ if (logicalLines.length === 0 ||
894
+ (logicalLines.length === 1 && logicalLines[0] === "")) {
895
+ if (visualLines.length === 0) {
896
+ visualLines.push("");
897
+ if (!logicalToVisualMap[0])
898
+ logicalToVisualMap[0] = [];
899
+ logicalToVisualMap[0].push([0, 0]);
900
+ visualToLogicalMap.push([0, 0]);
901
+ visualToTransformedMap.push(0);
902
+ }
903
+ }
904
+ return {
905
+ visualLines,
906
+ logicalToVisualMap,
907
+ visualToLogicalMap,
908
+ transformedToLogicalMaps,
909
+ visualToTransformedMap,
910
+ };
911
+ }
912
+ export function getTransformUnderCursor(row, col, spansByLine, options = {}) {
913
+ const spans = spansByLine[row];
914
+ if (!spans || spans.length === 0)
915
+ return null;
916
+ for (const span of spans) {
917
+ if (col >= span.logStart &&
918
+ (options.includeEdge ? col <= span.logEnd : col < span.logEnd)) {
919
+ return span;
920
+ }
921
+ if (col < span.logStart)
922
+ break;
923
+ }
924
+ return null;
925
+ }
926
+ export function calculateTransformations(lines, pastedContent = {}, expandedPaste = null) {
927
+ return lines.map((line, index) => calculateTransformationsForLine(line, pastedContent, expandedPaste, index));
928
+ }
929
+ function textBufferReducer(state, action) {
930
+ const newState = bufferReducerLogic(state, action);
931
+ const newTransformedLines = newState.lines !== state.lines ||
932
+ newState.pastedContent !== state.pastedContent ||
933
+ newState.expandedPaste !== state.expandedPaste
934
+ ? calculateTransformations(newState.lines, newState.pastedContent, newState.expandedPaste)
935
+ : state.transformationsByLine;
936
+ const oldTransform = getTransformUnderCursor(state.cursorRow, state.cursorCol, state.transformationsByLine);
937
+ const newTransform = getTransformUnderCursor(newState.cursorRow, newState.cursorCol, newTransformedLines);
938
+ const oldInside = oldTransform !== null;
939
+ const newInside = newTransform !== null;
940
+ const movedBetweenTransforms = oldTransform !== newTransform &&
941
+ (oldTransform !== null || newTransform !== null);
942
+ if (newState.lines !== state.lines ||
943
+ newState.pastedContent !== state.pastedContent ||
944
+ newState.expandedPaste !== state.expandedPaste ||
945
+ newState.viewportWidth !== state.viewportWidth ||
946
+ oldInside !== newInside ||
947
+ movedBetweenTransforms) {
948
+ const shouldResetPreferred = oldInside !== newInside || movedBetweenTransforms;
949
+ return {
950
+ ...newState,
951
+ preferredCol: shouldResetPreferred ? null : newState.preferredCol,
952
+ visualLayout: calculateLayout(newState.lines, newState.viewportWidth, [newState.cursorRow, newState.cursorCol], newState.pastedContent, newState.expandedPaste),
953
+ transformationsByLine: newTransformedLines,
954
+ };
955
+ }
956
+ return newState;
957
+ }
958
+ export function useTextBuffer({ initialText = "", viewportWidth = 80, viewportHeight = 10, initialCursorOffset = 0, }) {
959
+ const initialState = useMemo(() => {
960
+ const lines = initialText.split("\n");
961
+ const [initialCursorRow, initialCursorCol] = calculateInitialCursorPosition(lines.length === 0 ? [""] : lines, initialCursorOffset);
962
+ const transformationsByLine = calculateTransformations(lines.length === 0 ? [""] : lines, {}, null);
963
+ const visualLayout = calculateLayout(lines.length === 0 ? [""] : lines, viewportWidth, [initialCursorRow, initialCursorCol], {}, null);
964
+ return {
965
+ lines: lines.length === 0 ? [""] : lines,
966
+ cursorRow: initialCursorRow,
967
+ cursorCol: initialCursorCol,
968
+ transformationsByLine,
969
+ preferredCol: null,
970
+ undoStack: [],
971
+ redoStack: [],
972
+ viewportWidth,
973
+ viewportHeight,
974
+ visualLayout,
975
+ pastedContent: {},
976
+ expandedPaste: null,
977
+ visualScrollRow: 0,
978
+ };
979
+ }, [initialText, initialCursorOffset, viewportWidth, viewportHeight]);
980
+ const [state, dispatch] = useReducer(textBufferReducer, initialState);
981
+ useEffect(() => {
982
+ dispatch({
983
+ type: "SET_VIEWPORT",
984
+ payload: { width: viewportWidth, height: viewportHeight },
985
+ });
986
+ }, [viewportWidth, viewportHeight]);
987
+ const insert = useCallback((ch, { paste = false } = {}) => {
988
+ if (typeof ch !== "string") {
989
+ return;
990
+ }
991
+ const textToInsert = ch;
992
+ let currentText = "";
993
+ for (const char of toCodePoints(textToInsert)) {
994
+ if (char.codePointAt(0) === 127) {
995
+ if (currentText.length > 0) {
996
+ dispatch({ type: "INSERT", payload: currentText, isPaste: paste });
997
+ currentText = "";
998
+ }
999
+ dispatch({ type: "BACKSPACE" });
1000
+ }
1001
+ else {
1002
+ currentText += char;
1003
+ }
1004
+ }
1005
+ if (currentText.length > 0) {
1006
+ dispatch({ type: "INSERT", payload: currentText, isPaste: paste });
1007
+ }
1008
+ }, []);
1009
+ const setText = useCallback((text, cursorPosition) => dispatch({ type: "SET_TEXT", payload: text, cursorPosition }), []);
1010
+ const backspace = useCallback(() => dispatch({ type: "BACKSPACE" }), []);
1011
+ const deleteChar = useCallback(() => dispatch({ type: "DELETE" }), []);
1012
+ const newline = useCallback(() => dispatch({ type: "NEWLINE" }), []);
1013
+ const killLineRight = useCallback(() => dispatch({ type: "KILL_LINE_RIGHT" }), []);
1014
+ const killLineLeft = useCallback(() => dispatch({ type: "KILL_LINE_LEFT" }), []);
1015
+ const deleteWordLeft = useCallback(() => dispatch({ type: "DELETE_WORD_LEFT" }), []);
1016
+ const deleteWordRight = useCallback(() => dispatch({ type: "DELETE_WORD_RIGHT" }), []);
1017
+ const move = useCallback((dir) => dispatch({ type: "MOVE", dir }), []);
1018
+ const undo = useCallback(() => dispatch({ type: "UNDO" }), []);
1019
+ const redo = useCallback(() => dispatch({ type: "REDO" }), []);
1020
+ const replaceRange = useCallback((startRow, startCol, endRow, endCol, text) => dispatch({
1021
+ type: "REPLACE_RANGE",
1022
+ payload: { startRow, startCol, endRow, endCol, text },
1023
+ }), []);
1024
+ const getOffset = useCallback(() => logicalPosToOffset(state.lines, state.cursorRow, state.cursorCol), [state.lines, state.cursorRow, state.cursorCol]);
1025
+ const togglePasteExpansion = useCallback((id, row, col) => dispatch({
1026
+ type: "TOGGLE_PASTE_EXPANSION",
1027
+ payload: { id, row, col },
1028
+ }), []);
1029
+ const handleInput = useCallback((key) => {
1030
+ const { sequence: input } = key;
1031
+ if (key.name === "backspace") {
1032
+ backspace();
1033
+ return true;
1034
+ }
1035
+ if (key.name === "delete") {
1036
+ deleteChar();
1037
+ return true;
1038
+ }
1039
+ if (key.name === "left") {
1040
+ move("left");
1041
+ return true;
1042
+ }
1043
+ if (key.name === "right") {
1044
+ move("right");
1045
+ return true;
1046
+ }
1047
+ if (key.name === "up") {
1048
+ move("up");
1049
+ return true;
1050
+ }
1051
+ if (key.name === "down") {
1052
+ move("down");
1053
+ return true;
1054
+ }
1055
+ if (key.name === "home") {
1056
+ move("home");
1057
+ return true;
1058
+ }
1059
+ if (key.name === "end") {
1060
+ move("end");
1061
+ return true;
1062
+ }
1063
+ if (key.name === "enter" && key.shift) {
1064
+ newline();
1065
+ return true;
1066
+ }
1067
+ if (key.name === "paste") {
1068
+ insert(input, { paste: true });
1069
+ return true;
1070
+ }
1071
+ if (key.insertable) {
1072
+ insert(input, { paste: false });
1073
+ return true;
1074
+ }
1075
+ return false;
1076
+ }, [backspace, deleteChar, move, insert, newline]);
1077
+ const [vRow, vColIdx, vWidth] = useMemo(() => calculateVisualCursorFromLayout(state.visualLayout, [
1078
+ state.cursorRow,
1079
+ state.cursorCol,
1080
+ ]), [state.visualLayout, state.cursorRow, state.cursorCol]);
1081
+ const visualCursor = useMemo(() => [vRow, vWidth], [vRow, vWidth]);
1082
+ const visualCursorColIndex = vColIdx;
1083
+ const viewportVisualLines = useMemo(() => state.visualLayout.visualLines.slice(state.visualScrollRow, state.visualScrollRow + state.viewportHeight), [
1084
+ state.visualLayout.visualLines,
1085
+ state.visualScrollRow,
1086
+ state.viewportHeight,
1087
+ ]);
1088
+ return useMemo(() => ({
1089
+ lines: state.lines,
1090
+ text: state.lines.join("\n"),
1091
+ cursor: [state.cursorRow, state.cursorCol],
1092
+ preferredCol: state.preferredCol,
1093
+ selectionAnchor: null,
1094
+ pastedContent: state.pastedContent,
1095
+ // Visual / Viewport properties
1096
+ allVisualLines: state.visualLayout.visualLines,
1097
+ viewportVisualLines,
1098
+ visualCursor,
1099
+ visualCursorColIndex,
1100
+ visualCursorColWidth: vWidth,
1101
+ visualScrollRow: state.visualScrollRow,
1102
+ viewportHeight: state.viewportHeight,
1103
+ visualToLogicalMap: state.visualLayout.visualToLogicalMap,
1104
+ transformedToLogicalMaps: state.visualLayout.transformedToLogicalMaps,
1105
+ transformationsByLine: state.transformationsByLine,
1106
+ visualToTransformedMap: state.visualLayout.visualToTransformedMap,
1107
+ visualLayout: state.visualLayout,
1108
+ // Core actions
1109
+ insert,
1110
+ setText,
1111
+ newline,
1112
+ backspace,
1113
+ del: deleteChar,
1114
+ move,
1115
+ undo,
1116
+ redo,
1117
+ replaceRange,
1118
+ deleteWordLeft,
1119
+ deleteWordRight,
1120
+ killLineRight,
1121
+ killLineLeft,
1122
+ handleInput,
1123
+ getOffset,
1124
+ replaceRangeByOffset: (_s, _e, _t) => { },
1125
+ moveToOffset: (_offset) => { },
1126
+ moveToVisualPosition: (_vr, _vc) => { },
1127
+ getLogicalPositionFromVisual: (_vr, _vc) => null,
1128
+ getExpandedPasteAtLine: (lineIndex) => getExpandedPasteAtLine(lineIndex, state.expandedPaste),
1129
+ togglePasteExpansion,
1130
+ }), [
1131
+ state,
1132
+ viewportVisualLines,
1133
+ visualCursor,
1134
+ visualCursorColIndex,
1135
+ vWidth,
1136
+ insert,
1137
+ setText,
1138
+ newline,
1139
+ backspace,
1140
+ deleteChar,
1141
+ move,
1142
+ undo,
1143
+ redo,
1144
+ replaceRange,
1145
+ deleteWordLeft,
1146
+ deleteWordRight,
1147
+ killLineRight,
1148
+ killLineLeft,
1149
+ handleInput,
1150
+ getOffset,
1151
+ togglePasteExpansion,
1152
+ ]);
1153
+ }
1154
+ //# sourceMappingURL=textBuffer.js.map