@pierre/diffs 1.3.0-beta.9 → 1.3.0-rc.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.
Files changed (219) hide show
  1. package/dist/components/CodeView.d.ts +12 -12
  2. package/dist/components/CodeView.d.ts.map +1 -1
  3. package/dist/components/CodeView.js +49 -42
  4. package/dist/components/CodeView.js.map +1 -1
  5. package/dist/components/File.d.ts +7 -2
  6. package/dist/components/File.d.ts.map +1 -1
  7. package/dist/components/File.js +36 -8
  8. package/dist/components/File.js.map +1 -1
  9. package/dist/components/FileDiff.d.ts +34 -6
  10. package/dist/components/FileDiff.d.ts.map +1 -1
  11. package/dist/components/FileDiff.js +198 -17
  12. package/dist/components/FileDiff.js.map +1 -1
  13. package/dist/components/VirtualizedFile.d.ts +2 -1
  14. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  15. package/dist/components/VirtualizedFile.js +9 -5
  16. package/dist/components/VirtualizedFile.js.map +1 -1
  17. package/dist/components/VirtualizedFileDiff.d.ts +14 -0
  18. package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
  19. package/dist/components/VirtualizedFileDiff.js +61 -3
  20. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  21. package/dist/components/Virtualizer.d.ts +14 -2
  22. package/dist/components/Virtualizer.d.ts.map +1 -1
  23. package/dist/components/Virtualizer.js +27 -2
  24. package/dist/components/Virtualizer.js.map +1 -1
  25. package/dist/edit/index.d.ts +5 -0
  26. package/dist/edit/index.js +3 -0
  27. package/dist/editor/command.d.ts +1 -1
  28. package/dist/editor/command.d.ts.map +1 -1
  29. package/dist/editor/command.js +11 -0
  30. package/dist/editor/command.js.map +1 -1
  31. package/dist/editor/editStack.d.ts +6 -2
  32. package/dist/editor/editStack.d.ts.map +1 -1
  33. package/dist/editor/editStack.js +53 -12
  34. package/dist/editor/editStack.js.map +1 -1
  35. package/dist/editor/editor.d.ts +51 -23
  36. package/dist/editor/editor.d.ts.map +1 -1
  37. package/dist/editor/editor.js +978 -238
  38. package/dist/editor/editor.js.map +1 -1
  39. package/dist/editor/editor2.js +1 -1
  40. package/dist/editor/languages.d.ts +26 -0
  41. package/dist/editor/languages.d.ts.map +1 -0
  42. package/dist/editor/languages.js +363 -0
  43. package/dist/editor/languages.js.map +1 -0
  44. package/dist/editor/lineAnnotations.d.ts.map +1 -1
  45. package/dist/editor/lineAnnotations.js +25 -26
  46. package/dist/editor/lineAnnotations.js.map +1 -1
  47. package/dist/editor/marker.d.ts +5 -3
  48. package/dist/editor/marker.d.ts.map +1 -1
  49. package/dist/editor/marker.js +111 -91
  50. package/dist/editor/marker.js.map +1 -1
  51. package/dist/editor/matchBrackets.d.ts +3 -2
  52. package/dist/editor/matchBrackets.d.ts.map +1 -1
  53. package/dist/editor/matchBrackets.js +2 -10
  54. package/dist/editor/matchBrackets.js.map +1 -1
  55. package/dist/editor/pieceTable.d.ts +2 -1
  56. package/dist/editor/pieceTable.d.ts.map +1 -1
  57. package/dist/editor/pieceTable.js +103 -39
  58. package/dist/editor/pieceTable.js.map +1 -1
  59. package/dist/editor/platform.js +2 -0
  60. package/dist/editor/platform.js.map +1 -1
  61. package/dist/editor/popover.d.ts +25 -5
  62. package/dist/editor/popover.d.ts.map +1 -1
  63. package/dist/editor/popover.js +60 -14
  64. package/dist/editor/popover.js.map +1 -1
  65. package/dist/editor/searchPanel.d.ts.map +1 -1
  66. package/dist/editor/searchPanel.js +9 -2
  67. package/dist/editor/searchPanel.js.map +1 -1
  68. package/dist/editor/selection.d.ts +31 -11
  69. package/dist/editor/selection.d.ts.map +1 -1
  70. package/dist/editor/selection.js +198 -70
  71. package/dist/editor/selection.js.map +1 -1
  72. package/dist/editor/selectionAction.d.ts +4 -3
  73. package/dist/editor/selectionAction.d.ts.map +1 -1
  74. package/dist/editor/selectionAction.js +9 -5
  75. package/dist/editor/selectionAction.js.map +1 -1
  76. package/dist/editor/stateStorage.d.ts +15 -0
  77. package/dist/editor/stateStorage.d.ts.map +1 -0
  78. package/dist/editor/stateStorage.js +65 -0
  79. package/dist/editor/stateStorage.js.map +1 -0
  80. package/dist/editor/textDocument.d.ts +12 -77
  81. package/dist/editor/textDocument.d.ts.map +1 -1
  82. package/dist/editor/textDocument.js +76 -34
  83. package/dist/editor/textDocument.js.map +1 -1
  84. package/dist/editor/textMeasure.d.ts +7 -12
  85. package/dist/editor/textMeasure.d.ts.map +1 -1
  86. package/dist/editor/textMeasure.js +40 -32
  87. package/dist/editor/textMeasure.js.map +1 -1
  88. package/dist/editor/{tokenzier.d.ts → tokenizer.d.ts} +3 -3
  89. package/dist/editor/tokenizer.d.ts.map +1 -0
  90. package/dist/editor/{tokenzier.js → tokenizer.js} +91 -26
  91. package/dist/editor/tokenizer.js.map +1 -0
  92. package/dist/editor/utils.js +1 -1
  93. package/dist/editor/utils.js.map +1 -1
  94. package/dist/index.d.ts +4 -3
  95. package/dist/index.js +6 -5
  96. package/dist/managers/InteractionManager.d.ts +18 -7
  97. package/dist/managers/InteractionManager.d.ts.map +1 -1
  98. package/dist/managers/InteractionManager.js +154 -99
  99. package/dist/managers/InteractionManager.js.map +1 -1
  100. package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
  101. package/dist/managers/UniversalRenderingManager.js +1 -2
  102. package/dist/managers/UniversalRenderingManager.js.map +1 -1
  103. package/dist/react/CodeView.d.ts +13 -13
  104. package/dist/react/CodeView.d.ts.map +1 -1
  105. package/dist/react/CodeView.js +38 -29
  106. package/dist/react/CodeView.js.map +1 -1
  107. package/dist/react/EditContext.d.ts +20 -0
  108. package/dist/react/EditContext.d.ts.map +1 -0
  109. package/dist/react/EditContext.js +20 -0
  110. package/dist/react/EditContext.js.map +1 -0
  111. package/dist/react/File.d.ts +2 -1
  112. package/dist/react/File.d.ts.map +1 -1
  113. package/dist/react/File.js +3 -2
  114. package/dist/react/File.js.map +1 -1
  115. package/dist/react/FileDiff.d.ts +2 -2
  116. package/dist/react/FileDiff.d.ts.map +1 -1
  117. package/dist/react/FileDiff.js +3 -2
  118. package/dist/react/FileDiff.js.map +1 -1
  119. package/dist/react/MultiFileDiff.d.ts +2 -2
  120. package/dist/react/MultiFileDiff.d.ts.map +1 -1
  121. package/dist/react/MultiFileDiff.js +3 -2
  122. package/dist/react/MultiFileDiff.js.map +1 -1
  123. package/dist/react/PatchDiff.d.ts +2 -2
  124. package/dist/react/PatchDiff.d.ts.map +1 -1
  125. package/dist/react/PatchDiff.js +3 -2
  126. package/dist/react/PatchDiff.js.map +1 -1
  127. package/dist/react/UnresolvedFile.d.ts +1 -1
  128. package/dist/react/UnresolvedFile.js.map +1 -1
  129. package/dist/react/index.d.ts +4 -4
  130. package/dist/react/index.js +3 -3
  131. package/dist/react/types.d.ts +9 -1
  132. package/dist/react/types.d.ts.map +1 -1
  133. package/dist/react/utils/useFileDiffInstance.d.ts +5 -3
  134. package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
  135. package/dist/react/utils/useFileDiffInstance.js +28 -27
  136. package/dist/react/utils/useFileDiffInstance.js.map +1 -1
  137. package/dist/react/utils/useFileInstance.d.ts +5 -3
  138. package/dist/react/utils/useFileInstance.d.ts.map +1 -1
  139. package/dist/react/utils/useFileInstance.js +29 -25
  140. package/dist/react/utils/useFileInstance.js.map +1 -1
  141. package/dist/react/utils/useUnresolvedFileInstance.js +3 -3
  142. package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
  143. package/dist/renderers/DiffHunksRenderer.d.ts +33 -1
  144. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  145. package/dist/renderers/DiffHunksRenderer.js +134 -21
  146. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  147. package/dist/renderers/FileRenderer.d.ts +2 -0
  148. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  149. package/dist/renderers/FileRenderer.js +33 -11
  150. package/dist/renderers/FileRenderer.js.map +1 -1
  151. package/dist/ssr/FileDiffReact.js +1 -1
  152. package/dist/ssr/index.d.ts +2 -2
  153. package/dist/ssr/preloadDiffs.js +1 -1
  154. package/dist/style.js +1 -1
  155. package/dist/types.d.ts +185 -42
  156. package/dist/types.d.ts.map +1 -1
  157. package/dist/utils/annotationHelpers.d.ts +20 -0
  158. package/dist/utils/annotationHelpers.d.ts.map +1 -0
  159. package/dist/utils/annotationHelpers.js +29 -0
  160. package/dist/utils/annotationHelpers.js.map +1 -0
  161. package/dist/utils/computeFileOffsets.d.ts.map +1 -1
  162. package/dist/utils/computeFileOffsets.js +34 -5
  163. package/dist/utils/computeFileOffsets.js.map +1 -1
  164. package/dist/utils/editSessionHunks.d.ts +66 -0
  165. package/dist/utils/editSessionHunks.d.ts.map +1 -0
  166. package/dist/utils/editSessionHunks.js +437 -0
  167. package/dist/utils/editSessionHunks.js.map +1 -0
  168. package/dist/utils/getHunkSideBoundaries.d.ts +7 -0
  169. package/dist/utils/getHunkSideBoundaries.d.ts.map +1 -0
  170. package/dist/utils/getHunkSideBoundaries.js +12 -0
  171. package/dist/utils/getHunkSideBoundaries.js.map +1 -0
  172. package/dist/utils/getTotalLineCountFromHunks.d.ts.map +1 -1
  173. package/dist/utils/getTotalLineCountFromHunks.js +2 -1
  174. package/dist/utils/getTotalLineCountFromHunks.js.map +1 -1
  175. package/dist/utils/hydratePartialDiff.d.ts.map +1 -1
  176. package/dist/utils/hydratePartialDiff.js +4 -3
  177. package/dist/utils/hydratePartialDiff.js.map +1 -1
  178. package/dist/utils/isGutterUtilityPath.d.ts +5 -0
  179. package/dist/utils/isGutterUtilityPath.d.ts.map +1 -0
  180. package/dist/utils/isGutterUtilityPath.js +12 -0
  181. package/dist/utils/isGutterUtilityPath.js.map +1 -0
  182. package/dist/utils/iterateOverDiff.d.ts.map +1 -1
  183. package/dist/utils/iterateOverDiff.js +17 -12
  184. package/dist/utils/iterateOverDiff.js.map +1 -1
  185. package/dist/utils/parseMergeConflictDiffFromFile.d.ts.map +1 -1
  186. package/dist/utils/parseMergeConflictDiffFromFile.js +9 -6
  187. package/dist/utils/parseMergeConflictDiffFromFile.js.map +1 -1
  188. package/dist/utils/parsePatchFiles.d.ts.map +1 -1
  189. package/dist/utils/parsePatchFiles.js +6 -3
  190. package/dist/utils/parsePatchFiles.js.map +1 -1
  191. package/dist/utils/realignChangeContent.d.ts +33 -0
  192. package/dist/utils/realignChangeContent.d.ts.map +1 -0
  193. package/dist/utils/realignChangeContent.js +148 -0
  194. package/dist/utils/realignChangeContent.js.map +1 -0
  195. package/dist/utils/renderDiffWithHighlighter.js +1 -1
  196. package/dist/utils/resolveRegion.d.ts.map +1 -1
  197. package/dist/utils/resolveRegion.js +18 -2
  198. package/dist/utils/resolveRegion.js.map +1 -1
  199. package/dist/utils/updateDiffHunks.d.ts +8 -2
  200. package/dist/utils/updateDiffHunks.d.ts.map +1 -1
  201. package/dist/utils/updateDiffHunks.js +9 -7
  202. package/dist/utils/updateDiffHunks.js.map +1 -1
  203. package/dist/utils/virtualDiffLayout.d.ts +42 -2
  204. package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
  205. package/dist/utils/virtualDiffLayout.js +101 -7
  206. package/dist/utils/virtualDiffLayout.js.map +1 -1
  207. package/dist/worker/worker-portable.js +60 -20
  208. package/dist/worker/worker-portable.js.map +1 -1
  209. package/dist/worker/worker.js +61 -19
  210. package/dist/worker/worker.js.map +1 -1
  211. package/package.json +10 -7
  212. package/dist/editor/index.d.ts +0 -3
  213. package/dist/editor/index.js +0 -3
  214. package/dist/editor/tokenzier.d.ts.map +0 -1
  215. package/dist/editor/tokenzier.js.map +0 -1
  216. package/dist/react/EditorContext.d.ts +0 -15
  217. package/dist/react/EditorContext.d.ts.map +0 -1
  218. package/dist/react/EditorContext.js +0 -23
  219. package/dist/react/EditorContext.js.map +0 -1
@@ -1,21 +1,24 @@
1
1
  import { dequeueRender, queueRender } from "../managers/UniversalRenderingManager.js";
2
+ import { isGutterUtilityPath } from "../utils/isGutterUtilityPath.js";
2
3
  import { getFiletypeFromFileName } from "../utils/getFiletypeFromFileName.js";
3
4
  import { isMoveCursorShortcut, isPrimaryModifier, isSafari } from "./platform.js";
4
5
  import { resolveEditorCommandFromKeyboardEvent, resolveFindAgainShortcut } from "./command.js";
5
- import { EditStack } from "./editStack.js";
6
6
  import editor_default from "./editor2.js";
7
+ import { EditStack } from "./editStack.js";
8
+ import { resolveBlockCommentEdits, resolveCommentConfig, resolveLineCommentEdits } from "./languages.js";
7
9
  import { addEventListener, clampDomOffset, extend, getLineNumberAttr, h, round } from "./utils.js";
8
10
  import { applyDocumentChangeToLineAnnotations, renderLineAnnotations } from "./lineAnnotations.js";
9
11
  import { PopoverManager } from "./popover.js";
10
- import { applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNexMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectedLineBlocks, getSelectionAnchor, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveIndentEdits, resolveSelectionCut, selectionIntersects, shiftSelectionLines } from "./selection.js";
12
+ import { applyDeleteCharacterToSelections, applyDeleteHardLineForwardToSelections, applyDeleteSoftLineBackwardToSelections, applyDeleteWordBackwardToSelections, applyTextChangeToSelections, applyTextReplaceToSelections, applyTransposeToSelections, comparePosition, convertSelection, createSelectionFrom, createSelectionFromAnchorAndFocusOffsets, expandCollapsedSelectionToWord, extendSelection, extendSelections, findNextMatch, getAutoSurroundReplacementTexts, getCaretPosition, getDocumentBoundarySelection, getDocumentFullSelection, getSelectedLineBlocks, getSelectionAnchor, getSelectionClipboardTexts, getSelectionText, isCollapsedSelection, isLineEditable, mapCursorMove, mapSelectionShift, mergeOverlappingSelections, remapSelectionsAfterEdits, resolveIndentEdits, resolveSelectionCut, selectionIntersects, shiftSelectionLines } from "./selection.js";
11
13
  import { MarkerRenderer, markerSeverityDatasetKey } from "./marker.js";
12
14
  import { findBracketMatchRanges } from "./matchBrackets.js";
13
15
  import { createSpriteElement } from "./sprite.js";
14
16
  import { SearchPanelWidget } from "./searchPanel.js";
15
17
  import { SelectionActionWidget } from "./selectionAction.js";
18
+ import { cloneEditorState, createStateStorage } from "./stateStorage.js";
16
19
  import { TextDocument } from "./textDocument.js";
17
20
  import { Metrics, getExpandedAsciiTextColumns, getUnicodeMeasurementOffsets, snapTextOffsetToUnicodeBoundary } from "./textMeasure.js";
18
- import { EditorTokenizer, renderLineTokens } from "./tokenzier.js";
21
+ import { EditorTokenizer, renderLineTokens } from "./tokenizer.js";
19
22
  //#region src/editor/editor.ts
20
23
  function getShadowRootRange(shadowRoot) {
21
24
  const selection = shadowRoot.getSelection?.();
@@ -29,16 +32,38 @@ function getShadowRootRange(shadowRoot) {
29
32
  endOffset: range.endOffset
30
33
  };
31
34
  }
35
+ function requirePersistedCacheKey(file) {
36
+ if (typeof file.cacheKey !== "string" || file.cacheKey.length === 0) throw new Error(`Editor persistState requires a non-empty file.cacheKey for "${file.name}". Provide a unique, stable cacheKey for every editable file.`);
37
+ return file.cacheKey;
38
+ }
39
+ function isPromise(value) {
40
+ return typeof value === "object" && value !== null && "then" in value && typeof value.then === "function";
41
+ }
32
42
  const MAX_EDIT_WIDEN_WINDOW_MULTIPLE = 2;
43
+ const MAX_WRAP_OFFSETS_CACHE_LINES = 1e4;
44
+ const SELECTION_ACTION_POPOVER_PLACEMENT_KEY = "selection-action";
45
+ const MULTI_SELECTION_CLIPBOARD_TYPE = "application/vnd.pierre.diffs-selections+json";
33
46
  var Editor = class {
34
47
  #options;
35
48
  #metrics = new Metrics();
36
49
  #tokenizer;
37
50
  #popoverManager;
51
+ #textDocumentCache = /* @__PURE__ */ new Map();
52
+ #stateStorage;
53
+ #stateStorageOption;
54
+ #pendingStateWrites = /* @__PURE__ */ new Map();
55
+ #pendingStateRestore;
56
+ #stateRestoreGeneration = 0;
57
+ #restoreStateOnNextSync = false;
38
58
  #editorEventDisposes;
39
59
  #globalEventDisposes;
40
60
  #selectEventDisposes;
41
61
  #detach;
62
+ #attachState = {
63
+ generation: 0,
64
+ callback: void 0,
65
+ delivered: false
66
+ };
42
67
  #contentOffset;
43
68
  #gutterWidthCache;
44
69
  #contentWidthCache;
@@ -66,9 +91,11 @@ var Editor = class {
66
91
  #markerRenderer;
67
92
  #searchPanel;
68
93
  #selectionAction;
94
+ #canMountSelectionAction = true;
69
95
  #shouldIgnoreSelectionChange = false;
70
96
  #suppressNativeSelectionSync = false;
71
97
  #contentHasFocus = false;
98
+ #replacementFocusRequest;
72
99
  #isComposing = false;
73
100
  #isGutterMouseDown = false;
74
101
  #isContentMouseDown = false;
@@ -87,13 +114,13 @@ var Editor = class {
87
114
  #fontRemeasureScheduled = false;
88
115
  #themeSelectionRefreshFrame;
89
116
  #onDeferTokenize = (lines, themeType) => {
90
- this.#fileInstance?.updateRenderCache(lines, themeType, false);
117
+ this.#fileInstance?.updateRenderCache(lines, themeType, false, false);
91
118
  if (this.#renderRange !== void 0 && this.#renderRange.totalLines !== Infinity) {
92
119
  const { startingLine, totalLines } = this.#renderRange;
93
120
  const endLine = Math.min(startingLine + totalLines, this.#textDocument?.lineCount ?? 0);
94
121
  for (const [line, tokens] of lines) if (line >= startingLine && line < endLine) {
95
122
  const lineElement = this.#getLineElement(line);
96
- if (lineElement !== void 0) lineElement.replaceChildren(...renderLineTokens(tokens, themeType));
123
+ if (lineElement !== void 0) lineElement.replaceChildren(...renderLineTokens(tokens));
97
124
  }
98
125
  }
99
126
  };
@@ -101,21 +128,38 @@ var Editor = class {
101
128
  this.#options = options;
102
129
  }
103
130
  setOptions(options) {
104
- this.#options = {
131
+ const previousStorageOption = this.#options.persistStateStorage ?? "inMemory";
132
+ const nextOptions = {
105
133
  ...this.#options,
106
134
  ...options
107
135
  };
136
+ if (nextOptions.persistState === true && this.#fileInstance?.type === "file") {
137
+ const file = this.#fileInstance.__getCurrentFile?.() ?? this.#fileInfo;
138
+ if (file !== void 0) requirePersistedCacheKey(file);
139
+ }
140
+ this.#options = nextOptions;
141
+ if (this.#options.persistState !== true) {
142
+ this.#textDocumentCache.clear();
143
+ this.#stateRestoreGeneration++;
144
+ this.#restoreStateOnNextSync = false;
145
+ }
146
+ if ((this.#options.persistStateStorage ?? "inMemory") !== previousStorageOption) {
147
+ this.#stateRestoreGeneration++;
148
+ this.#stateStorage = void 0;
149
+ this.#stateStorageOption = void 0;
150
+ this.#pendingStateWrites.clear();
151
+ }
108
152
  }
109
153
  edit(fileInstance) {
110
- const { useTokenTransformer, enableGutterUtility, enableLineSelection, expandUnchanged, lineHoverHighlight = "disabled", ...rest } = fileInstance.options;
111
- if (useTokenTransformer !== true || enableGutterUtility === true || enableLineSelection === true || expandUnchanged !== true && fileInstance.type === "file-diff" || lineHoverHighlight !== "disabled") {
154
+ if (this.#options.persistState === true && fileInstance.type === "file") {
155
+ const file = fileInstance.__getCurrentFile?.();
156
+ if (file !== void 0) requirePersistedCacheKey(file);
157
+ }
158
+ this.#invalidateOnAttach();
159
+ if (fileInstance.options.useTokenTransformer !== true) {
112
160
  fileInstance.setOptions({
113
- ...rest,
114
- useTokenTransformer: true,
115
- enableGutterUtility: false,
116
- enableLineSelection: false,
117
- expandUnchanged: true,
118
- lineHoverHighlight: "disabled"
161
+ ...fileInstance.options,
162
+ useTokenTransformer: true
119
163
  });
120
164
  fileInstance.rerender();
121
165
  }
@@ -125,23 +169,22 @@ var Editor = class {
125
169
  return () => this.cleanUp();
126
170
  }
127
171
  /**
128
- * Apply edits to current attached file.
172
+ * Apply edits to current attached file. Every edit joins the undo timeline:
173
+ * a programmatic edit must leave the document and its history exactly as
174
+ * the same edit typed by the user would (history equivalence — see
175
+ * TextDocument.applyResolvedEdits), so it is undoable like any other edit.
176
+ *
177
+ * @param updateHistory Whether to record caller selection snapshots for
178
+ * exact undo/redo restoration. Defaults to true. When false, live selections
179
+ * are remapped during replay and the text edit still joins the undo timeline.
129
180
  */
130
- applyEdits(edits, updateHistory = false) {
181
+ applyEdits(edits, updateHistory = true) {
131
182
  const textDocument = this.#textDocument;
132
183
  if (textDocument == null) throw new Error("Editor is not attached");
133
184
  const wasFocused = this.#contentHasFocus;
134
185
  const selectionsBefore = this.#selections;
135
186
  const selectionOffsetsBefore = selectionsBefore?.map((selection) => [textDocument.offsetAt(selection.start), textDocument.offsetAt(selection.end)]);
136
- const resolvedEditOffsets = selectionsBefore === void 0 ? void 0 : edits.map((edit) => {
137
- const a = textDocument.offsetAt(edit.range.start);
138
- const b = textDocument.offsetAt(edit.range.end);
139
- return {
140
- start: Math.min(a, b),
141
- end: Math.max(a, b),
142
- text: edit.newText
143
- };
144
- }).sort((a, b) => a.start - b.start);
187
+ const resolvedEditOffsets = selectionsBefore === void 0 ? void 0 : textDocument.resolveEdits(edits).sort((a, b) => a.start - b.start);
145
188
  const change = textDocument.applyEdits(edits, updateHistory, selectionsBefore);
146
189
  if (change === void 0) return;
147
190
  let nextSelections;
@@ -167,45 +210,58 @@ var Editor = class {
167
210
  redo() {
168
211
  this.#runCommand("redo");
169
212
  }
213
+ getFile() {
214
+ const fileInfo = this.#fileInfo;
215
+ const textDocument = this.#textDocument;
216
+ if (fileInfo === void 0 || textDocument === void 0) return;
217
+ const file = { ...fileInfo };
218
+ Object.defineProperty(file, "contents", {
219
+ enumerable: true,
220
+ get: () => textDocument.getText()
221
+ });
222
+ return file;
223
+ }
224
+ getText() {
225
+ return this.#textDocument?.getText() ?? "";
226
+ }
170
227
  getState() {
171
- const fileRef = this.#getFileRef();
172
- if (fileRef === void 0) throw new Error("Editor is not attached");
228
+ const fileInstance = this.#fileInstance;
173
229
  return {
174
- file: {
175
- ...fileRef,
176
- cacheKey: "edited-at-" + Date.now()
177
- },
178
230
  selections: this.#selections,
179
- lineAnnotations: this.#lineAnnotations,
180
- renderRange: this.#renderRange
231
+ view: fileInstance != null ? { scrollLeft: fileInstance.getCodeScrollLeft() } : void 0
181
232
  };
182
233
  }
183
- setState({ file, lineAnnotations, renderRange, selections }) {
184
- this.#resetCache();
185
- this.#resetState();
186
- this.#initSelections = selections;
187
- this.#fileInstance?.render({
188
- file: {
189
- ...file,
190
- cacheKey: "edited-at-" + Date.now()
191
- },
192
- lineAnnotations,
193
- renderRange
194
- });
234
+ setState({ selections, view }) {
235
+ if (this.#fileInstance === void 0 || this.#textDocument === void 0) throw new Error("Editor is not attached");
236
+ this.#canMountSelectionAction = false;
237
+ this.#updateSelections(selections ?? []);
238
+ if (view != null) {
239
+ this.#fileInstance.setCodeScrollLeft(view.scrollLeft);
240
+ return;
241
+ }
242
+ this.#scrollToPrimaryCaret();
195
243
  }
196
244
  setSelections(selections) {
197
245
  const textDocument = this.#textDocument;
198
246
  if (textDocument === void 0) throw new Error("Text document is not initialized");
199
247
  const resolvedSelections = selections.map((selection) => {
200
- const start = textDocument.normalizePosition(selection.start);
201
- const end = textDocument.normalizePosition(selection.end);
248
+ let start = textDocument.normalizePosition(selection.start);
249
+ let end = textDocument.normalizePosition(selection.end);
250
+ let direction = selection.direction === "none" ? 0 : selection.direction === "backward" ? -1 : 1;
251
+ if (comparePosition(start, end) > 0) {
252
+ [start, end] = [end, start];
253
+ if (direction !== 0) direction = direction === 1 ? -1 : 1;
254
+ }
202
255
  return {
203
- direction: selection.direction === "none" ? 0 : selection.direction === "backward" ? -1 : 1,
256
+ direction,
204
257
  start,
205
258
  end
206
259
  };
207
260
  });
261
+ this.#canMountSelectionAction = false;
208
262
  this.#updateSelections(resolvedSelections);
263
+ const primarySelection = resolvedSelections.at(-1);
264
+ if (primarySelection !== void 0) this.#revealLineIfCollapsed(getCaretPosition(primarySelection).line);
209
265
  this.#scrollToPrimaryCaret();
210
266
  }
211
267
  setMarkers(markers) {
@@ -232,6 +288,19 @@ var Editor = class {
232
288
  }
233
289
  focus(options) {
234
290
  const preventScroll = options?.preventScroll ?? false;
291
+ const lineNumber = options?.lineNumber;
292
+ if (lineNumber === "first-visible" || typeof lineNumber === "number") {
293
+ const textDocument = this.#textDocument;
294
+ if (textDocument == null || this.#fileInstance == null) return;
295
+ const targetLineNumber = lineNumber === "first-visible" ? this.#getFirstVisibleLineNumber(options?.offset) : lineNumber;
296
+ if (targetLineNumber == null) return;
297
+ const position = textDocument.normalizePosition({
298
+ line: targetLineNumber - 1,
299
+ character: lineNumber === "first-visible" ? 0 : options?.character ?? 0
300
+ });
301
+ this.#focusAtPosition(position, preventScroll);
302
+ return;
303
+ }
235
304
  const primarySelection = this.#selections?.at(-1);
236
305
  if (primarySelection !== void 0) {
237
306
  const pos = primarySelection.direction === -1 ? primarySelection.end : primarySelection.start;
@@ -239,9 +308,19 @@ var Editor = class {
239
308
  } else this.#focus(void 0, preventScroll);
240
309
  }
241
310
  blur() {
311
+ this.#replacementFocusRequest = void 0;
312
+ this.#contentHasFocus = false;
313
+ this.#shouldIgnoreSelectionChange = false;
242
314
  this.#contentElement?.blur();
243
315
  }
244
316
  cleanUp(recycle = false) {
317
+ this.#invalidateOnAttach();
318
+ if (!recycle) this.#attachState.delivered = false;
319
+ const hadFileInstance = this.#fileInstance != null;
320
+ const shouldRestoreState = this.#isStatePersistenceEnabled;
321
+ this.#stateRestoreGeneration++;
322
+ this.#persistCurrentState();
323
+ if (hadFileInstance) this.#restoreStateOnNextSync = shouldRestoreState;
245
324
  dequeueRender(this.#handleCustomPasteEvent);
246
325
  this.#tokenizer?.cleanUp();
247
326
  this.#tokenizer = void 0;
@@ -255,7 +334,7 @@ var Editor = class {
255
334
  this.#editorEventDisposes = void 0;
256
335
  this.#selectEventDisposes?.forEach((dispose) => dispose());
257
336
  this.#selectEventDisposes = void 0;
258
- this.#detach?.();
337
+ this.#detach?.(recycle);
259
338
  this.#detach = void 0;
260
339
  this.#gutterWidthCache = void 0;
261
340
  this.#contentWidthCache = void 0;
@@ -277,6 +356,7 @@ var Editor = class {
277
356
  this.#gutterElement = void 0;
278
357
  this.#contentElement?.removeAttribute("contentEditable");
279
358
  this.#contentElement = void 0;
359
+ this.#replacementFocusRequest = void 0;
280
360
  this.#contentHasFocus = false;
281
361
  this.#overlayElement?.remove();
282
362
  this.#overlayElement = void 0;
@@ -288,6 +368,24 @@ var Editor = class {
288
368
  this.#themeSelectionRefreshFrame = void 0;
289
369
  }
290
370
  this.#resetState();
371
+ this.#fileInstance = void 0;
372
+ }
373
+ /** @internal Capture outgoing state and substitute cached text before render. */
374
+ __prepareFile(file) {
375
+ if (this.#options.persistState !== true) return file;
376
+ const cacheKey = requirePersistedCacheKey(file);
377
+ const fileInfo = this.#fileInfo;
378
+ const languageId = file.lang ?? getFiletypeFromFileName(file.name);
379
+ if (fileInfo !== void 0 && (requirePersistedCacheKey(fileInfo) !== cacheKey || fileInfo.name !== file.name || this.#textDocument?.languageId !== languageId)) {
380
+ this.#stateRestoreGeneration++;
381
+ this.#persistCurrentState();
382
+ }
383
+ const textDocument = this.#getCachedTextDocument(file, cacheKey);
384
+ if (textDocument === void 0 || textDocument.getText() === file.contents) return file;
385
+ return {
386
+ ...file,
387
+ contents: textDocument.getText()
388
+ };
291
389
  }
292
390
  /** @internal */
293
391
  __postponeBgTokenizeToNextFrame() {
@@ -300,7 +398,15 @@ var Editor = class {
300
398
  }
301
399
  }
302
400
  /** @internal */
401
+ __captureFocusForDOMReplacement() {
402
+ const contentElement = this.#contentElement;
403
+ const shadowActiveElement = this.#fileContainer?.shadowRoot?.activeElement;
404
+ if (contentElement != null && (this.#contentHasFocus || shadowActiveElement === contentElement || shadowActiveElement != null && contentElement.contains(shadowActiveElement))) this.#replacementFocusRequest = {};
405
+ }
406
+ /** @internal */
303
407
  __syncRenderView = (highlighter, fileContainer, fileOrDiff, lineAnnotations, renderRange) => {
408
+ const fileInstance = this.#fileInstance;
409
+ if (fileInstance == null) return;
304
410
  const shadowRoot = fileContainer.shadowRoot;
305
411
  if (shadowRoot == null) {
306
412
  console.error("[editor] Could not find the shadow root.");
@@ -319,7 +425,10 @@ var Editor = class {
319
425
  }
320
426
  break;
321
427
  }
322
- if (codeElement === void 0 || contentEl === void 0) return;
428
+ if (codeElement === void 0 || contentEl === void 0) {
429
+ this.#replacementFocusRequest = void 0;
430
+ return;
431
+ }
323
432
  this.#getPopoverManager().setViewportElements(fileContainer, codeElement);
324
433
  if (this.#fileContainer !== fileContainer) {
325
434
  this.#fileContainer = fileContainer;
@@ -328,28 +437,42 @@ var Editor = class {
328
437
  if (this.#themeStyleElement !== void 0) shadowRoot.appendChild(this.#themeStyleElement);
329
438
  if (this.#spriteElement !== void 0) shadowRoot.prepend(this.#spriteElement);
330
439
  }
331
- if (this.#textDocument === void 0 || this.#fileInfo === void 0 || this.#fileInfo.name !== fileOrDiff.name || this.#fileInfo.lang !== fileOrDiff.lang || this.#fileInfo.cacheKey !== fileOrDiff.cacheKey) {
440
+ const shouldRebuildDocument = this.#textDocument === void 0 || this.#fileInfo === void 0 || this.#fileInfo.name !== fileOrDiff.name || this.#fileInfo.lang !== fileOrDiff.lang || this.#fileInfo.cacheKey !== fileOrDiff.cacheKey;
441
+ const persistedCacheKey = this.#isStatePersistenceEnabled ? requirePersistedCacheKey(fileOrDiff) : void 0;
442
+ let persistedStateTarget;
443
+ if (shouldRebuildDocument) {
444
+ this.#invalidateOnAttach();
332
445
  let contents = "";
333
446
  if ("contents" in fileOrDiff) contents = fileOrDiff.contents;
334
447
  else contents = fileOrDiff.additionLines.join("");
335
448
  const editStack = new EditStack({ maxEntries: this.#options.historyMaxEntries });
336
- const textDocument = new TextDocument(fileOrDiff.name, contents, fileOrDiff.lang ?? getFiletypeFromFileName(fileOrDiff.name), 0, editStack);
337
449
  const { name, lang, cacheKey } = fileOrDiff;
450
+ const languageId = lang ?? getFiletypeFromFileName(fileOrDiff.name);
451
+ const textDocument = (persistedCacheKey !== void 0 ? this.#getCachedTextDocument(fileOrDiff, persistedCacheKey) : void 0) ?? new TextDocument(fileOrDiff.name, contents, languageId, 0, editStack);
338
452
  this.#fileInfo = {
339
453
  name,
340
454
  lang,
341
455
  cacheKey
342
456
  };
343
457
  this.#textDocument = textDocument;
458
+ if (persistedCacheKey !== void 0) {
459
+ this.#textDocumentCache.set(persistedCacheKey, textDocument);
460
+ persistedStateTarget = {
461
+ cacheKey: persistedCacheKey,
462
+ textDocument
463
+ };
464
+ }
344
465
  this.#tokenizer?.cleanUp();
345
466
  this.#tokenizer = void 0;
346
467
  this.#resetState();
468
+ this.#wrapLineOffsetsCache.clear();
347
469
  this.#selections = this.#initSelections;
348
- requestAnimationFrame(() => {
349
- this.#options.onAttach?.(this, this.#fileInstance);
350
- });
351
470
  if (this.#textDocument !== void 0 && this.#options.__debug === true) console.log("[diffs/editor] text document rebuilt from", fileOrDiff.name);
352
471
  }
472
+ if (persistedStateTarget === void 0 && this.#restoreStateOnNextSync && persistedCacheKey !== void 0 && this.#textDocument !== void 0) persistedStateTarget = {
473
+ cacheKey: persistedCacheKey,
474
+ textDocument: this.#textDocument
475
+ };
353
476
  const textDocument = this.#textDocument;
354
477
  if (this.#tokenizer == null && textDocument != null) this.#tokenizer = new EditorTokenizer({
355
478
  highlighter,
@@ -381,11 +504,12 @@ var Editor = class {
381
504
  this.#listenContentElement(contentEl, gutterEl);
382
505
  if (this.#contentElement !== void 0 && this.#options.__debug === true) console.log("[diffs/editor] full re-render triggered !!!");
383
506
  }
507
+ if (this.#replacementFocusRequest != null) this.#replacementFocusRequest.target = contentEl;
384
508
  if (contentEl.ariaLabel !== fileOrDiff.name) contentEl.ariaLabel = fileOrDiff.name;
385
- if (lineAnnotations !== void 0 && lineAnnotations.length > 0 || this.#isDiff && this.#diffSyle === "unified") for (const child of this.#contentElement.children) {
509
+ if (lineAnnotations !== void 0 && lineAnnotations.length > 0 || this.#isDiff) for (const child of this.#contentElement.children) {
386
510
  const el = child;
387
- const { lineAnnotation, lineType } = el.dataset;
388
- if (lineAnnotation !== void 0 || lineType === "change-deletion") el.setAttribute("contenteditable", "false");
511
+ const { lineAnnotation, lineType, separator } = el.dataset;
512
+ if (lineAnnotation !== void 0 || separator !== void 0 || lineType === "change-deletion") el.setAttribute("contenteditable", "false");
389
513
  }
390
514
  this.#resetCache();
391
515
  this.#tokenizer?.syncTheme(this.#fileInstance?.options ?? {});
@@ -393,19 +517,140 @@ var Editor = class {
393
517
  this.#renderRange = renderRange;
394
518
  this.#viewportWindowLines = renderRange?.totalLines;
395
519
  this.#tokenizer?.prebuildStateStack(renderRange);
396
- this.#markerRenderer?.removePopup();
520
+ this.#markerRenderer?.removePopover();
397
521
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) this.#updateSelections(this.#selections ?? []);
398
522
  if (this.#initSelections !== void 0 && this.#primaryCaretElement !== void 0) {
399
523
  this.#initSelections = void 0;
400
524
  this.#scrollToPrimaryCaret(false, "center");
401
525
  } else if (this.#scrollingToLine !== void 0) this.#scrollToLine(this.#scrollingToLine, this.#scrollingToLineChar, this.#scrollingToLineNoFocus);
402
- else if (this.#selections !== void 0 && this.#selections.length > 0 && !this.#retainSearchPanelFocus) this.focus({ preventScroll: true });
526
+ else if (this.#replacementFocusRequest !== void 0) this.#restoreReplacementFocus();
527
+ else if (this.#selections !== void 0 && this.#selections.length > 0 && this.#contentHasFocus && !this.#retainSearchPanelFocus) this.focus({ preventScroll: true });
403
528
  if (this.#retainSearchPanelFocus) this.#searchPanel?.focus();
404
529
  if (this.#options.__debug === true && renderRange !== void 0) {
405
530
  const { startingLine, totalLines } = renderRange;
406
531
  console.log("[diffs/editor] render file:", fileOrDiff.name, "RenderRange:", startingLine + "-" + (startingLine + totalLines), "of", this.#textDocument?.lineCount, "lines");
407
532
  }
533
+ if (persistedStateTarget !== void 0) {
534
+ this.#restoreStateOnNextSync = false;
535
+ this.#restorePersistedState(persistedStateTarget.cacheKey, persistedStateTarget.textDocument);
536
+ }
537
+ this.#scheduleOnAttach(fileInstance);
408
538
  };
539
+ get #isStatePersistenceEnabled() {
540
+ return this.#options.persistState === true && this.#fileInstance?.type === "file";
541
+ }
542
+ #getCachedTextDocument(file, cacheKey) {
543
+ const textDocument = this.#textDocumentCache.get(cacheKey);
544
+ const languageId = file.lang ?? getFiletypeFromFileName(file.name);
545
+ return textDocument?.languageId === languageId ? textDocument : void 0;
546
+ }
547
+ #getStateStorage() {
548
+ const option = this.#options.persistStateStorage ?? "inMemory";
549
+ if (this.#stateStorage === void 0 || this.#stateStorageOption !== option) {
550
+ this.#stateStorage = createStateStorage(option);
551
+ this.#stateStorageOption = option;
552
+ }
553
+ return this.#stateStorage;
554
+ }
555
+ #persistCurrentState() {
556
+ const fileInfo = this.#fileInfo;
557
+ const textDocument = this.#textDocument;
558
+ if (!this.#isStatePersistenceEnabled || fileInfo === void 0 || textDocument === void 0) return;
559
+ const cacheKey = requirePersistedCacheKey(fileInfo);
560
+ this.#textDocumentCache.set(cacheKey, textDocument);
561
+ let storage;
562
+ try {
563
+ storage = this.#getStateStorage();
564
+ } catch {
565
+ return;
566
+ }
567
+ const state = cloneEditorState(this.getState());
568
+ const pendingRestore = this.#pendingStateRestore;
569
+ if (pendingRestore?.cacheKey === cacheKey && pendingRestore.textDocument === textDocument) {
570
+ this.#pendingStateRestore = void 0;
571
+ if (textDocument.version === pendingRestore.documentVersion && this.#selections === pendingRestore.selections && state.view?.scrollLeft === pendingRestore.view?.scrollLeft) return;
572
+ this.#writeState(storage, cacheKey, state, pendingRestore.completion);
573
+ return;
574
+ }
575
+ this.#writeState(storage, cacheKey, state);
576
+ }
577
+ #writeState(storage, cacheKey, state, waitFor) {
578
+ const previousWrite = this.#pendingStateWrites.get(cacheKey);
579
+ if (waitFor !== void 0 || previousWrite !== void 0) {
580
+ let pending = waitFor ?? Promise.resolve();
581
+ if (previousWrite !== void 0) pending = pending.then(() => previousWrite);
582
+ this.#trackStateWrite(cacheKey, pending.then(() => storage.set(cacheKey, state)));
583
+ return;
584
+ }
585
+ let result;
586
+ try {
587
+ result = storage.set(cacheKey, state);
588
+ } catch {
589
+ return;
590
+ }
591
+ if (!isPromise(result)) return;
592
+ this.#trackStateWrite(cacheKey, result);
593
+ }
594
+ #trackStateWrite(cacheKey, result) {
595
+ const pending = result.catch(() => {});
596
+ this.#pendingStateWrites.set(cacheKey, pending);
597
+ pending.finally(() => {
598
+ if (this.#pendingStateWrites.get(cacheKey) === pending) this.#pendingStateWrites.delete(cacheKey);
599
+ });
600
+ }
601
+ #restorePersistedState(cacheKey, textDocument) {
602
+ const generation = ++this.#stateRestoreGeneration;
603
+ const documentVersion = textDocument.version;
604
+ const selections = this.#selections;
605
+ const view = this.getState().view;
606
+ const applyState = (state) => {
607
+ const currentView = this.getState().view;
608
+ if (state === void 0 || generation !== this.#stateRestoreGeneration || this.#textDocument !== textDocument || textDocument.version !== documentVersion || this.#selections !== selections || currentView?.scrollLeft !== view?.scrollLeft || this.#fileInfo === void 0 || requirePersistedCacheKey(this.#fileInfo) !== cacheKey) return;
609
+ this.setState(cloneEditorState(state));
610
+ };
611
+ const readState = () => {
612
+ let result;
613
+ try {
614
+ result = this.#getStateStorage().get(cacheKey);
615
+ } catch {
616
+ return;
617
+ }
618
+ if (isPromise(result)) return result.then(applyState).catch(() => {});
619
+ else try {
620
+ applyState(result);
621
+ } catch {}
622
+ };
623
+ const pendingWrite = this.#pendingStateWrites.get(cacheKey);
624
+ const result = pendingWrite === void 0 ? readState() : pendingWrite.then(readState);
625
+ if (isPromise(result)) {
626
+ const pendingRestore = {
627
+ cacheKey,
628
+ textDocument,
629
+ documentVersion,
630
+ selections,
631
+ view,
632
+ completion: result.catch(() => {})
633
+ };
634
+ this.#pendingStateRestore = pendingRestore;
635
+ pendingRestore.completion.finally(() => {
636
+ if (this.#pendingStateRestore === pendingRestore) this.#pendingStateRestore = void 0;
637
+ });
638
+ }
639
+ }
640
+ #isLineRenderable(line) {
641
+ return this.#fileInstance?.isLineRenderable?.(line + 1) ?? true;
642
+ }
643
+ get #resolveRenderableLine() {
644
+ const fileInstance = this.#fileInstance;
645
+ if (fileInstance?.getNearestRenderableLine == null) return;
646
+ return (line, direction) => {
647
+ const nearest = fileInstance.getNearestRenderableLine(line + 1, direction);
648
+ return nearest == null ? void 0 : nearest - 1;
649
+ };
650
+ }
651
+ #revealLineIfCollapsed(line) {
652
+ if (!this.#isLineRenderable(line)) this.#fileInstance?.revealLine?.(line + 1);
653
+ }
409
654
  get #diffSyle() {
410
655
  return this.#fileInstance?.options.diffStyle ?? "split";
411
656
  }
@@ -417,21 +662,24 @@ var Editor = class {
417
662
  }
418
663
  #getPopoverManager() {
419
664
  return this.#popoverManager ??= new PopoverManager({
420
- hasActivePopover: () => this.#selectionAction !== void 0,
421
- updateActivePopover: () => this.#updateSelectionActionPopover()
665
+ hasActivePopover: () => this.#selectionAction !== void 0 || this.#markerRenderer?.isPopoverVisible() === true,
666
+ updateActivePopover: () => {
667
+ this.#updateSelectionActionPopover();
668
+ this.#markerRenderer?.updatePopoverPosition();
669
+ }
422
670
  });
423
671
  }
424
672
  #resetCache() {
425
673
  this.#lineYCache.clear();
426
- this.#wrapLineOffsetsCache.clear();
427
674
  this.#lineElementsCache.clear();
428
675
  this.#lastAccessedCharX = void 0;
429
676
  }
430
677
  #resetState() {
431
- this.#setSelectedLinesSafe(null);
678
+ this.#setEditorActiveLineSafe(null);
432
679
  this.#gutterWidthCache = void 0;
433
680
  this.#contentWidthCache = void 0;
434
681
  this.#shouldIgnoreSelectionChange = false;
682
+ this.#suppressNativeSelectionSync = false;
435
683
  this.#overlayElements?.forEach((el) => el.remove());
436
684
  this.#overlayElements = void 0;
437
685
  this.#selections = void 0;
@@ -443,6 +691,33 @@ var Editor = class {
443
691
  this.#searchPanel = void 0;
444
692
  this.#selectionAction?.cleanup();
445
693
  this.#selectionAction = void 0;
694
+ this.#canMountSelectionAction = true;
695
+ }
696
+ #invalidateOnAttach() {
697
+ const attachState = this.#attachState;
698
+ attachState.generation++;
699
+ if (attachState.callback != null) {
700
+ dequeueRender(attachState.callback);
701
+ attachState.callback = void 0;
702
+ }
703
+ }
704
+ #scheduleOnAttach(fileInstance) {
705
+ const attachState = this.#attachState;
706
+ const textDocument = this.#textDocument;
707
+ if (attachState.delivered || attachState.callback != null || textDocument == null) return;
708
+ const { generation } = attachState;
709
+ const callback = () => {
710
+ if (attachState.callback !== callback) return;
711
+ attachState.callback = void 0;
712
+ if (generation !== attachState.generation || this.#fileInstance !== fileInstance || this.#textDocument !== textDocument) return;
713
+ const contentElement = this.#contentElement;
714
+ const shadowRoot = this.#fileContainer?.shadowRoot;
715
+ if (contentElement == null || shadowRoot == null || !shadowRoot.contains(contentElement)) return;
716
+ attachState.delivered = true;
717
+ this.#options.onAttach?.(this, fileInstance);
718
+ };
719
+ attachState.callback = callback;
720
+ queueRender(callback);
446
721
  }
447
722
  #initialize() {
448
723
  this.#globalStyleElement = h("style", {
@@ -461,7 +736,26 @@ var Editor = class {
461
736
  this.#themeStyleElement = h("style", { dataset: "editorThemeCss" });
462
737
  this.#spriteElement = createSpriteElement();
463
738
  this.#overlayElement = h("div", { dataset: "editorOverlay" });
739
+ const replacementFocusTargetIsContent = (event) => {
740
+ const target = event.composedPath()[0];
741
+ return target instanceof Node && this.#contentElement?.contains(target) === true;
742
+ };
743
+ const cancelReplacementFocus = () => {
744
+ if (this.#replacementFocusRequest == null) return;
745
+ this.#replacementFocusRequest = void 0;
746
+ this.#contentHasFocus = false;
747
+ this.#shouldIgnoreSelectionChange = false;
748
+ };
464
749
  this.#globalEventDisposes = [
750
+ addEventListener(document, "focusin", (event) => {
751
+ if (!replacementFocusTargetIsContent(event)) cancelReplacementFocus();
752
+ }, { passive: true }),
753
+ addEventListener(document, "pointerdown", (event) => {
754
+ if (!replacementFocusTargetIsContent(event)) queueMicrotask(cancelReplacementFocus);
755
+ }, {
756
+ capture: true,
757
+ passive: true
758
+ }),
465
759
  addEventListener(document, "selectionchange", () => {
466
760
  const shadowRoot = this.#fileContainer?.shadowRoot;
467
761
  if (this.#shouldIgnoreSelectionChange || this.#suppressNativeSelectionSync || shadowRoot == null || !this.#contentHasFocus) return;
@@ -514,6 +808,12 @@ var Editor = class {
514
808
  this.#selectEventDisposes?.forEach((dispose) => dispose());
515
809
  this.#selectEventDisposes = disposes;
516
810
  }
811
+ #isLineSelectionEnabled() {
812
+ return this.#fileInstance?.options.enableLineSelection === true;
813
+ }
814
+ #preserveEditorSelectionsForGutterGesture() {
815
+ this.#suppressNativeSelectionSync = this.#selections !== void 0;
816
+ }
517
817
  #listenContentElement(contentEl, gutterEl) {
518
818
  const { onFocus, onBlur } = this.#options;
519
819
  const targetIsContentElement = (e) => {
@@ -528,10 +828,15 @@ var Editor = class {
528
828
  if (!this.#isContentMouseDown && !this.#shouldIgnoreSelectionChange && this.#selections !== void 0 && this.#selections.length > 0) this.#setWindowSelection(this.#selections.at(-1));
529
829
  }, { passive: true }),
530
830
  addEventListener(contentEl, "blur", () => {
831
+ if (this.#replacementFocusRequest?.target === contentEl) {
832
+ this.#replacementFocusRequest = void 0;
833
+ this.#shouldIgnoreSelectionChange = false;
834
+ }
531
835
  this.#contentHasFocus = false;
532
836
  onBlur?.();
533
837
  }, { passive: true }),
534
838
  addEventListener(contentEl, "pointerdown", (e) => {
839
+ this.#canMountSelectionAction = true;
535
840
  this.#suppressNativeSelectionSync = false;
536
841
  if (e.pointerType !== "mouse") return;
537
842
  if (this.#isDeletedLineTarget(e)) {
@@ -540,7 +845,7 @@ var Editor = class {
540
845
  return;
541
846
  }
542
847
  this.#setDeletedTextSelectionActive(false);
543
- this.#markerRenderer?.removePopup();
848
+ this.#markerRenderer?.removePopover();
544
849
  if (isSafari() && this.#lineAnnotations !== void 0 && this.#lineAnnotations.length > 0) {
545
850
  const annotationDisposes = [...contentEl.querySelectorAll("[data-line-annotation]")].map((el) => [addEventListener(el, "mouseenter", () => {
546
851
  this.#shouldIgnoreSelectionChange = true;
@@ -573,27 +878,21 @@ var Editor = class {
573
878
  this.#retainSearchPanelFocus = false;
574
879
  this.#selectionAction?.cleanup();
575
880
  this.#selectionAction = void 0;
576
- if (this.#selections !== void 0 && this.#selections.length > 0) {
577
- const primarySelection = this.#selections.at(-1);
578
- if (!isCollapsedSelection(primarySelection) || this.#selections.length > 1) {
579
- const pos = getCaretPosition(primarySelection);
580
- this.#updateSelections([{
581
- start: pos,
582
- end: pos,
583
- direction: 0
584
- }]);
585
- this.#focus(pos);
586
- }
587
- }
881
+ this.#runCommand("simplifySelection");
588
882
  return;
589
883
  }
590
884
  if (!targetIsContentElement(e)) return;
885
+ this.#canMountSelectionAction = true;
591
886
  this.#suppressNativeSelectionSync = false;
592
887
  const mvShortcut = isMoveCursorShortcut(e);
593
888
  const textDocument = this.#textDocument;
594
889
  if (this.#selections !== void 0 && this.#selections.length > 0 && mvShortcut !== void 0 && textDocument !== void 0) {
595
- if (e.shiftKey) this.#updateSelections(mapSelectionShift(textDocument, this.#selections, mvShortcut));
596
- else this.#updateSelections(mapCursorMove(textDocument, this.#selections, mvShortcut));
890
+ const cursorMoveOptions = {
891
+ getSoftLineOffsets: this.#isWrap ? (line) => this.#wrapLineText(line) : void 0,
892
+ resolveRenderableLine: this.#resolveRenderableLine
893
+ };
894
+ if (e.shiftKey) this.#updateSelections(mapSelectionShift(textDocument, this.#selections, mvShortcut, cursorMoveOptions));
895
+ else this.#updateSelections(mapCursorMove(textDocument, this.#selections, mvShortcut, cursorMoveOptions));
597
896
  this.#scrollToPrimaryCaret();
598
897
  e.preventDefault();
599
898
  return;
@@ -626,19 +925,34 @@ var Editor = class {
626
925
  addEventListener(contentEl, "copy", (e) => {
627
926
  if (!targetIsContentElement(e)) return;
628
927
  e.preventDefault();
629
- e.clipboardData?.setData("text", this.#isDeletedTextSelectionActive() ? this.#deletedTextForClipboard() : this.#getSelectionText());
928
+ if (this.#isDeletedTextSelectionActive()) e.clipboardData?.setData("text", this.#deletedTextForClipboard());
929
+ else this.#writeSelectionClipboardData(e.clipboardData, this.#getSelectionText(), this.#getSelectionClipboardTexts());
630
930
  }),
631
931
  addEventListener(contentEl, "cut", (e) => {
632
932
  if (!targetIsContentElement(e)) return;
633
933
  e.preventDefault();
634
- e.clipboardData?.setData("text", this.#isDeletedTextSelectionActive() ? this.#deletedTextForClipboard() : this.#cutSelectionText());
934
+ if (this.#isDeletedTextSelectionActive()) e.clipboardData?.setData("text", this.#deletedTextForClipboard());
935
+ else {
936
+ const selectionTexts = this.#getSelectionClipboardTexts();
937
+ this.#writeSelectionClipboardData(e.clipboardData, this.#cutSelectionText(), selectionTexts);
938
+ }
635
939
  }),
636
940
  addEventListener(contentEl, "paste", (e) => {
637
941
  if (!targetIsContentElement(e)) return;
638
942
  e.preventDefault();
639
- const text = e.clipboardData?.getData("text");
943
+ const clipboardData = e.clipboardData;
640
944
  const textDocument = this.#textDocument;
641
- if (text !== void 0 && textDocument !== void 0) this.#replaceSelectionText(textDocument.normalizeEol(text), void 0, true);
945
+ if (clipboardData === null || textDocument === void 0) return;
946
+ let text = clipboardData.getData("text");
947
+ const selectionCount = this.#selections?.length ?? 0;
948
+ if (selectionCount > 1) {
949
+ const multiSelectionText = clipboardData.getData(MULTI_SELECTION_CLIPBOARD_TYPE);
950
+ if (multiSelectionText !== void 0) try {
951
+ const selectionTexts = JSON.parse(multiSelectionText);
952
+ if (Array.isArray(selectionTexts) && selectionTexts.length === selectionCount) text = selectionTexts;
953
+ } catch {}
954
+ }
955
+ this.#replaceSelectionText(Array.isArray(text) ? text.map((t) => textDocument.normalizeEol(t)) : textDocument.normalizeEol(text), void 0, true, "document");
642
956
  }),
643
957
  addEventListener(contentEl, "beforeinput", (e) => {
644
958
  if (!targetIsContentElement(e)) return;
@@ -665,9 +979,20 @@ var Editor = class {
665
979
  ];
666
980
  const deletionsCode = this.#fileContainer?.shadowRoot?.querySelector("[data-deletions]");
667
981
  if (deletionsCode != null) this.#editorEventDisposes.push(addEventListener(deletionsCode, "pointerdown", (e) => {
668
- const target = e.composedPath()[0];
982
+ const path = e.composedPath();
983
+ if (isGutterUtilityPath(path)) {
984
+ this.#preserveEditorSelectionsForGutterGesture();
985
+ return;
986
+ }
987
+ const target = path[0];
669
988
  const gutterRow = target instanceof HTMLElement ? target.closest("[data-column-number]") : null;
670
- if (gutterRow instanceof HTMLElement && this.#beginDeletionGutterSelection(gutterRow, deletionsCode, e.pointerType === "mouse")) return;
989
+ if (gutterRow instanceof HTMLElement) {
990
+ if (this.#isLineSelectionEnabled()) {
991
+ this.#preserveEditorSelectionsForGutterGesture();
992
+ return;
993
+ }
994
+ if (this.#beginDeletionGutterSelection(gutterRow, deletionsCode, e.pointerType === "mouse")) return;
995
+ }
671
996
  this.#setDeletedTextSelectionActive(true);
672
997
  if (this.#selections !== void 0) this.#updateSelections([]);
673
998
  }, { passive: true }));
@@ -686,7 +1011,16 @@ var Editor = class {
686
1011
  return lineNumber - 1;
687
1012
  };
688
1013
  this.#editorEventDisposes.push(addEventListener(gutterEl, "pointerdown", (e) => {
689
- const gutterRow = resolveGutterTarget(e.composedPath()[0]);
1014
+ const path = e.composedPath();
1015
+ if (isGutterUtilityPath(path)) {
1016
+ this.#preserveEditorSelectionsForGutterGesture();
1017
+ return;
1018
+ }
1019
+ const gutterRow = resolveGutterTarget(path[0]);
1020
+ if (gutterRow !== void 0 && this.#isLineSelectionEnabled()) {
1021
+ this.#preserveEditorSelectionsForGutterGesture();
1022
+ return;
1023
+ }
690
1024
  if (gutterRow?.dataset.lineType === "change-deletion") {
691
1025
  const code = gutterRow.closest("[data-code]");
692
1026
  if (code != null) this.#beginDeletionGutterSelection(gutterRow, code, e.pointerType === "mouse");
@@ -696,7 +1030,8 @@ var Editor = class {
696
1030
  const textDocument = this.#textDocument;
697
1031
  const lineIndex = resolveEditableLine(gutterRow);
698
1032
  if (lineIndex === void 0 || textDocument === void 0) return;
699
- this.#markerRenderer?.removePopup();
1033
+ this.#canMountSelectionAction = true;
1034
+ this.#markerRenderer?.removePopover();
700
1035
  const selection = this.#spanLineSelection(lineIndex, lineIndex, textDocument);
701
1036
  this.#isGutterMouseDown = true;
702
1037
  this.#selectionStart = selection;
@@ -726,8 +1061,17 @@ var Editor = class {
726
1061
  #handleCustomPasteEvent = async () => {
727
1062
  const clipboard = this.#options.clipboard;
728
1063
  if (clipboard !== void 0) {
729
- const text = await clipboard.readText();
730
- this.#replaceSelectionText(text, void 0, true);
1064
+ let text = await clipboard.readText();
1065
+ const selectionCount = this.#selections?.length ?? 0;
1066
+ if (selectionCount > 1) {
1067
+ const multiSelectionText = await clipboard.readText(MULTI_SELECTION_CLIPBOARD_TYPE);
1068
+ try {
1069
+ const selectionTexts = JSON.parse(multiSelectionText);
1070
+ if (Array.isArray(selectionTexts) && selectionTexts.length === selectionCount) text = selectionTexts;
1071
+ } catch {}
1072
+ }
1073
+ const textDocument = this.#textDocument;
1074
+ if (textDocument !== void 0) this.#replaceSelectionText(Array.isArray(text) ? text.map((t) => textDocument.normalizeEol(t)) : textDocument.normalizeEol(text), void 0, true, "document");
731
1075
  }
732
1076
  };
733
1077
  #computeContentOffset(contentEl) {
@@ -763,9 +1107,11 @@ var Editor = class {
763
1107
  this.#updateSelections(expanded);
764
1108
  this.focus();
765
1109
  } else {
766
- const nextMatch = findNexMatch(textDocument, selections);
1110
+ const nextMatch = findNextMatch(textDocument, selections);
767
1111
  if (nextMatch !== void 0) {
768
1112
  this.#updateSelections(nextMatch);
1113
+ const primaryMatch = nextMatch.at(-1);
1114
+ if (primaryMatch !== void 0) this.#revealLineIfCollapsed(getCaretPosition(primaryMatch).line);
769
1115
  this.#scrollToPrimaryCaret();
770
1116
  }
771
1117
  }
@@ -775,18 +1121,74 @@ var Editor = class {
775
1121
  case "moveLineDown":
776
1122
  this.#moveSelectedLines(command === "moveLineUp" ? -1 : 1);
777
1123
  break;
1124
+ case "copyLineUp":
1125
+ case "copyLineDown":
1126
+ this.#copySelectedLines(command === "copyLineUp" ? -1 : 1);
1127
+ break;
1128
+ case "simplifySelection": {
1129
+ const selections = this.#selections;
1130
+ const primarySelection = selections?.at(-1);
1131
+ if (selections === void 0 || primarySelection === void 0) break;
1132
+ if (selections.length > 1) {
1133
+ this.#updateSelections([primarySelection]);
1134
+ this.#focus(getCaretPosition(primarySelection));
1135
+ } else if (!isCollapsedSelection(primarySelection)) {
1136
+ const caret = getCaretPosition(primarySelection);
1137
+ this.#updateSelections([{
1138
+ start: caret,
1139
+ end: caret,
1140
+ direction: 0
1141
+ }]);
1142
+ this.#focus(caret);
1143
+ }
1144
+ break;
1145
+ }
1146
+ case "insertBlankLine":
1147
+ this.#insertBlankLine();
1148
+ break;
1149
+ case "deleteHardLineForward":
1150
+ this.#deleteHardLineForward();
1151
+ break;
1152
+ case "toggleComment":
1153
+ case "toggleBlockComment": {
1154
+ const selections = this.#selections;
1155
+ if (selections === void 0) break;
1156
+ const { lineComment, blockComment } = resolveCommentConfig(textDocument.languageId, this.#options.languageCommentConfig);
1157
+ if (command === "toggleComment" && lineComment !== null) {
1158
+ this.#applyCommandEdits(resolveLineCommentEdits(textDocument, selections, lineComment));
1159
+ break;
1160
+ }
1161
+ const linewise = command === "toggleComment";
1162
+ const result = resolveBlockCommentEdits(textDocument, selections, blockComment, linewise);
1163
+ if (result !== void 0) this.#applyCommandEdits(result.edits, linewise ? void 0 : (document) => result.nextSelectionOffsets.map(([start, end, direction]) => ({
1164
+ start: document.positionAt(start),
1165
+ end: document.positionAt(end),
1166
+ direction
1167
+ })));
1168
+ break;
1169
+ }
778
1170
  case "indent":
779
1171
  case "outdent":
1172
+ case "indentLess":
1173
+ case "indentMore":
780
1174
  if (this.#selections !== void 0) {
781
1175
  const edits = [];
782
1176
  const nextSelections = [];
1177
+ const editedLines = /* @__PURE__ */ new Set();
783
1178
  const sameLineIndents = [];
784
1179
  for (const selection of this.#selections) {
785
1180
  const startLine = selection.start.line;
786
- const outdent = command === "outdent";
787
- if (startLine !== selection.end.line || outdent) {
1181
+ const outdent = command === "outdent" || command === "indentLess";
1182
+ const lineBased = command === "indentLess" || command === "indentMore";
1183
+ if (startLine !== selection.end.line || outdent || lineBased) {
788
1184
  const ret = resolveIndentEdits(textDocument, selection, this.#metrics.tabSize, outdent);
789
- edits.push(...ret[0]);
1185
+ for (const edit of ret[0]) {
1186
+ const line = edit.range.start.line;
1187
+ if (!editedLines.has(line)) {
1188
+ editedLines.add(line);
1189
+ edits.push(edit);
1190
+ }
1191
+ }
790
1192
  nextSelections.push(ret[1]);
791
1193
  } else {
792
1194
  const text = textDocument.charAt({
@@ -838,17 +1240,19 @@ var Editor = class {
838
1240
  const fullSelection = getDocumentFullSelection(textDocument);
839
1241
  this.#updateSelections([fullSelection]);
840
1242
  this.focus();
841
- const renderedLines = this.#getRenderedEditableLineRange();
842
- if (renderedLines !== void 0) {
1243
+ const renderedLineRanges = this.#getRenderedEditableLineRanges();
1244
+ const firstRenderedLine = renderedLineRanges[0]?.[0];
1245
+ const lastRenderedLine = renderedLineRanges.at(-1)?.[1];
1246
+ if (firstRenderedLine !== void 0 && lastRenderedLine !== void 0) {
843
1247
  this.#suppressNativeSelectionSync = true;
844
1248
  this.#setWindowSelection({
845
1249
  start: {
846
- line: renderedLines.first,
1250
+ line: firstRenderedLine,
847
1251
  character: 0
848
1252
  },
849
1253
  end: {
850
- line: renderedLines.last,
851
- character: textDocument.getLineLength(renderedLines.last)
1254
+ line: lastRenderedLine,
1255
+ character: textDocument.getLineLength(lastRenderedLine)
852
1256
  },
853
1257
  direction: 1
854
1258
  });
@@ -858,8 +1262,9 @@ var Editor = class {
858
1262
  case "moveCursorToDocStart":
859
1263
  case "moveCursorToDocEnd":
860
1264
  {
861
- const atEnd = command === "moveCursorToDocEnd";
862
- this.#updateSelections([getDocumentBoundarySelection(textDocument, atEnd, this.#isDiff)]);
1265
+ const boundarySelection = getDocumentBoundarySelection(textDocument, command === "moveCursorToDocEnd", this.#isDiff);
1266
+ this.#updateSelections([boundarySelection]);
1267
+ this.#revealLineIfCollapsed(getCaretPosition(boundarySelection).line);
863
1268
  this.#scrollToPrimaryCaret();
864
1269
  }
865
1270
  break;
@@ -869,25 +1274,191 @@ var Editor = class {
869
1274
  const atEnd = command === "expandSelectionDocEnd";
870
1275
  const selections = this.#selections;
871
1276
  if (selections !== void 0) {
872
- this.#updateSelections(extendSelections(selections, getDocumentBoundarySelection(textDocument, atEnd, this.#isDiff)));
1277
+ const boundarySelection = getDocumentBoundarySelection(textDocument, atEnd, this.#isDiff);
1278
+ this.#updateSelections(extendSelections(selections, boundarySelection));
1279
+ this.#revealLineIfCollapsed(getCaretPosition(boundarySelection).line);
873
1280
  this.#scrollToPrimaryCaret();
874
1281
  }
875
1282
  }
876
1283
  break;
877
1284
  case "undo":
878
- if (this.#textDocument?.canUndo === true) {
879
- const undoResult = this.#textDocument.undo();
880
- if (undoResult !== void 0) this.#applyChange(...undoResult);
881
- }
882
- break;
883
1285
  case "redo":
884
- if (this.#textDocument?.canRedo === true) {
885
- const redoResult = this.#textDocument.redo();
886
- if (redoResult !== void 0) this.#applyChange(...redoResult);
887
- }
1286
+ this.#applyHistoryChange(command);
888
1287
  break;
889
1288
  }
890
1289
  }
1290
+ #applyHistoryChange(command) {
1291
+ const textDocument = this.#textDocument;
1292
+ if (textDocument === void 0) return;
1293
+ if (command === "undo" && !textDocument.canUndo || command === "redo" && !textDocument.canRedo) return;
1294
+ const selections = this.#selections;
1295
+ const selectionOffsets = selections?.map((selection) => [textDocument.offsetAt(selection.start), textDocument.offsetAt(selection.end)]);
1296
+ const result = command === "undo" ? textDocument.undo() : textDocument.redo();
1297
+ if (result === void 0) return;
1298
+ const [change, recordedSelections, lineAnnotations, selectionEdits] = result;
1299
+ const nextSelections = recordedSelections ?? (selections !== void 0 && selectionOffsets !== void 0 && selectionEdits !== void 0 ? remapSelectionsAfterEdits(textDocument, selections, selectionOffsets, selectionEdits) : void 0);
1300
+ this.#applyChange(change, nextSelections, lineAnnotations);
1301
+ }
1302
+ /** Applies one undoable command batch and records its resulting selections. */
1303
+ #applyCommandEdits(edits, resolveNextSelections) {
1304
+ const textDocument = this.#textDocument;
1305
+ const selections = this.#selections;
1306
+ if (textDocument === void 0 || selections === void 0) return;
1307
+ const remapSelections = resolveNextSelections === void 0;
1308
+ const selectionOffsets = remapSelections ? selections.map((selection) => [textDocument.offsetAt(selection.start), textDocument.offsetAt(selection.end)]) : void 0;
1309
+ const resolvedEdits = remapSelections ? edits.map((edit) => {
1310
+ const start = textDocument.offsetAt(edit.range.start);
1311
+ const end = textDocument.offsetAt(edit.range.end);
1312
+ return {
1313
+ start: Math.min(start, end),
1314
+ end: Math.max(start, end),
1315
+ text: edit.newText
1316
+ };
1317
+ }).sort((a, b) => {
1318
+ const startOrder = a.start - b.start;
1319
+ return startOrder !== 0 ? startOrder : a.end - b.end;
1320
+ }) : void 0;
1321
+ const change = textDocument.applyEdits(edits, true, selections, void 0, true);
1322
+ if (change === void 0) return;
1323
+ const nextSelections = remapSelections ? remapSelectionsAfterEdits(textDocument, selections, selectionOffsets, resolvedEdits) : resolveNextSelections(textDocument);
1324
+ textDocument.setLastUndoSelectionsAfter(nextSelections);
1325
+ this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1326
+ }
1327
+ /** Copies selected line blocks and keeps the selection in the requested copy. */
1328
+ #copySelectedLines(direction) {
1329
+ const textDocument = this.#textDocument;
1330
+ const selections = this.#selections;
1331
+ if (textDocument === void 0 || selections === void 0) return;
1332
+ const blocks = getSelectedLineBlocks(selections);
1333
+ if (blocks.length === 0) return;
1334
+ const copiedLinesBefore = [];
1335
+ let copiedLineCount = 0;
1336
+ const edits = [];
1337
+ for (const block of blocks) {
1338
+ copiedLinesBefore.push(copiedLineCount);
1339
+ const blockLineCount = block.endLine - block.startLine + 1;
1340
+ copiedLineCount += blockLineCount;
1341
+ const text = textDocument.getText({
1342
+ start: {
1343
+ line: block.startLine,
1344
+ character: 0
1345
+ },
1346
+ end: {
1347
+ line: block.endLine,
1348
+ character: textDocument.getLineLength(block.endLine)
1349
+ }
1350
+ });
1351
+ if (direction > 0) {
1352
+ const position = {
1353
+ line: block.startLine,
1354
+ character: 0
1355
+ };
1356
+ edits.push({
1357
+ range: {
1358
+ start: position,
1359
+ end: position
1360
+ },
1361
+ newText: text + textDocument.eol
1362
+ });
1363
+ } else if (block.endLine < textDocument.lineCount - 1) {
1364
+ const position = {
1365
+ line: block.endLine + 1,
1366
+ character: 0
1367
+ };
1368
+ edits.push({
1369
+ range: {
1370
+ start: position,
1371
+ end: position
1372
+ },
1373
+ newText: text + textDocument.eol
1374
+ });
1375
+ } else {
1376
+ const position = {
1377
+ line: block.endLine,
1378
+ character: textDocument.getLineLength(block.endLine)
1379
+ };
1380
+ edits.push({
1381
+ range: {
1382
+ start: position,
1383
+ end: position
1384
+ },
1385
+ newText: textDocument.eol + text
1386
+ });
1387
+ }
1388
+ }
1389
+ const nextSelections = selections.map((selection) => {
1390
+ const line = selection.start.line;
1391
+ let low = 0;
1392
+ let high = blocks.length - 1;
1393
+ while (low <= high) {
1394
+ const middle = low + high >>> 1;
1395
+ const block = blocks[middle];
1396
+ if (line < block.startLine) high = middle - 1;
1397
+ else if (line > block.endLine) low = middle + 1;
1398
+ else {
1399
+ low = middle;
1400
+ break;
1401
+ }
1402
+ }
1403
+ const blockIndex = Math.max(0, low <= high ? low : high);
1404
+ const block = blocks[blockIndex];
1405
+ const shift = copiedLinesBefore[blockIndex] + (direction > 0 ? block.endLine - block.startLine + 1 : 0);
1406
+ return {
1407
+ start: {
1408
+ ...selection.start,
1409
+ line: selection.start.line + shift
1410
+ },
1411
+ end: {
1412
+ ...selection.end,
1413
+ line: selection.end.line + shift
1414
+ },
1415
+ direction: selection.direction
1416
+ };
1417
+ });
1418
+ this.#applyCommandEdits(edits, () => nextSelections);
1419
+ }
1420
+ /** Inserts an indented blank line after each selection's final line. */
1421
+ #insertBlankLine() {
1422
+ const textDocument = this.#textDocument;
1423
+ const selections = this.#selections;
1424
+ if (textDocument === void 0 || selections === void 0) return;
1425
+ const selectionLines = selections.map((selection) => getCaretPosition(selection).line);
1426
+ const targetLines = Array.from(new Set(selectionLines)).sort((a, b) => a - b);
1427
+ const targetIndex = /* @__PURE__ */ new Map();
1428
+ const indents = /* @__PURE__ */ new Map();
1429
+ const edits = [];
1430
+ for (let index = 0; index < targetLines.length; index++) {
1431
+ const line = targetLines[index];
1432
+ const lineText = textDocument.getLineText(line);
1433
+ const indent = /^\s*/.exec(lineText)?.[0] ?? "";
1434
+ targetIndex.set(line, index);
1435
+ indents.set(line, indent);
1436
+ const position = {
1437
+ line,
1438
+ character: textDocument.getLineLength(line)
1439
+ };
1440
+ edits.push({
1441
+ range: {
1442
+ start: position,
1443
+ end: position
1444
+ },
1445
+ newText: textDocument.eol + indent
1446
+ });
1447
+ }
1448
+ const nextSelections = selections.map((_, index) => {
1449
+ const line = selectionLines[index];
1450
+ const position = {
1451
+ line: line + 1 + targetIndex.get(line),
1452
+ character: indents.get(line).length
1453
+ };
1454
+ return {
1455
+ start: position,
1456
+ end: position,
1457
+ direction: 0
1458
+ };
1459
+ });
1460
+ this.#applyCommandEdits(edits, () => nextSelections);
1461
+ }
891
1462
  #moveSelectedLines(direction) {
892
1463
  const textDocument = this.#textDocument;
893
1464
  const selections = this.#selections;
@@ -956,15 +1527,15 @@ var Editor = class {
956
1527
  this.#lineElementsCache.clear();
957
1528
  this.#lastAccessedCharX = void 0;
958
1529
  this.#metrics.clearTextWidthCache();
959
- if (contentWidthChanged && (this.#isWrap || lineAnnotations > 0)) {
960
- this.#lineYCache.clear();
1530
+ if (contentWidthChanged) {
961
1531
  this.#wrapLineOffsetsCache.clear();
1532
+ if (this.#isWrap || lineAnnotations > 0) this.#lineYCache.clear();
962
1533
  }
963
1534
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) {
964
1535
  this.#updateSelections(this.#selections ?? []);
965
- if (this.#selections !== void 0) this.focus();
1536
+ if (this.#selections !== void 0 && this.#contentHasFocus) this.focus();
966
1537
  }
967
- this.#markerRenderer?.removePopup();
1538
+ this.#markerRenderer?.removePopover();
968
1539
  this.#computeContentOffset(this.#contentElement);
969
1540
  };
970
1541
  #remeasureMetricsOnFontLoad() {
@@ -977,8 +1548,9 @@ var Editor = class {
977
1548
  this.#gutterWidthCache = void 0;
978
1549
  this.#contentWidthCache = void 0;
979
1550
  this.#resetCache();
1551
+ this.#wrapLineOffsetsCache.clear();
980
1552
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) this.#updateSelections(this.#selections ?? []);
981
- this.#markerRenderer?.removePopup();
1553
+ this.#markerRenderer?.removePopover();
982
1554
  });
983
1555
  }
984
1556
  #rerender(change, newLineAnnotations, renderRange = this.#renderRange, shouldUpdateBuffer) {
@@ -994,45 +1566,57 @@ var Editor = class {
994
1566
  const t2 = performance.now();
995
1567
  if (dirtyLines.size > 0) {
996
1568
  const children = contentEl.children;
997
- const dirtyLineIndexes = new Set(dirtyLines.keys());
998
- const startingLine = renderRange?.startingLine ?? 0;
999
- for (let i = change.startLine - startingLine; i < children.length; i++) {
1569
+ const dirtyLineIndexes = /* @__PURE__ */ new Set();
1570
+ for (const lineIndex of dirtyLines.keys()) if (this.#isLineRenderable(lineIndex)) dirtyLineIndexes.add(lineIndex);
1571
+ for (let i = 0; i < children.length; i++) {
1000
1572
  const child = children[i];
1001
1573
  if (child !== void 0) {
1002
1574
  const lineNumber = getLineNumberAttr(child);
1003
1575
  const lineType = child.dataset.lineType;
1004
1576
  if (lineNumber === void 0 || lineType === "change-deletion") continue;
1005
1577
  const lineIndex = lineNumber - 1;
1006
- if (dirtyLines.has(lineIndex)) {
1578
+ if (dirtyLineIndexes.has(lineIndex)) {
1007
1579
  const tokens = dirtyLines.get(lineIndex);
1008
- child.replaceChildren(...renderLineTokens(tokens, tokenizer.themeType));
1580
+ child.replaceChildren(...renderLineTokens(tokens));
1009
1581
  dirtyLineIndexes.delete(lineIndex);
1010
1582
  if (dirtyLineIndexes.size === 0) break;
1011
1583
  }
1012
1584
  }
1013
1585
  }
1014
- if (dirtyLineIndexes.size > 0) for (const lineIndex of dirtyLineIndexes) {
1015
- const tokens = dirtyLines.get(lineIndex);
1016
- const lineNumber = String(lineIndex + 1);
1017
- h("div", {
1018
- dataset: {
1019
- line: lineNumber,
1020
- lineType: "context",
1021
- lineIndex: lineIndex.toString()
1022
- },
1023
- children: renderLineTokens(tokens, tokenizer.themeType)
1024
- }, contentEl);
1025
- if (gutterEl !== void 0) h("div", {
1026
- dataset: {
1027
- lineType: "context",
1028
- columnNumber: lineNumber,
1029
- lineIndex: lineIndex.toString()
1030
- },
1031
- children: [h("span", {
1032
- dataset: { lineNumberContent: "" },
1033
- textContent: lineNumber
1034
- })]
1035
- }, gutterEl);
1586
+ if (dirtyLineIndexes.size > 0) {
1587
+ let lastRenderedLineNumber = 0;
1588
+ for (let i = children.length - 1; i >= 0; i--) {
1589
+ const child = children[i];
1590
+ const lineNumber = getLineNumberAttr(child);
1591
+ if (lineNumber !== void 0 && child.dataset.lineType !== "change-deletion") {
1592
+ lastRenderedLineNumber = lineNumber;
1593
+ break;
1594
+ }
1595
+ }
1596
+ for (const lineIndex of dirtyLineIndexes) {
1597
+ if (lineIndex < lastRenderedLineNumber) continue;
1598
+ const tokens = dirtyLines.get(lineIndex);
1599
+ const lineNumber = String(lineIndex + 1);
1600
+ h("div", {
1601
+ dataset: {
1602
+ line: lineNumber,
1603
+ lineType: "context",
1604
+ lineIndex: lineIndex.toString()
1605
+ },
1606
+ children: renderLineTokens(tokens)
1607
+ }, contentEl);
1608
+ if (gutterEl !== void 0) h("div", {
1609
+ dataset: {
1610
+ lineType: "context",
1611
+ columnNumber: lineNumber,
1612
+ lineIndex: lineIndex.toString()
1613
+ },
1614
+ children: [h("span", {
1615
+ dataset: { lineNumberContent: "" },
1616
+ textContent: lineNumber
1617
+ })]
1618
+ }, gutterEl);
1619
+ }
1036
1620
  }
1037
1621
  }
1038
1622
  if (change.lineDelta < 0) for (const children of [contentEl.children, gutterEl?.children ?? []]) for (let i = children.length - 1; i >= 0; i--) {
@@ -1053,9 +1637,9 @@ var Editor = class {
1053
1637
  contentEl.style.gridRow = "span " + gridRow;
1054
1638
  if (gutterEl !== void 0) gutterEl.style.gridRow = "span " + gridRow;
1055
1639
  }
1056
- fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange);
1640
+ fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange, didLineCountChange);
1057
1641
  if (didLineCountChange) fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer);
1058
- if (this.#isDiff && (this.#diffSyle === "unified" || didLineCountChange)) this.#resetCache();
1642
+ if (didLineCountChange || this.#isDiff && this.#diffSyle === "unified") this.#resetCache();
1059
1643
  if (newLineAnnotations !== void 0) {
1060
1644
  this.#lineAnnotations = newLineAnnotations;
1061
1645
  renderLineAnnotations(newLineAnnotations, contentEl, gutterEl);
@@ -1075,7 +1659,7 @@ var Editor = class {
1075
1659
  case "insertCompositionText": break;
1076
1660
  case "insertLineBreak":
1077
1661
  case "insertParagraph":
1078
- this.#replaceSelectionText("\n");
1662
+ this.#replaceSelectionText(this.#textDocument?.eol ?? "\n");
1079
1663
  break;
1080
1664
  case "deleteContentBackward":
1081
1665
  this.#deleteSelectionText();
@@ -1087,9 +1671,6 @@ var Editor = class {
1087
1671
  case "deleteHardLineBackward":
1088
1672
  this.#deleteSoftLineBackward();
1089
1673
  break;
1090
- case "deleteHardLineForward":
1091
- this.#deleteHardLineForward();
1092
- break;
1093
1674
  case "deleteWordBackward":
1094
1675
  this.#deleteWordBackward();
1095
1676
  break;
@@ -1101,7 +1682,7 @@ var Editor = class {
1101
1682
  break;
1102
1683
  }
1103
1684
  }
1104
- #focus(position, preventScroll = true) {
1685
+ #focus(position, preventScroll = true, shouldFocus) {
1105
1686
  this.#contentHasFocus = true;
1106
1687
  if (position !== void 0) {
1107
1688
  this.#shouldIgnoreSelectionChange = true;
@@ -1110,13 +1691,115 @@ var Editor = class {
1110
1691
  end: position,
1111
1692
  direction: 0
1112
1693
  });
1113
- requestAnimationFrame(() => {
1694
+ queueRender(() => {
1695
+ if (shouldFocus?.() === false) {
1696
+ this.#shouldIgnoreSelectionChange = false;
1697
+ return;
1698
+ }
1114
1699
  this.#contentElement?.focus({ preventScroll });
1115
- requestAnimationFrame(() => {
1700
+ queueRender(() => {
1116
1701
  this.#shouldIgnoreSelectionChange = false;
1117
1702
  });
1118
1703
  });
1119
- } else this.#contentElement?.focus({ preventScroll });
1704
+ } else if (shouldFocus?.() !== false) this.#contentElement?.focus({ preventScroll });
1705
+ else this.#contentHasFocus = false;
1706
+ }
1707
+ #restoreReplacementFocus(position) {
1708
+ const request = this.#replacementFocusRequest;
1709
+ if (request == null) return;
1710
+ if (this.#retainSearchPanelFocus) {
1711
+ this.#replacementFocusRequest = void 0;
1712
+ this.#contentHasFocus = false;
1713
+ this.#shouldIgnoreSelectionChange = false;
1714
+ return;
1715
+ }
1716
+ const contentElement = this.#contentElement;
1717
+ if (contentElement == null || !this.#replacementFocusIsAvailable(contentElement)) {
1718
+ this.#replacementFocusRequest = void 0;
1719
+ this.#contentHasFocus = false;
1720
+ return;
1721
+ }
1722
+ const shouldFocus = () => {
1723
+ if (this.#replacementFocusRequest !== request) return false;
1724
+ const shouldRestore = this.#replacementFocusIsAvailable(contentElement);
1725
+ this.#replacementFocusRequest = void 0;
1726
+ return shouldRestore;
1727
+ };
1728
+ if (position != null) {
1729
+ this.#focus(position, true, shouldFocus);
1730
+ return;
1731
+ }
1732
+ const primarySelection = this.#selections?.at(-1);
1733
+ if (primarySelection == null) {
1734
+ this.#focus(void 0, true, shouldFocus);
1735
+ return;
1736
+ }
1737
+ const primaryPosition = primarySelection.direction === -1 ? primarySelection.end : primarySelection.start;
1738
+ this.#focus(primaryPosition, true, shouldFocus);
1739
+ }
1740
+ #replacementFocusIsAvailable(contentElement) {
1741
+ const fileContainer = this.#fileContainer;
1742
+ const shadowRoot = fileContainer?.shadowRoot;
1743
+ if (fileContainer == null || shadowRoot == null || this.#contentElement !== contentElement || !shadowRoot.contains(contentElement)) return false;
1744
+ const { activeElement: shadowActiveElement } = shadowRoot;
1745
+ if (shadowActiveElement === contentElement || shadowActiveElement != null && contentElement.contains(shadowActiveElement)) return true;
1746
+ if (shadowActiveElement != null) return false;
1747
+ const { ownerDocument } = fileContainer;
1748
+ const { activeElement } = ownerDocument;
1749
+ return activeElement == null || activeElement === ownerDocument.body || activeElement === ownerDocument.documentElement || activeElement === fileContainer;
1750
+ }
1751
+ #getFirstVisibleLineNumber(offset = 0) {
1752
+ const contentElement = this.#contentElement;
1753
+ const fileContainer = this.#fileContainer;
1754
+ if (this.#textDocument == null || contentElement == null || fileContainer == null) return;
1755
+ const viewport = this.#fileInstance?.getEditorViewport?.() ?? this.#getDefaultEditorViewport(fileContainer);
1756
+ let viewportTop;
1757
+ let viewportBottom;
1758
+ if (viewport instanceof HTMLElement) {
1759
+ const viewportRect = viewport.getBoundingClientRect();
1760
+ viewportTop = viewportRect.top;
1761
+ viewportBottom = viewportRect.bottom;
1762
+ } else {
1763
+ const viewportWindow = viewport.defaultView;
1764
+ if (viewportWindow == null) return;
1765
+ viewportTop = 0;
1766
+ viewportBottom = viewportWindow.innerHeight;
1767
+ }
1768
+ const stickyHeader = fileContainer.shadowRoot?.querySelector("[data-diffs-header][data-sticky]");
1769
+ if (stickyHeader != null) viewportTop = Math.max(viewportTop, stickyHeader.getBoundingClientRect().bottom);
1770
+ if (Number.isFinite(offset)) viewportTop += Math.max(0, offset);
1771
+ if (viewportBottom <= viewportTop) return;
1772
+ for (const child of contentElement.children) {
1773
+ const row = child;
1774
+ const lineNumber = getLineNumberAttr(row);
1775
+ const lineType = row.dataset.lineType;
1776
+ if (lineNumber == null || lineType == null || !isLineEditable(lineType)) continue;
1777
+ const rowRect = row.getBoundingClientRect();
1778
+ if (rowRect.bottom <= rowRect.top) continue;
1779
+ if (rowRect.top >= viewportTop && rowRect.top < viewportBottom) return lineNumber;
1780
+ }
1781
+ }
1782
+ #getDefaultEditorViewport(fileContainer) {
1783
+ const ownerDocument = fileContainer.ownerDocument;
1784
+ let element = fileContainer.parentElement;
1785
+ while (element != null && element !== ownerDocument.body && element !== ownerDocument.documentElement) {
1786
+ const overflowY = element.ownerDocument.defaultView?.getComputedStyle(element).overflowY;
1787
+ if (overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") return element;
1788
+ element = element.parentElement;
1789
+ }
1790
+ return ownerDocument;
1791
+ }
1792
+ #focusAtPosition(position, preventScroll) {
1793
+ const selection = {
1794
+ start: position,
1795
+ end: position,
1796
+ direction: 0
1797
+ };
1798
+ this.#canMountSelectionAction = false;
1799
+ this.#updateSelections([selection]);
1800
+ this.#revealLineIfCollapsed(position.line);
1801
+ if (preventScroll) this.#focus(position, true);
1802
+ else this.#scrollToPrimaryCaret();
1120
1803
  }
1121
1804
  #setWindowSelection(selection) {
1122
1805
  const winSelection = window.getSelection();
@@ -1149,7 +1832,11 @@ var Editor = class {
1149
1832
  block: scrollPosition,
1150
1833
  inline: "nearest"
1151
1834
  });
1152
- if (!noFocus) this.#focus(primarySelection.direction === -1 ? primarySelection.end : primarySelection.start);
1835
+ if (!noFocus) {
1836
+ const position = primarySelection.direction === -1 ? primarySelection.end : primarySelection.start;
1837
+ if (this.#replacementFocusRequest !== void 0) this.#restoreReplacementFocus(position);
1838
+ else this.#focus(position);
1839
+ }
1153
1840
  } else {
1154
1841
  const pos = getCaretPosition(primarySelection);
1155
1842
  this.#scrollToLine(pos.line, pos.character, noFocus);
@@ -1179,10 +1866,14 @@ var Editor = class {
1179
1866
  block: "center",
1180
1867
  inline: "nearest"
1181
1868
  });
1182
- if (!noFocus) this.#focus({
1183
- line,
1184
- character: char
1185
- });
1869
+ if (!noFocus) {
1870
+ const position = {
1871
+ line,
1872
+ character: char
1873
+ };
1874
+ if (this.#replacementFocusRequest !== void 0) this.#restoreReplacementFocus(position);
1875
+ else this.#focus(position);
1876
+ }
1186
1877
  this.#scrollingToLine = void 0;
1187
1878
  this.#scrollingToLineChar = void 0;
1188
1879
  this.#scrollingToLineFixed = false;
@@ -1196,7 +1887,7 @@ var Editor = class {
1196
1887
  block: "center",
1197
1888
  inline: "nearest"
1198
1889
  });
1199
- if (modelLinePosition.height > 0) {
1890
+ if (modelLinePosition.height > 0 && this.#isLineRenderable(line)) {
1200
1891
  this.#scrollingToLine = line;
1201
1892
  this.#scrollingToLineChar = char;
1202
1893
  this.#scrollingToLineNoFocus = noFocus;
@@ -1223,7 +1914,7 @@ var Editor = class {
1223
1914
  block: "center",
1224
1915
  inline: "nearest"
1225
1916
  });
1226
- if (this.#scrollingToLine === line && (yFix === 0 || this.#scrollingToLineFixed)) {
1917
+ if (this.#scrollingToLine === line && (yFix === 0 || this.#scrollingToLineFixed) || !this.#isLineRenderable(line)) {
1227
1918
  this.#scrollingToLine = void 0;
1228
1919
  this.#scrollingToLineChar = void 0;
1229
1920
  this.#scrollingToLineFixed = false;
@@ -1277,7 +1968,7 @@ var Editor = class {
1277
1968
  }
1278
1969
  this.#setDeletedTextSelectionActive(true);
1279
1970
  this.#updateSelections([]);
1280
- [...deletionsCode.querySelectorAll("[data-gutter] > [data-column-number]")][focusIndex]?.setAttribute("data-selected-line", "single");
1971
+ this.#setEditorActiveLineSafe(getLineNumberAttr(focusContent) ?? null, true, "deletions");
1281
1972
  const lo = Math.min(anchorIndex, focusIndex);
1282
1973
  const hi = Math.max(anchorIndex, focusIndex);
1283
1974
  this.#deletedSelectionText = lo < 0 ? "" : rows.slice(lo, hi + 1).filter((row) => row.getAttribute("data-line-type") === "change-deletion").map((row) => row.textContent ?? "").join("\n");
@@ -1315,19 +2006,18 @@ var Editor = class {
1315
2006
  if (pre == null) return;
1316
2007
  if (active) {
1317
2008
  pre.setAttribute("data-deleted-text-selection", "");
1318
- for (const highlighted of pre.querySelectorAll("[data-selected-line]")) highlighted.removeAttribute("data-selected-line");
2009
+ this.#setEditorActiveLineSafe(null);
1319
2010
  this.#deletedSelectionText = "";
1320
2011
  } else {
1321
2012
  pre.removeAttribute("data-deleted-text-selection");
1322
2013
  this.#deletedSelectionText = "";
1323
2014
  }
1324
2015
  }
1325
- #setSelectedLinesSafe(range, lineNumberOnly = false) {
2016
+ #setEditorActiveLineSafe(lineNumber, lineNumberOnly = false, side = "additions") {
1326
2017
  try {
1327
- this.#fileInstance?.setSelectedLines(range, {
1328
- notify: false,
1329
- activeLineSide: "additions",
1330
- lineNumberOnly
2018
+ this.#fileInstance?.setEditorActiveLine(lineNumber, {
2019
+ lineNumberOnly,
2020
+ side
1331
2021
  });
1332
2022
  } catch {}
1333
2023
  }
@@ -1341,7 +2031,7 @@ var Editor = class {
1341
2031
  #updateSelections(selections) {
1342
2032
  this.__postponeBgTokenizeToNextFrame();
1343
2033
  this.#primaryCaretElement = void 0;
1344
- this.#setSelectedLinesSafe(null);
2034
+ this.#setEditorActiveLineSafe(null);
1345
2035
  if (selections.length === 0 && this.#matches === void 0 && this.#markerRenderer === void 0) {
1346
2036
  this.#selections = void 0;
1347
2037
  this.#overlayElements?.forEach((el) => el.remove());
@@ -1361,10 +2051,7 @@ var Editor = class {
1361
2051
  this.#selections = normalizedSelections;
1362
2052
  const hasNonEmptySelection = normalizedSelections.some((selection) => !isCollapsedSelection(selection));
1363
2053
  const caretLine = getCaretPosition(primarySelection).line + 1;
1364
- this.#setSelectedLinesSafe({
1365
- start: caretLine,
1366
- end: caretLine
1367
- }, hasNonEmptySelection);
2054
+ this.#setEditorActiveLineSafe(caretLine, hasNonEmptySelection);
1368
2055
  for (const selection of normalizedSelections) {
1369
2056
  if (!isCollapsedSelection(selection)) this.#renderSelection(renderCtx, "selection", selection);
1370
2057
  this.#renderCaret(renderCtx, selection, selection === primarySelection);
@@ -1374,16 +2061,50 @@ var Editor = class {
1374
2061
  }
1375
2062
  const textDocument = this.#textDocument;
1376
2063
  if (this.#matches !== void 0 && textDocument !== void 0) {
2064
+ const matches = this.#matches;
2065
+ const renderRange = this.#renderRange;
2066
+ const renderedLineRanges = renderRange !== void 0 && Number.isFinite(renderRange.totalLines) || this.#isDiff && this.#fileInstance?.options.expandUnchanged !== true ? this.#getRenderedEditableLineRanges() : void 0;
1377
2067
  const primarySelection = this.#selections?.at(-1);
1378
2068
  const primaryStartOffset = primarySelection !== void 0 ? textDocument.offsetAt(primarySelection.start) : -1;
1379
2069
  const primaryEndOffset = primarySelection !== void 0 ? textDocument.offsetAt(primarySelection.end) : -1;
1380
- for (const [startOffset, endOffset] of this.#matches) {
1381
- const range = {
1382
- start: textDocument.positionAt(startOffset),
1383
- end: textDocument.positionAt(endOffset)
1384
- };
1385
- const isFocused = primaryStartOffset === startOffset && primaryEndOffset === endOffset;
1386
- this.#renderSelection(renderCtx, "match", range, isFocused ? "focus" : void 0);
2070
+ let firstMatchIndex = 0;
2071
+ const rangeCount = renderedLineRanges?.length ?? 1;
2072
+ for (let rangeIndex = 0; rangeIndex < rangeCount; rangeIndex++) {
2073
+ let visibleEndOffset = Infinity;
2074
+ const lineRange = renderedLineRanges?.[rangeIndex];
2075
+ if (lineRange !== void 0) {
2076
+ const [firstLine, lastLine] = lineRange;
2077
+ const visibleStartOffset = textDocument.offsetAt({
2078
+ line: firstLine,
2079
+ character: 0
2080
+ });
2081
+ const endLine = lastLine + 1;
2082
+ visibleEndOffset = endLine < textDocument.lineCount ? textDocument.offsetAt({
2083
+ line: endLine,
2084
+ character: 0
2085
+ }) : textDocument.offsetAt({
2086
+ line: endLine - 1,
2087
+ character: textDocument.getLineLength(endLine - 1)
2088
+ });
2089
+ let low = firstMatchIndex;
2090
+ let high = matches.length;
2091
+ while (low < high) {
2092
+ const middle = low + Math.floor((high - low) / 2);
2093
+ if (matches[middle][1] <= visibleStartOffset) low = middle + 1;
2094
+ else high = middle;
2095
+ }
2096
+ firstMatchIndex = low;
2097
+ }
2098
+ for (; firstMatchIndex < matches.length; firstMatchIndex++) {
2099
+ const [startOffset, endOffset] = matches[firstMatchIndex];
2100
+ if (startOffset >= visibleEndOffset) break;
2101
+ const range = {
2102
+ start: textDocument.positionAt(startOffset),
2103
+ end: textDocument.positionAt(endOffset)
2104
+ };
2105
+ const isFocused = primaryStartOffset === startOffset && primaryEndOffset === endOffset;
2106
+ this.#renderSelection(renderCtx, "match", range, isFocused ? "focus" : void 0);
2107
+ }
1387
2108
  }
1388
2109
  }
1389
2110
  if (this.#markerRenderer !== void 0 && textDocument !== void 0) for (const marker of this.#markerRenderer.markers) this.#renderSelection(renderCtx, "marker", marker, markerSeverityDatasetKey(marker.severity));
@@ -1396,7 +2117,14 @@ var Editor = class {
1396
2117
  #renderSelection(renderCtx, type, range, extraDataset) {
1397
2118
  if (this.#textDocument === void 0) return;
1398
2119
  const { start, end } = range;
1399
- for (let line = start.line; line <= end.line; line++) {
2120
+ let firstLine = start.line;
2121
+ let endLine = end.line + 1;
2122
+ if (this.#renderRange !== void 0) {
2123
+ const { startingLine, totalLines } = this.#renderRange;
2124
+ firstLine = Math.max(firstLine, startingLine);
2125
+ endLine = Math.min(endLine, startingLine + totalLines);
2126
+ }
2127
+ for (let line = firstLine; line < endLine; line++) {
1400
2128
  if (!this.#isLineVisible(line)) continue;
1401
2129
  const isLastLine = line === end.line;
1402
2130
  const startChar = line === start.line ? start.character : 0;
@@ -1453,8 +2181,10 @@ var Editor = class {
1453
2181
  const rounded = (this.#options.roundedSelection ?? true) && type === "selection";
1454
2182
  const addRoundedCorner = (line, wrapLine, left, radius) => {
1455
2183
  const top = this.#getLineY(line) + wrapLine * lineHeight;
1456
- const cornerBg = this.#lineBackgroundColor(line);
1457
- const css = `width:${ch}px;transform:translateX(${left}px) translateY(${top}px);` + (cornerBg !== void 0 ? `--diffs-selection-corner-bg:${cornerBg};` : "");
2184
+ const lineElement = this.#getLineElement(line);
2185
+ let cornerBg = "initial";
2186
+ if (lineElement !== void 0 && (lineElement.dataset.lineType === "change-addition" || lineElement.dataset.selectedLine !== void 0)) cornerBg = getComputedStyle(lineElement).getPropertyValue("--diffs-line-bg");
2187
+ const css = `width:${ch}px;transform:translateX(${left}px) translateY(${top}px);--diffs-selection-corner-bg:${cornerBg}`;
1458
2188
  const dataset = {
1459
2189
  selectionCorner: "",
1460
2190
  [radius]: ""
@@ -1573,13 +2303,14 @@ var Editor = class {
1573
2303
  return;
1574
2304
  }
1575
2305
  if (this.#selectionAction === void 0) {
2306
+ if (!this.#canMountSelectionAction) return;
1576
2307
  const getActiveSelection = () => this.#selections?.at(-1) ?? primarySelection;
1577
2308
  const selectionActionElement = renderSelectionAction({
1578
2309
  textDocument,
1579
2310
  get selection() {
1580
2311
  return getActiveSelection();
1581
2312
  },
1582
- applyEdits: (edits) => this.applyEdits(edits, true),
2313
+ applyEdits: (edits) => this.applyEdits(edits),
1583
2314
  getSelectionText: () => this.#textDocument?.getText(getActiveSelection()) ?? "",
1584
2315
  replaceSelectionText: (text) => {
1585
2316
  this.#replaceSelectionText(text, [getActiveSelection()]);
@@ -1590,7 +2321,7 @@ var Editor = class {
1590
2321
  }
1591
2322
  });
1592
2323
  this.#selectionAction = new SelectionActionWidget(selectionActionElement, overlayElement, () => this.#updateSelectionActionPopover());
1593
- this.#getPopoverManager().resetPlacement();
2324
+ this.#getPopoverManager().resetPlacement(SELECTION_ACTION_POPOVER_PLACEMENT_KEY);
1594
2325
  }
1595
2326
  const lineHeight = this.#metrics.lineHeight;
1596
2327
  const isBackward = primarySelection.direction === -1;
@@ -1624,17 +2355,19 @@ var Editor = class {
1624
2355
  const atDocumentEdge = isBackward ? head.line < 3 : head.line >= lineCount - 3;
1625
2356
  const canUseFallback = this.#isLineVisible(fallback.anchor.line);
1626
2357
  const popoverManager = this.#getPopoverManager();
2358
+ const viewport = popoverManager.getPlacementBounds();
1627
2359
  const useFallback = canUseFallback && popoverManager.choosePlacement({
1628
2360
  preferred: preferredGeometry,
1629
2361
  fallback: fallbackGeometry,
1630
- viewport: popoverManager.getPlacementBounds(),
2362
+ viewport,
1631
2363
  popoverHeight,
1632
- atDocumentEdge
2364
+ atDocumentEdge,
2365
+ placementKey: SELECTION_ACTION_POPOVER_PLACEMENT_KEY
1633
2366
  }) === "fallback";
1634
- if (!canUseFallback) popoverManager.setPlacement("preferred");
2367
+ if (!canUseFallback) popoverManager.setPlacement("preferred", SELECTION_ACTION_POPOVER_PLACEMENT_KEY);
1635
2368
  const { placeAbove } = useFallback ? fallback : preferred;
1636
2369
  const { left, anchorTop } = useFallback ? fallbackGeometry : preferredGeometry;
1637
- this.#selectionAction.reposition(left, anchorTop, this.#getGutterWidth(), placeAbove);
2370
+ this.#selectionAction.reposition(left, anchorTop, this.#getGutterWidth(), placeAbove, viewport);
1638
2371
  }
1639
2372
  #openSearchPanel(mode) {
1640
2373
  if (this.#searchPanel !== void 0) {
@@ -1666,6 +2399,7 @@ var Editor = class {
1666
2399
  const scrollToMatch = ([startOffset, endOffset], retainFocus) => {
1667
2400
  const nextSelection = createSelectionFromAnchorAndFocusOffsets(textDocument, startOffset, endOffset);
1668
2401
  this.#updateSelections([nextSelection]);
2402
+ this.#revealLineIfCollapsed(getCaretPosition(nextSelection).line);
1669
2403
  this.#scrollToPrimaryCaret(true);
1670
2404
  this.#retainSearchPanelFocus = retainFocus;
1671
2405
  };
@@ -1732,6 +2466,18 @@ var Editor = class {
1732
2466
  if (textDocument === void 0 || selections === void 0) return "";
1733
2467
  return getSelectionText(textDocument, selections);
1734
2468
  }
2469
+ #getSelectionClipboardTexts() {
2470
+ const textDocument = this.#textDocument;
2471
+ const selections = this.#selections;
2472
+ if (textDocument === void 0 || selections === void 0) return [];
2473
+ return getSelectionClipboardTexts(textDocument, selections);
2474
+ }
2475
+ /** Writes both the portable text and the selection pairing metadata. */
2476
+ #writeSelectionClipboardData(clipboardData, text, selectionTexts) {
2477
+ if (clipboardData === null) return;
2478
+ clipboardData.setData("text", text);
2479
+ if (selectionTexts.length > 1) clipboardData.setData(MULTI_SELECTION_CLIPBOARD_TYPE, JSON.stringify(selectionTexts));
2480
+ }
1735
2481
  #cutSelectionText() {
1736
2482
  const textDocument = this.#textDocument;
1737
2483
  const selections = this.#selections;
@@ -1762,15 +2508,15 @@ var Editor = class {
1762
2508
  textDocument.setLastUndoSelectionsAfter(nextSelections);
1763
2509
  this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1764
2510
  }
1765
- #replaceSelectionText(text, selections = this.#selections, undoBoundary = false) {
2511
+ #replaceSelectionText(text, selections = this.#selections, undoBoundary = false, textOrder = "selection") {
1766
2512
  if (selections === void 0) return;
1767
2513
  const textDocument = this.#textDocument;
1768
2514
  const primarySelection = selections.at(-1);
1769
2515
  if (textDocument === void 0 || primarySelection === void 0) return;
1770
- const { nextSelections, change } = Array.isArray(text) && text.length === selections.length ? applyTextReplaceToSelections(textDocument, selections, text, this.#lineAnnotations, undoBoundary) : applyTextChangeToSelections(textDocument, selections, {
2516
+ const { nextSelections, change } = Array.isArray(text) && text.length === selections.length ? applyTextReplaceToSelections(textDocument, selections, text, this.#lineAnnotations, undoBoundary, textOrder) : applyTextChangeToSelections(textDocument, selections, {
1771
2517
  start: textDocument.offsetAt(primarySelection.start),
1772
2518
  end: textDocument.offsetAt(primarySelection.end),
1773
- text: Array.isArray(text) ? text.join("\n") : text
2519
+ text: Array.isArray(text) ? text.join(textDocument.eol) : text
1774
2520
  }, this.#lineAnnotations, void 0, undoBoundary);
1775
2521
  if (change !== void 0) this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1776
2522
  }
@@ -1817,26 +2563,16 @@ var Editor = class {
1817
2563
  const { nextSelections, change } = applyTransposeToSelections(textDocument, selections, this.#lineAnnotations);
1818
2564
  if (change !== void 0) this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1819
2565
  }
1820
- #getFileRef() {
1821
- const fileInfo = this.#fileInfo;
1822
- const textDocument = this.#textDocument;
1823
- if (fileInfo === void 0 || textDocument === void 0) return;
1824
- const file = { ...fileInfo };
1825
- Object.defineProperty(file, "contents", {
1826
- enumerable: true,
1827
- get: () => textDocument.getText()
1828
- });
1829
- return file;
1830
- }
1831
2566
  #applyChange(change, newSelections, newLineAnnotations, options) {
1832
- const fileRef = this.#getFileRef();
2567
+ const fileRef = this.getFile();
1833
2568
  const onChange = this.#options.onChange;
1834
2569
  if (fileRef !== void 0 && onChange !== void 0) onChange(fileRef, newLineAnnotations ?? this.#lineAnnotations);
1835
2570
  if (change.lineDelta !== 0 || this.#isWrap) {
1836
2571
  for (const line of this.#lineYCache.keys()) if (line >= change.startLine) this.#lineYCache.delete(line);
1837
2572
  }
1838
- if (this.#isWrap) {
1839
- for (const line of this.#wrapLineOffsetsCache.keys()) if (line >= change.startLine) this.#wrapLineOffsetsCache.delete(line);
2573
+ if (this.#wrapLineOffsetsCache.size > 0) {
2574
+ if (change.lineDelta === 0 && (change.changedLineRanges.length === 1 || change.changedLineChanges?.every(([, , editLineDelta]) => editLineDelta === 0) === true)) for (const [rangeStart, rangeEnd] of change.changedLineRanges) for (let line = rangeStart; line <= rangeEnd; line++) this.#wrapLineOffsetsCache.delete(line);
2575
+ else for (const line of this.#wrapLineOffsetsCache.keys()) if (line >= change.startLine) this.#wrapLineOffsetsCache.delete(line);
1840
2576
  }
1841
2577
  this.#lastAccessedCharX = void 0;
1842
2578
  let renderRange = this.#renderRange;
@@ -1847,7 +2583,7 @@ var Editor = class {
1847
2583
  if (primarySelection.end.line >= renderRangeEndLine) {
1848
2584
  const widenedTotalLines = primarySelection.end.line - renderRange.startingLine + 1;
1849
2585
  const maxWidenLines = this.#viewportWindowLines === void 0 || this.#viewportWindowLines === Infinity ? Infinity : this.#viewportWindowLines * MAX_EDIT_WIDEN_WINDOW_MULTIPLE;
1850
- if (change.startLine <= renderRangeEndLine && widenedTotalLines <= maxWidenLines) {
2586
+ if (change.startLine <= renderRangeEndLine && widenedTotalLines <= maxWidenLines && this.#isLineRenderable(primarySelection.end.line)) {
1851
2587
  if (primarySelection.end.line > renderRangeEndLine) shouldUpdateBuffer = true;
1852
2588
  renderRange = {
1853
2589
  ...renderRange,
@@ -1862,6 +2598,8 @@ var Editor = class {
1862
2598
  if (newSelections !== void 0) {
1863
2599
  this.#updateSelections(newSelections);
1864
2600
  if (options?.skipFocus !== true) {
2601
+ const revealTarget = newSelections.at(-1);
2602
+ if (revealTarget !== void 0) this.#revealLineIfCollapsed(getCaretPosition(revealTarget).line);
1865
2603
  if (this.#primaryCaretElement !== void 0) requestAnimationFrame(() => {
1866
2604
  this.#primaryCaretElement?.scrollIntoView({
1867
2605
  block: "nearest",
@@ -1885,39 +2623,31 @@ var Editor = class {
1885
2623
  }
1886
2624
  }
1887
2625
  }
1888
- #lineBackgroundColor(line) {
1889
- const lineElement = this.#getLineElement(line);
1890
- if (lineElement === void 0) return;
1891
- if (navigator.userAgent.includes("jsdom")) return;
1892
- const backgroundColor = getComputedStyle(lineElement, "::after").backgroundColor;
1893
- return backgroundColor === "" || backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)" ? void 0 : backgroundColor;
1894
- }
1895
- #getRenderedEditableLineRange() {
2626
+ #getRenderedEditableLineRanges() {
1896
2627
  const contentElement = this.#contentElement;
1897
- if (contentElement === void 0) return;
1898
- let first;
1899
- let last;
2628
+ if (contentElement === void 0) return [];
2629
+ const ranges = [];
1900
2630
  for (const child of contentElement.children) {
1901
2631
  const el = child;
1902
2632
  const lineType = el.dataset.lineType;
1903
2633
  const lineNumber = getLineNumberAttr(el);
1904
2634
  if (lineNumber === void 0 || lineType === void 0 || !isLineEditable(lineType)) continue;
1905
2635
  const line = lineNumber - 1;
1906
- if (first === void 0 || line < first) first = line;
1907
- if (last === void 0 || line > last) last = line;
2636
+ const current = ranges.at(-1);
2637
+ if (current !== void 0 && line <= current[1] + 1) current[1] = Math.max(current[1], line);
2638
+ else ranges.push([line, line]);
1908
2639
  }
1909
- return first === void 0 || last === void 0 ? void 0 : {
1910
- first,
1911
- last
1912
- };
2640
+ return ranges;
1913
2641
  }
1914
2642
  #getLineElement(line) {
1915
2643
  let lineElement = this.#lineElementsCache.get(line);
1916
2644
  if (lineElement !== void 0) return lineElement ?? void 0;
2645
+ const renderRange = this.#renderRange;
2646
+ if (renderRange !== void 0 && (line < renderRange.startingLine || line >= renderRange.startingLine + renderRange.totalLines)) return;
1917
2647
  const contentElement = this.#contentElement;
1918
2648
  if (contentElement === void 0) return;
1919
- if (this.#renderRange !== void 0) {
1920
- const { startingLine } = this.#renderRange;
2649
+ if (renderRange !== void 0) {
2650
+ const { startingLine } = renderRange;
1921
2651
  const { children } = contentElement;
1922
2652
  for (let i = line - startingLine; i <= children.length; i++) {
1923
2653
  const child = children[i];
@@ -2008,6 +2738,7 @@ var Editor = class {
2008
2738
  #wrapLineText(line) {
2009
2739
  const cachedOffsets = this.#wrapLineOffsetsCache.get(line);
2010
2740
  if (cachedOffsets !== void 0) return cachedOffsets;
2741
+ if (this.#wrapLineOffsetsCache.size >= MAX_WRAP_OFFSETS_CACHE_LINES) this.#wrapLineOffsetsCache.clear();
2011
2742
  const lineText = this.#textDocument?.getLineText(line);
2012
2743
  if (lineText === void 0 || lineText.length === 0) {
2013
2744
  const offsets = new Uint32Array([0]);
@@ -2037,21 +2768,30 @@ var Editor = class {
2037
2768
  try {
2038
2769
  const unicodeOffsets = getUnicodeMeasurementOffsets(lineText);
2039
2770
  const wrapLineStartLeft = div.getBoundingClientRect().left + this.#metrics.ch;
2040
- let previousOffset = 0;
2041
- let lastTop = Number.NEGATIVE_INFINITY;
2042
- for (let i = 0, offsetIndex = 0; i < lineText.length;) {
2043
- const nextOffset = unicodeOffsets === void 0 ? i + 1 : unicodeOffsets[offsetIndex + 1];
2044
- range.setStart(textNode, i);
2045
- range.setEnd(textNode, nextOffset);
2046
- const { left, top } = range.getBoundingClientRect();
2047
- if (top > lastTop) {
2048
- const startsPastLineStart = isSafari() && starts.length > 0 && left - wrapLineStartLeft > this.#metrics.ch / 2;
2049
- starts.push(startsPastLineStart ? previousOffset : i);
2050
- lastTop = top;
2771
+ const graphemeCount = unicodeOffsets === void 0 ? lineText.length : unicodeOffsets.length - 1;
2772
+ const graphemeStart = (index) => unicodeOffsets === void 0 ? index : unicodeOffsets[index];
2773
+ const measureGrapheme = (index) => {
2774
+ range.setStart(textNode, graphemeStart(index));
2775
+ range.setEnd(textNode, graphemeStart(index + 1));
2776
+ return range.getBoundingClientRect();
2777
+ };
2778
+ starts.push(0);
2779
+ let lastTop = measureGrapheme(0).top;
2780
+ let searchStart = 1;
2781
+ while (searchStart < graphemeCount) {
2782
+ let low = searchStart;
2783
+ let high = graphemeCount;
2784
+ while (low < high) {
2785
+ const mid = low + high >> 1;
2786
+ if (measureGrapheme(mid).top > lastTop) high = mid;
2787
+ else low = mid + 1;
2051
2788
  }
2052
- previousOffset = i;
2053
- i = nextOffset;
2054
- offsetIndex++;
2789
+ if (low >= graphemeCount) break;
2790
+ const { left, top } = measureGrapheme(low);
2791
+ const startsPastLineStart = isSafari() && left - wrapLineStartLeft > this.#metrics.ch / 2;
2792
+ starts.push(graphemeStart(startsPastLineStart ? low - 1 : low));
2793
+ lastTop = top;
2794
+ searchStart = low + 1;
2055
2795
  }
2056
2796
  const offsets = new Uint32Array(starts.length + 1);
2057
2797
  for (let i = 0; i < starts.length; i++) offsets[i] = starts[i];