@pierre/diffs 1.3.0-beta.8 → 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 (229) 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 +51 -43
  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/FileStream.d.ts.map +1 -1
  14. package/dist/components/FileStream.js +2 -1
  15. package/dist/components/FileStream.js.map +1 -1
  16. package/dist/components/VirtualizedFile.d.ts +2 -1
  17. package/dist/components/VirtualizedFile.d.ts.map +1 -1
  18. package/dist/components/VirtualizedFile.js +9 -5
  19. package/dist/components/VirtualizedFile.js.map +1 -1
  20. package/dist/components/VirtualizedFileDiff.d.ts +14 -0
  21. package/dist/components/VirtualizedFileDiff.d.ts.map +1 -1
  22. package/dist/components/VirtualizedFileDiff.js +61 -3
  23. package/dist/components/VirtualizedFileDiff.js.map +1 -1
  24. package/dist/components/Virtualizer.d.ts +14 -2
  25. package/dist/components/Virtualizer.d.ts.map +1 -1
  26. package/dist/components/Virtualizer.js +29 -3
  27. package/dist/components/Virtualizer.js.map +1 -1
  28. package/dist/edit/index.d.ts +5 -0
  29. package/dist/edit/index.js +3 -0
  30. package/dist/editor/command.d.ts +1 -1
  31. package/dist/editor/command.d.ts.map +1 -1
  32. package/dist/editor/command.js +11 -0
  33. package/dist/editor/command.js.map +1 -1
  34. package/dist/editor/editStack.d.ts +6 -2
  35. package/dist/editor/editStack.d.ts.map +1 -1
  36. package/dist/editor/editStack.js +54 -13
  37. package/dist/editor/editStack.js.map +1 -1
  38. package/dist/editor/editor.d.ts +51 -23
  39. package/dist/editor/editor.d.ts.map +1 -1
  40. package/dist/editor/editor.js +995 -242
  41. package/dist/editor/editor.js.map +1 -1
  42. package/dist/editor/editor2.js +1 -1
  43. package/dist/editor/languages.d.ts +26 -0
  44. package/dist/editor/languages.d.ts.map +1 -0
  45. package/dist/editor/languages.js +363 -0
  46. package/dist/editor/languages.js.map +1 -0
  47. package/dist/editor/lineAnnotations.d.ts.map +1 -1
  48. package/dist/editor/lineAnnotations.js +25 -26
  49. package/dist/editor/lineAnnotations.js.map +1 -1
  50. package/dist/editor/marker.d.ts +5 -3
  51. package/dist/editor/marker.d.ts.map +1 -1
  52. package/dist/editor/marker.js +111 -91
  53. package/dist/editor/marker.js.map +1 -1
  54. package/dist/editor/matchBrackets.d.ts +3 -2
  55. package/dist/editor/matchBrackets.d.ts.map +1 -1
  56. package/dist/editor/matchBrackets.js +2 -10
  57. package/dist/editor/matchBrackets.js.map +1 -1
  58. package/dist/editor/pieceTable.d.ts +2 -1
  59. package/dist/editor/pieceTable.d.ts.map +1 -1
  60. package/dist/editor/pieceTable.js +103 -39
  61. package/dist/editor/pieceTable.js.map +1 -1
  62. package/dist/editor/platform.js +2 -0
  63. package/dist/editor/platform.js.map +1 -1
  64. package/dist/editor/popover.d.ts +25 -5
  65. package/dist/editor/popover.d.ts.map +1 -1
  66. package/dist/editor/popover.js +60 -14
  67. package/dist/editor/popover.js.map +1 -1
  68. package/dist/editor/searchPanel.d.ts +1 -0
  69. package/dist/editor/searchPanel.d.ts.map +1 -1
  70. package/dist/editor/searchPanel.js +15 -8
  71. package/dist/editor/searchPanel.js.map +1 -1
  72. package/dist/editor/selection.d.ts +31 -11
  73. package/dist/editor/selection.d.ts.map +1 -1
  74. package/dist/editor/selection.js +198 -70
  75. package/dist/editor/selection.js.map +1 -1
  76. package/dist/editor/selectionAction.d.ts +4 -3
  77. package/dist/editor/selectionAction.d.ts.map +1 -1
  78. package/dist/editor/selectionAction.js +9 -5
  79. package/dist/editor/selectionAction.js.map +1 -1
  80. package/dist/editor/stateStorage.d.ts +15 -0
  81. package/dist/editor/stateStorage.d.ts.map +1 -0
  82. package/dist/editor/stateStorage.js +65 -0
  83. package/dist/editor/stateStorage.js.map +1 -0
  84. package/dist/editor/textDocument.d.ts +12 -77
  85. package/dist/editor/textDocument.d.ts.map +1 -1
  86. package/dist/editor/textDocument.js +76 -34
  87. package/dist/editor/textDocument.js.map +1 -1
  88. package/dist/editor/textMeasure.d.ts +7 -12
  89. package/dist/editor/textMeasure.d.ts.map +1 -1
  90. package/dist/editor/textMeasure.js +40 -32
  91. package/dist/editor/textMeasure.js.map +1 -1
  92. package/dist/editor/{tokenzier.d.ts → tokenizer.d.ts} +3 -3
  93. package/dist/editor/tokenizer.d.ts.map +1 -0
  94. package/dist/editor/{tokenzier.js → tokenizer.js} +91 -26
  95. package/dist/editor/tokenizer.js.map +1 -0
  96. package/dist/editor/utils.js +1 -1
  97. package/dist/editor/utils.js.map +1 -1
  98. package/dist/index.d.ts +5 -4
  99. package/dist/index.js +7 -6
  100. package/dist/managers/InteractionManager.d.ts +18 -7
  101. package/dist/managers/InteractionManager.d.ts.map +1 -1
  102. package/dist/managers/InteractionManager.js +154 -99
  103. package/dist/managers/InteractionManager.js.map +1 -1
  104. package/dist/managers/UniversalRenderingManager.d.ts +2 -1
  105. package/dist/managers/UniversalRenderingManager.d.ts.map +1 -1
  106. package/dist/managers/UniversalRenderingManager.js +7 -3
  107. package/dist/managers/UniversalRenderingManager.js.map +1 -1
  108. package/dist/react/CodeView.d.ts +13 -13
  109. package/dist/react/CodeView.d.ts.map +1 -1
  110. package/dist/react/CodeView.js +38 -29
  111. package/dist/react/CodeView.js.map +1 -1
  112. package/dist/react/EditContext.d.ts +20 -0
  113. package/dist/react/EditContext.d.ts.map +1 -0
  114. package/dist/react/EditContext.js +20 -0
  115. package/dist/react/EditContext.js.map +1 -0
  116. package/dist/react/File.d.ts +2 -1
  117. package/dist/react/File.d.ts.map +1 -1
  118. package/dist/react/File.js +3 -2
  119. package/dist/react/File.js.map +1 -1
  120. package/dist/react/FileDiff.d.ts +2 -2
  121. package/dist/react/FileDiff.d.ts.map +1 -1
  122. package/dist/react/FileDiff.js +3 -2
  123. package/dist/react/FileDiff.js.map +1 -1
  124. package/dist/react/MultiFileDiff.d.ts +2 -2
  125. package/dist/react/MultiFileDiff.d.ts.map +1 -1
  126. package/dist/react/MultiFileDiff.js +3 -2
  127. package/dist/react/MultiFileDiff.js.map +1 -1
  128. package/dist/react/PatchDiff.d.ts +2 -2
  129. package/dist/react/PatchDiff.d.ts.map +1 -1
  130. package/dist/react/PatchDiff.js +3 -2
  131. package/dist/react/PatchDiff.js.map +1 -1
  132. package/dist/react/UnresolvedFile.d.ts +1 -1
  133. package/dist/react/UnresolvedFile.js.map +1 -1
  134. package/dist/react/index.d.ts +4 -4
  135. package/dist/react/index.js +3 -3
  136. package/dist/react/types.d.ts +9 -1
  137. package/dist/react/types.d.ts.map +1 -1
  138. package/dist/react/utils/useFileDiffInstance.d.ts +5 -3
  139. package/dist/react/utils/useFileDiffInstance.d.ts.map +1 -1
  140. package/dist/react/utils/useFileDiffInstance.js +28 -27
  141. package/dist/react/utils/useFileDiffInstance.js.map +1 -1
  142. package/dist/react/utils/useFileInstance.d.ts +5 -3
  143. package/dist/react/utils/useFileInstance.d.ts.map +1 -1
  144. package/dist/react/utils/useFileInstance.js +29 -25
  145. package/dist/react/utils/useFileInstance.js.map +1 -1
  146. package/dist/react/utils/useUnresolvedFileInstance.js +3 -3
  147. package/dist/react/utils/useUnresolvedFileInstance.js.map +1 -1
  148. package/dist/renderers/DiffHunksRenderer.d.ts +33 -1
  149. package/dist/renderers/DiffHunksRenderer.d.ts.map +1 -1
  150. package/dist/renderers/DiffHunksRenderer.js +134 -21
  151. package/dist/renderers/DiffHunksRenderer.js.map +1 -1
  152. package/dist/renderers/FileRenderer.d.ts +2 -0
  153. package/dist/renderers/FileRenderer.d.ts.map +1 -1
  154. package/dist/renderers/FileRenderer.js +33 -11
  155. package/dist/renderers/FileRenderer.js.map +1 -1
  156. package/dist/ssr/FileDiffReact.js +1 -1
  157. package/dist/ssr/index.d.ts +2 -2
  158. package/dist/ssr/preloadDiffs.js +1 -1
  159. package/dist/style.js +1 -1
  160. package/dist/types.d.ts +185 -42
  161. package/dist/types.d.ts.map +1 -1
  162. package/dist/utils/annotationHelpers.d.ts +20 -0
  163. package/dist/utils/annotationHelpers.d.ts.map +1 -0
  164. package/dist/utils/annotationHelpers.js +29 -0
  165. package/dist/utils/annotationHelpers.js.map +1 -0
  166. package/dist/utils/computeFileOffsets.d.ts.map +1 -1
  167. package/dist/utils/computeFileOffsets.js +34 -5
  168. package/dist/utils/computeFileOffsets.js.map +1 -1
  169. package/dist/utils/editSessionHunks.d.ts +66 -0
  170. package/dist/utils/editSessionHunks.d.ts.map +1 -0
  171. package/dist/utils/editSessionHunks.js +437 -0
  172. package/dist/utils/editSessionHunks.js.map +1 -0
  173. package/dist/utils/getHunkSideBoundaries.d.ts +7 -0
  174. package/dist/utils/getHunkSideBoundaries.d.ts.map +1 -0
  175. package/dist/utils/getHunkSideBoundaries.js +12 -0
  176. package/dist/utils/getHunkSideBoundaries.js.map +1 -0
  177. package/dist/utils/getTotalLineCountFromHunks.d.ts.map +1 -1
  178. package/dist/utils/getTotalLineCountFromHunks.js +2 -1
  179. package/dist/utils/getTotalLineCountFromHunks.js.map +1 -1
  180. package/dist/utils/hydratePartialDiff.d.ts.map +1 -1
  181. package/dist/utils/hydratePartialDiff.js +4 -3
  182. package/dist/utils/hydratePartialDiff.js.map +1 -1
  183. package/dist/utils/isGutterUtilityPath.d.ts +5 -0
  184. package/dist/utils/isGutterUtilityPath.d.ts.map +1 -0
  185. package/dist/utils/isGutterUtilityPath.js +12 -0
  186. package/dist/utils/isGutterUtilityPath.js.map +1 -0
  187. package/dist/utils/iterateOverDiff.d.ts.map +1 -1
  188. package/dist/utils/iterateOverDiff.js +17 -12
  189. package/dist/utils/iterateOverDiff.js.map +1 -1
  190. package/dist/utils/parseMergeConflictDiffFromFile.d.ts.map +1 -1
  191. package/dist/utils/parseMergeConflictDiffFromFile.js +9 -6
  192. package/dist/utils/parseMergeConflictDiffFromFile.js.map +1 -1
  193. package/dist/utils/parsePatchFiles.d.ts.map +1 -1
  194. package/dist/utils/parsePatchFiles.js +6 -3
  195. package/dist/utils/parsePatchFiles.js.map +1 -1
  196. package/dist/utils/realignChangeContent.d.ts +33 -0
  197. package/dist/utils/realignChangeContent.d.ts.map +1 -0
  198. package/dist/utils/realignChangeContent.js +148 -0
  199. package/dist/utils/realignChangeContent.js.map +1 -0
  200. package/dist/utils/renderDiffWithHighlighter.js +1 -1
  201. package/dist/utils/resolveRegion.d.ts.map +1 -1
  202. package/dist/utils/resolveRegion.js +18 -2
  203. package/dist/utils/resolveRegion.js.map +1 -1
  204. package/dist/utils/updateDiffHunks.d.ts +8 -2
  205. package/dist/utils/updateDiffHunks.d.ts.map +1 -1
  206. package/dist/utils/updateDiffHunks.js +9 -7
  207. package/dist/utils/updateDiffHunks.js.map +1 -1
  208. package/dist/utils/virtualDiffLayout.d.ts +42 -2
  209. package/dist/utils/virtualDiffLayout.d.ts.map +1 -1
  210. package/dist/utils/virtualDiffLayout.js +101 -7
  211. package/dist/utils/virtualDiffLayout.js.map +1 -1
  212. package/dist/worker/WorkerPoolManager.d.ts.map +1 -1
  213. package/dist/worker/WorkerPoolManager.js +4 -2
  214. package/dist/worker/WorkerPoolManager.js.map +1 -1
  215. package/dist/worker/types.d.ts +4 -0
  216. package/dist/worker/types.d.ts.map +1 -1
  217. package/dist/worker/worker-portable.js +60 -20
  218. package/dist/worker/worker-portable.js.map +1 -1
  219. package/dist/worker/worker.js +61 -19
  220. package/dist/worker/worker.js.map +1 -1
  221. package/package.json +13 -8
  222. package/dist/editor/index.d.ts +0 -3
  223. package/dist/editor/index.js +0 -3
  224. package/dist/editor/tokenzier.d.ts.map +0 -1
  225. package/dist/editor/tokenzier.js.map +0 -1
  226. package/dist/react/EditorContext.d.ts +0 -15
  227. package/dist/react/EditorContext.d.ts.map +0 -1
  228. package/dist/react/EditorContext.js +0 -23
  229. package/dist/react/EditorContext.js.map +0 -1
@@ -1,32 +1,69 @@
1
- import { queueRender } from "../managers/UniversalRenderingManager.js";
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
23
+ function getShadowRootRange(shadowRoot) {
24
+ const selection = shadowRoot.getSelection?.();
25
+ if (selection == null || selection.rangeCount === 0) return;
26
+ const range = selection.getRangeAt(0);
27
+ return {
28
+ collapsed: range.collapsed,
29
+ startContainer: range.startContainer,
30
+ startOffset: range.startOffset,
31
+ endContainer: range.endContainer,
32
+ endOffset: range.endOffset
33
+ };
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
+ }
20
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";
21
46
  var Editor = class {
22
47
  #options;
23
48
  #metrics = new Metrics();
24
49
  #tokenizer;
25
50
  #popoverManager;
51
+ #textDocumentCache = /* @__PURE__ */ new Map();
52
+ #stateStorage;
53
+ #stateStorageOption;
54
+ #pendingStateWrites = /* @__PURE__ */ new Map();
55
+ #pendingStateRestore;
56
+ #stateRestoreGeneration = 0;
57
+ #restoreStateOnNextSync = false;
26
58
  #editorEventDisposes;
27
59
  #globalEventDisposes;
28
60
  #selectEventDisposes;
29
61
  #detach;
62
+ #attachState = {
63
+ generation: 0,
64
+ callback: void 0,
65
+ delivered: false
66
+ };
30
67
  #contentOffset;
31
68
  #gutterWidthCache;
32
69
  #contentWidthCache;
@@ -54,9 +91,11 @@ var Editor = class {
54
91
  #markerRenderer;
55
92
  #searchPanel;
56
93
  #selectionAction;
94
+ #canMountSelectionAction = true;
57
95
  #shouldIgnoreSelectionChange = false;
58
96
  #suppressNativeSelectionSync = false;
59
97
  #contentHasFocus = false;
98
+ #replacementFocusRequest;
60
99
  #isComposing = false;
61
100
  #isGutterMouseDown = false;
62
101
  #isContentMouseDown = false;
@@ -75,13 +114,13 @@ var Editor = class {
75
114
  #fontRemeasureScheduled = false;
76
115
  #themeSelectionRefreshFrame;
77
116
  #onDeferTokenize = (lines, themeType) => {
78
- this.#fileInstance?.updateRenderCache(lines, themeType, false);
117
+ this.#fileInstance?.updateRenderCache(lines, themeType, false, false);
79
118
  if (this.#renderRange !== void 0 && this.#renderRange.totalLines !== Infinity) {
80
119
  const { startingLine, totalLines } = this.#renderRange;
81
120
  const endLine = Math.min(startingLine + totalLines, this.#textDocument?.lineCount ?? 0);
82
121
  for (const [line, tokens] of lines) if (line >= startingLine && line < endLine) {
83
122
  const lineElement = this.#getLineElement(line);
84
- if (lineElement !== void 0) lineElement.replaceChildren(...renderLineTokens(tokens, themeType));
123
+ if (lineElement !== void 0) lineElement.replaceChildren(...renderLineTokens(tokens));
85
124
  }
86
125
  }
87
126
  };
@@ -89,21 +128,38 @@ var Editor = class {
89
128
  this.#options = options;
90
129
  }
91
130
  setOptions(options) {
92
- this.#options = {
131
+ const previousStorageOption = this.#options.persistStateStorage ?? "inMemory";
132
+ const nextOptions = {
93
133
  ...this.#options,
94
134
  ...options
95
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
+ }
96
152
  }
97
153
  edit(fileInstance) {
98
- const { useTokenTransformer, enableGutterUtility, enableLineSelection, expandUnchanged, lineHoverHighlight = "disabled", ...rest } = fileInstance.options;
99
- 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) {
100
160
  fileInstance.setOptions({
101
- ...rest,
102
- useTokenTransformer: true,
103
- enableGutterUtility: false,
104
- enableLineSelection: false,
105
- expandUnchanged: true,
106
- lineHoverHighlight: "disabled"
161
+ ...fileInstance.options,
162
+ useTokenTransformer: true
107
163
  });
108
164
  fileInstance.rerender();
109
165
  }
@@ -113,23 +169,22 @@ var Editor = class {
113
169
  return () => this.cleanUp();
114
170
  }
115
171
  /**
116
- * 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.
117
180
  */
118
- applyEdits(edits, updateHistory = false) {
181
+ applyEdits(edits, updateHistory = true) {
119
182
  const textDocument = this.#textDocument;
120
183
  if (textDocument == null) throw new Error("Editor is not attached");
121
184
  const wasFocused = this.#contentHasFocus;
122
185
  const selectionsBefore = this.#selections;
123
186
  const selectionOffsetsBefore = selectionsBefore?.map((selection) => [textDocument.offsetAt(selection.start), textDocument.offsetAt(selection.end)]);
124
- const resolvedEditOffsets = selectionsBefore === void 0 ? void 0 : edits.map((edit) => {
125
- const a = textDocument.offsetAt(edit.range.start);
126
- const b = textDocument.offsetAt(edit.range.end);
127
- return {
128
- start: Math.min(a, b),
129
- end: Math.max(a, b),
130
- text: edit.newText
131
- };
132
- }).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);
133
188
  const change = textDocument.applyEdits(edits, updateHistory, selectionsBefore);
134
189
  if (change === void 0) return;
135
190
  let nextSelections;
@@ -155,45 +210,58 @@ var Editor = class {
155
210
  redo() {
156
211
  this.#runCommand("redo");
157
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
+ }
158
227
  getState() {
159
- const fileRef = this.#getFileRef();
160
- if (fileRef === void 0) throw new Error("Editor is not attached");
228
+ const fileInstance = this.#fileInstance;
161
229
  return {
162
- file: {
163
- ...fileRef,
164
- cacheKey: "edited-at-" + Date.now()
165
- },
166
230
  selections: this.#selections,
167
- lineAnnotations: this.#lineAnnotations,
168
- renderRange: this.#renderRange
231
+ view: fileInstance != null ? { scrollLeft: fileInstance.getCodeScrollLeft() } : void 0
169
232
  };
170
233
  }
171
- setState({ file, lineAnnotations, renderRange, selections }) {
172
- this.#resetCache();
173
- this.#resetState();
174
- this.#initSelections = selections;
175
- this.#fileInstance?.render({
176
- file: {
177
- ...file,
178
- cacheKey: "edited-at-" + Date.now()
179
- },
180
- lineAnnotations,
181
- renderRange
182
- });
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();
183
243
  }
184
244
  setSelections(selections) {
185
245
  const textDocument = this.#textDocument;
186
246
  if (textDocument === void 0) throw new Error("Text document is not initialized");
187
247
  const resolvedSelections = selections.map((selection) => {
188
- const start = textDocument.normalizePosition(selection.start);
189
- 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
+ }
190
255
  return {
191
- direction: selection.direction === "none" ? 0 : selection.direction === "backward" ? -1 : 1,
256
+ direction,
192
257
  start,
193
258
  end
194
259
  };
195
260
  });
261
+ this.#canMountSelectionAction = false;
196
262
  this.#updateSelections(resolvedSelections);
263
+ const primarySelection = resolvedSelections.at(-1);
264
+ if (primarySelection !== void 0) this.#revealLineIfCollapsed(getCaretPosition(primarySelection).line);
197
265
  this.#scrollToPrimaryCaret();
198
266
  }
199
267
  setMarkers(markers) {
@@ -220,6 +288,19 @@ var Editor = class {
220
288
  }
221
289
  focus(options) {
222
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
+ }
223
304
  const primarySelection = this.#selections?.at(-1);
224
305
  if (primarySelection !== void 0) {
225
306
  const pos = primarySelection.direction === -1 ? primarySelection.end : primarySelection.start;
@@ -227,9 +308,20 @@ var Editor = class {
227
308
  } else this.#focus(void 0, preventScroll);
228
309
  }
229
310
  blur() {
311
+ this.#replacementFocusRequest = void 0;
312
+ this.#contentHasFocus = false;
313
+ this.#shouldIgnoreSelectionChange = false;
230
314
  this.#contentElement?.blur();
231
315
  }
232
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;
324
+ dequeueRender(this.#handleCustomPasteEvent);
233
325
  this.#tokenizer?.cleanUp();
234
326
  this.#tokenizer = void 0;
235
327
  if (!recycle) {
@@ -242,7 +334,7 @@ var Editor = class {
242
334
  this.#editorEventDisposes = void 0;
243
335
  this.#selectEventDisposes?.forEach((dispose) => dispose());
244
336
  this.#selectEventDisposes = void 0;
245
- this.#detach?.();
337
+ this.#detach?.(recycle);
246
338
  this.#detach = void 0;
247
339
  this.#gutterWidthCache = void 0;
248
340
  this.#contentWidthCache = void 0;
@@ -264,6 +356,7 @@ var Editor = class {
264
356
  this.#gutterElement = void 0;
265
357
  this.#contentElement?.removeAttribute("contentEditable");
266
358
  this.#contentElement = void 0;
359
+ this.#replacementFocusRequest = void 0;
267
360
  this.#contentHasFocus = false;
268
361
  this.#overlayElement?.remove();
269
362
  this.#overlayElement = void 0;
@@ -275,6 +368,24 @@ var Editor = class {
275
368
  this.#themeSelectionRefreshFrame = void 0;
276
369
  }
277
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
+ };
278
389
  }
279
390
  /** @internal */
280
391
  __postponeBgTokenizeToNextFrame() {
@@ -287,7 +398,15 @@ var Editor = class {
287
398
  }
288
399
  }
289
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 */
290
407
  __syncRenderView = (highlighter, fileContainer, fileOrDiff, lineAnnotations, renderRange) => {
408
+ const fileInstance = this.#fileInstance;
409
+ if (fileInstance == null) return;
291
410
  const shadowRoot = fileContainer.shadowRoot;
292
411
  if (shadowRoot == null) {
293
412
  console.error("[editor] Could not find the shadow root.");
@@ -306,7 +425,10 @@ var Editor = class {
306
425
  }
307
426
  break;
308
427
  }
309
- if (codeElement === void 0 || contentEl === void 0) return;
428
+ if (codeElement === void 0 || contentEl === void 0) {
429
+ this.#replacementFocusRequest = void 0;
430
+ return;
431
+ }
310
432
  this.#getPopoverManager().setViewportElements(fileContainer, codeElement);
311
433
  if (this.#fileContainer !== fileContainer) {
312
434
  this.#fileContainer = fileContainer;
@@ -315,28 +437,42 @@ var Editor = class {
315
437
  if (this.#themeStyleElement !== void 0) shadowRoot.appendChild(this.#themeStyleElement);
316
438
  if (this.#spriteElement !== void 0) shadowRoot.prepend(this.#spriteElement);
317
439
  }
318
- 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();
319
445
  let contents = "";
320
446
  if ("contents" in fileOrDiff) contents = fileOrDiff.contents;
321
447
  else contents = fileOrDiff.additionLines.join("");
322
448
  const editStack = new EditStack({ maxEntries: this.#options.historyMaxEntries });
323
- const textDocument = new TextDocument(fileOrDiff.name, contents, fileOrDiff.lang ?? getFiletypeFromFileName(fileOrDiff.name), 0, editStack);
324
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);
325
452
  this.#fileInfo = {
326
453
  name,
327
454
  lang,
328
455
  cacheKey
329
456
  };
330
457
  this.#textDocument = textDocument;
458
+ if (persistedCacheKey !== void 0) {
459
+ this.#textDocumentCache.set(persistedCacheKey, textDocument);
460
+ persistedStateTarget = {
461
+ cacheKey: persistedCacheKey,
462
+ textDocument
463
+ };
464
+ }
331
465
  this.#tokenizer?.cleanUp();
332
466
  this.#tokenizer = void 0;
333
467
  this.#resetState();
468
+ this.#wrapLineOffsetsCache.clear();
334
469
  this.#selections = this.#initSelections;
335
- requestAnimationFrame(() => {
336
- this.#options.onAttach?.(this, this.#fileInstance);
337
- });
338
470
  if (this.#textDocument !== void 0 && this.#options.__debug === true) console.log("[diffs/editor] text document rebuilt from", fileOrDiff.name);
339
471
  }
472
+ if (persistedStateTarget === void 0 && this.#restoreStateOnNextSync && persistedCacheKey !== void 0 && this.#textDocument !== void 0) persistedStateTarget = {
473
+ cacheKey: persistedCacheKey,
474
+ textDocument: this.#textDocument
475
+ };
340
476
  const textDocument = this.#textDocument;
341
477
  if (this.#tokenizer == null && textDocument != null) this.#tokenizer = new EditorTokenizer({
342
478
  highlighter,
@@ -368,11 +504,12 @@ var Editor = class {
368
504
  this.#listenContentElement(contentEl, gutterEl);
369
505
  if (this.#contentElement !== void 0 && this.#options.__debug === true) console.log("[diffs/editor] full re-render triggered !!!");
370
506
  }
507
+ if (this.#replacementFocusRequest != null) this.#replacementFocusRequest.target = contentEl;
371
508
  if (contentEl.ariaLabel !== fileOrDiff.name) contentEl.ariaLabel = fileOrDiff.name;
372
- 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) {
373
510
  const el = child;
374
- const { lineAnnotation, lineType } = el.dataset;
375
- 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");
376
513
  }
377
514
  this.#resetCache();
378
515
  this.#tokenizer?.syncTheme(this.#fileInstance?.options ?? {});
@@ -380,19 +517,140 @@ var Editor = class {
380
517
  this.#renderRange = renderRange;
381
518
  this.#viewportWindowLines = renderRange?.totalLines;
382
519
  this.#tokenizer?.prebuildStateStack(renderRange);
383
- this.#markerRenderer?.removePopup();
520
+ this.#markerRenderer?.removePopover();
384
521
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) this.#updateSelections(this.#selections ?? []);
385
522
  if (this.#initSelections !== void 0 && this.#primaryCaretElement !== void 0) {
386
523
  this.#initSelections = void 0;
387
524
  this.#scrollToPrimaryCaret(false, "center");
388
525
  } else if (this.#scrollingToLine !== void 0) this.#scrollToLine(this.#scrollingToLine, this.#scrollingToLineChar, this.#scrollingToLineNoFocus);
389
- 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 });
390
528
  if (this.#retainSearchPanelFocus) this.#searchPanel?.focus();
391
529
  if (this.#options.__debug === true && renderRange !== void 0) {
392
530
  const { startingLine, totalLines } = renderRange;
393
531
  console.log("[diffs/editor] render file:", fileOrDiff.name, "RenderRange:", startingLine + "-" + (startingLine + totalLines), "of", this.#textDocument?.lineCount, "lines");
394
532
  }
533
+ if (persistedStateTarget !== void 0) {
534
+ this.#restoreStateOnNextSync = false;
535
+ this.#restorePersistedState(persistedStateTarget.cacheKey, persistedStateTarget.textDocument);
536
+ }
537
+ this.#scheduleOnAttach(fileInstance);
395
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
+ }
396
654
  get #diffSyle() {
397
655
  return this.#fileInstance?.options.diffStyle ?? "split";
398
656
  }
@@ -404,21 +662,24 @@ var Editor = class {
404
662
  }
405
663
  #getPopoverManager() {
406
664
  return this.#popoverManager ??= new PopoverManager({
407
- hasActivePopover: () => this.#selectionAction !== void 0,
408
- updateActivePopover: () => this.#updateSelectionActionPopover()
665
+ hasActivePopover: () => this.#selectionAction !== void 0 || this.#markerRenderer?.isPopoverVisible() === true,
666
+ updateActivePopover: () => {
667
+ this.#updateSelectionActionPopover();
668
+ this.#markerRenderer?.updatePopoverPosition();
669
+ }
409
670
  });
410
671
  }
411
672
  #resetCache() {
412
673
  this.#lineYCache.clear();
413
- this.#wrapLineOffsetsCache.clear();
414
674
  this.#lineElementsCache.clear();
415
675
  this.#lastAccessedCharX = void 0;
416
676
  }
417
677
  #resetState() {
418
- this.#setSelectedLinesSafe(null);
678
+ this.#setEditorActiveLineSafe(null);
419
679
  this.#gutterWidthCache = void 0;
420
680
  this.#contentWidthCache = void 0;
421
681
  this.#shouldIgnoreSelectionChange = false;
682
+ this.#suppressNativeSelectionSync = false;
422
683
  this.#overlayElements?.forEach((el) => el.remove());
423
684
  this.#overlayElements = void 0;
424
685
  this.#selections = void 0;
@@ -430,6 +691,33 @@ var Editor = class {
430
691
  this.#searchPanel = void 0;
431
692
  this.#selectionAction?.cleanup();
432
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);
433
721
  }
434
722
  #initialize() {
435
723
  this.#globalStyleElement = h("style", {
@@ -448,14 +736,33 @@ var Editor = class {
448
736
  this.#themeStyleElement = h("style", { dataset: "editorThemeCss" });
449
737
  this.#spriteElement = createSpriteElement();
450
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
+ };
451
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
+ }),
452
759
  addEventListener(document, "selectionchange", () => {
453
760
  const shadowRoot = this.#fileContainer?.shadowRoot;
454
761
  if (this.#shouldIgnoreSelectionChange || this.#suppressNativeSelectionSync || shadowRoot == null || !this.#contentHasFocus) return;
455
762
  if (this.#selections !== void 0 && this.#selections.length > 1 && !this.#isContentMouseDown) return;
456
763
  const selectionRaw = document.getSelection();
457
- if (selectionRaw == null || typeof selectionRaw.getComposedRanges !== "function") return;
458
- const composedRange = selectionRaw.getComposedRanges({ shadowRoots: [shadowRoot] })?.[0];
764
+ if (selectionRaw == null) return;
765
+ const composedRange = typeof selectionRaw.getComposedRanges === "function" ? selectionRaw.getComposedRanges({ shadowRoots: [shadowRoot] })?.[0] : getShadowRootRange(shadowRoot);
459
766
  if (composedRange === void 0 || !this.#rangeBelongsToEditor(composedRange)) return;
460
767
  let selection = convertSelection(composedRange, 0);
461
768
  if (selection === void 0) return;
@@ -501,6 +808,12 @@ var Editor = class {
501
808
  this.#selectEventDisposes?.forEach((dispose) => dispose());
502
809
  this.#selectEventDisposes = disposes;
503
810
  }
811
+ #isLineSelectionEnabled() {
812
+ return this.#fileInstance?.options.enableLineSelection === true;
813
+ }
814
+ #preserveEditorSelectionsForGutterGesture() {
815
+ this.#suppressNativeSelectionSync = this.#selections !== void 0;
816
+ }
504
817
  #listenContentElement(contentEl, gutterEl) {
505
818
  const { onFocus, onBlur } = this.#options;
506
819
  const targetIsContentElement = (e) => {
@@ -515,10 +828,15 @@ var Editor = class {
515
828
  if (!this.#isContentMouseDown && !this.#shouldIgnoreSelectionChange && this.#selections !== void 0 && this.#selections.length > 0) this.#setWindowSelection(this.#selections.at(-1));
516
829
  }, { passive: true }),
517
830
  addEventListener(contentEl, "blur", () => {
831
+ if (this.#replacementFocusRequest?.target === contentEl) {
832
+ this.#replacementFocusRequest = void 0;
833
+ this.#shouldIgnoreSelectionChange = false;
834
+ }
518
835
  this.#contentHasFocus = false;
519
836
  onBlur?.();
520
837
  }, { passive: true }),
521
838
  addEventListener(contentEl, "pointerdown", (e) => {
839
+ this.#canMountSelectionAction = true;
522
840
  this.#suppressNativeSelectionSync = false;
523
841
  if (e.pointerType !== "mouse") return;
524
842
  if (this.#isDeletedLineTarget(e)) {
@@ -527,7 +845,7 @@ var Editor = class {
527
845
  return;
528
846
  }
529
847
  this.#setDeletedTextSelectionActive(false);
530
- this.#markerRenderer?.removePopup();
848
+ this.#markerRenderer?.removePopover();
531
849
  if (isSafari() && this.#lineAnnotations !== void 0 && this.#lineAnnotations.length > 0) {
532
850
  const annotationDisposes = [...contentEl.querySelectorAll("[data-line-annotation]")].map((el) => [addEventListener(el, "mouseenter", () => {
533
851
  this.#shouldIgnoreSelectionChange = true;
@@ -555,32 +873,26 @@ var Editor = class {
555
873
  addEventListener(contentEl, "keydown", (e) => {
556
874
  if (e.key === "Escape") {
557
875
  e.preventDefault();
558
- this.#searchPanel?.cleanup();
876
+ this.#searchPanel?.close();
559
877
  this.#searchPanel = void 0;
560
878
  this.#retainSearchPanelFocus = false;
561
879
  this.#selectionAction?.cleanup();
562
880
  this.#selectionAction = void 0;
563
- if (this.#selections !== void 0 && this.#selections.length > 0) {
564
- const primarySelection = this.#selections.at(-1);
565
- if (!isCollapsedSelection(primarySelection) || this.#selections.length > 1) {
566
- const pos = getCaretPosition(primarySelection);
567
- this.#updateSelections([{
568
- start: pos,
569
- end: pos,
570
- direction: 0
571
- }]);
572
- this.#focus(pos);
573
- }
574
- }
881
+ this.#runCommand("simplifySelection");
575
882
  return;
576
883
  }
577
884
  if (!targetIsContentElement(e)) return;
885
+ this.#canMountSelectionAction = true;
578
886
  this.#suppressNativeSelectionSync = false;
579
887
  const mvShortcut = isMoveCursorShortcut(e);
580
888
  const textDocument = this.#textDocument;
581
889
  if (this.#selections !== void 0 && this.#selections.length > 0 && mvShortcut !== void 0 && textDocument !== void 0) {
582
- if (e.shiftKey) this.#updateSelections(mapSelectionShift(textDocument, this.#selections, mvShortcut));
583
- 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));
584
896
  this.#scrollToPrimaryCaret();
585
897
  e.preventDefault();
586
898
  return;
@@ -613,19 +925,34 @@ var Editor = class {
613
925
  addEventListener(contentEl, "copy", (e) => {
614
926
  if (!targetIsContentElement(e)) return;
615
927
  e.preventDefault();
616
- 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());
617
930
  }),
618
931
  addEventListener(contentEl, "cut", (e) => {
619
932
  if (!targetIsContentElement(e)) return;
620
933
  e.preventDefault();
621
- 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
+ }
622
939
  }),
623
940
  addEventListener(contentEl, "paste", (e) => {
624
941
  if (!targetIsContentElement(e)) return;
625
942
  e.preventDefault();
626
- const text = e.clipboardData?.getData("text");
943
+ const clipboardData = e.clipboardData;
627
944
  const textDocument = this.#textDocument;
628
- 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");
629
956
  }),
630
957
  addEventListener(contentEl, "beforeinput", (e) => {
631
958
  if (!targetIsContentElement(e)) return;
@@ -652,9 +979,20 @@ var Editor = class {
652
979
  ];
653
980
  const deletionsCode = this.#fileContainer?.shadowRoot?.querySelector("[data-deletions]");
654
981
  if (deletionsCode != null) this.#editorEventDisposes.push(addEventListener(deletionsCode, "pointerdown", (e) => {
655
- 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];
656
988
  const gutterRow = target instanceof HTMLElement ? target.closest("[data-column-number]") : null;
657
- 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
+ }
658
996
  this.#setDeletedTextSelectionActive(true);
659
997
  if (this.#selections !== void 0) this.#updateSelections([]);
660
998
  }, { passive: true }));
@@ -673,7 +1011,16 @@ var Editor = class {
673
1011
  return lineNumber - 1;
674
1012
  };
675
1013
  this.#editorEventDisposes.push(addEventListener(gutterEl, "pointerdown", (e) => {
676
- 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
+ }
677
1024
  if (gutterRow?.dataset.lineType === "change-deletion") {
678
1025
  const code = gutterRow.closest("[data-code]");
679
1026
  if (code != null) this.#beginDeletionGutterSelection(gutterRow, code, e.pointerType === "mouse");
@@ -683,7 +1030,8 @@ var Editor = class {
683
1030
  const textDocument = this.#textDocument;
684
1031
  const lineIndex = resolveEditableLine(gutterRow);
685
1032
  if (lineIndex === void 0 || textDocument === void 0) return;
686
- this.#markerRenderer?.removePopup();
1033
+ this.#canMountSelectionAction = true;
1034
+ this.#markerRenderer?.removePopover();
687
1035
  const selection = this.#spanLineSelection(lineIndex, lineIndex, textDocument);
688
1036
  this.#isGutterMouseDown = true;
689
1037
  this.#selectionStart = selection;
@@ -713,8 +1061,17 @@ var Editor = class {
713
1061
  #handleCustomPasteEvent = async () => {
714
1062
  const clipboard = this.#options.clipboard;
715
1063
  if (clipboard !== void 0) {
716
- const text = await clipboard.readText();
717
- 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");
718
1075
  }
719
1076
  };
720
1077
  #computeContentOffset(contentEl) {
@@ -750,9 +1107,11 @@ var Editor = class {
750
1107
  this.#updateSelections(expanded);
751
1108
  this.focus();
752
1109
  } else {
753
- const nextMatch = findNexMatch(textDocument, selections);
1110
+ const nextMatch = findNextMatch(textDocument, selections);
754
1111
  if (nextMatch !== void 0) {
755
1112
  this.#updateSelections(nextMatch);
1113
+ const primaryMatch = nextMatch.at(-1);
1114
+ if (primaryMatch !== void 0) this.#revealLineIfCollapsed(getCaretPosition(primaryMatch).line);
756
1115
  this.#scrollToPrimaryCaret();
757
1116
  }
758
1117
  }
@@ -762,18 +1121,74 @@ var Editor = class {
762
1121
  case "moveLineDown":
763
1122
  this.#moveSelectedLines(command === "moveLineUp" ? -1 : 1);
764
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
+ }
765
1170
  case "indent":
766
1171
  case "outdent":
1172
+ case "indentLess":
1173
+ case "indentMore":
767
1174
  if (this.#selections !== void 0) {
768
1175
  const edits = [];
769
1176
  const nextSelections = [];
1177
+ const editedLines = /* @__PURE__ */ new Set();
770
1178
  const sameLineIndents = [];
771
1179
  for (const selection of this.#selections) {
772
1180
  const startLine = selection.start.line;
773
- const outdent = command === "outdent";
774
- 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) {
775
1184
  const ret = resolveIndentEdits(textDocument, selection, this.#metrics.tabSize, outdent);
776
- 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
+ }
777
1192
  nextSelections.push(ret[1]);
778
1193
  } else {
779
1194
  const text = textDocument.charAt({
@@ -825,17 +1240,19 @@ var Editor = class {
825
1240
  const fullSelection = getDocumentFullSelection(textDocument);
826
1241
  this.#updateSelections([fullSelection]);
827
1242
  this.focus();
828
- const renderedLines = this.#getRenderedEditableLineRange();
829
- 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) {
830
1247
  this.#suppressNativeSelectionSync = true;
831
1248
  this.#setWindowSelection({
832
1249
  start: {
833
- line: renderedLines.first,
1250
+ line: firstRenderedLine,
834
1251
  character: 0
835
1252
  },
836
1253
  end: {
837
- line: renderedLines.last,
838
- character: textDocument.getLineLength(renderedLines.last)
1254
+ line: lastRenderedLine,
1255
+ character: textDocument.getLineLength(lastRenderedLine)
839
1256
  },
840
1257
  direction: 1
841
1258
  });
@@ -845,8 +1262,9 @@ var Editor = class {
845
1262
  case "moveCursorToDocStart":
846
1263
  case "moveCursorToDocEnd":
847
1264
  {
848
- const atEnd = command === "moveCursorToDocEnd";
849
- 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);
850
1268
  this.#scrollToPrimaryCaret();
851
1269
  }
852
1270
  break;
@@ -856,25 +1274,191 @@ var Editor = class {
856
1274
  const atEnd = command === "expandSelectionDocEnd";
857
1275
  const selections = this.#selections;
858
1276
  if (selections !== void 0) {
859
- 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);
860
1280
  this.#scrollToPrimaryCaret();
861
1281
  }
862
1282
  }
863
1283
  break;
864
1284
  case "undo":
865
- if (this.#textDocument?.canUndo === true) {
866
- const undoResult = this.#textDocument.undo();
867
- if (undoResult !== void 0) this.#applyChange(...undoResult);
868
- }
869
- break;
870
1285
  case "redo":
871
- if (this.#textDocument?.canRedo === true) {
872
- const redoResult = this.#textDocument.redo();
873
- if (redoResult !== void 0) this.#applyChange(...redoResult);
874
- }
1286
+ this.#applyHistoryChange(command);
875
1287
  break;
876
1288
  }
877
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
+ }
878
1462
  #moveSelectedLines(direction) {
879
1463
  const textDocument = this.#textDocument;
880
1464
  const selections = this.#selections;
@@ -943,15 +1527,15 @@ var Editor = class {
943
1527
  this.#lineElementsCache.clear();
944
1528
  this.#lastAccessedCharX = void 0;
945
1529
  this.#metrics.clearTextWidthCache();
946
- if (contentWidthChanged && (this.#isWrap || lineAnnotations > 0)) {
947
- this.#lineYCache.clear();
1530
+ if (contentWidthChanged) {
948
1531
  this.#wrapLineOffsetsCache.clear();
1532
+ if (this.#isWrap || lineAnnotations > 0) this.#lineYCache.clear();
949
1533
  }
950
1534
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) {
951
1535
  this.#updateSelections(this.#selections ?? []);
952
- if (this.#selections !== void 0) this.focus();
1536
+ if (this.#selections !== void 0 && this.#contentHasFocus) this.focus();
953
1537
  }
954
- this.#markerRenderer?.removePopup();
1538
+ this.#markerRenderer?.removePopover();
955
1539
  this.#computeContentOffset(this.#contentElement);
956
1540
  };
957
1541
  #remeasureMetricsOnFontLoad() {
@@ -964,8 +1548,9 @@ var Editor = class {
964
1548
  this.#gutterWidthCache = void 0;
965
1549
  this.#contentWidthCache = void 0;
966
1550
  this.#resetCache();
1551
+ this.#wrapLineOffsetsCache.clear();
967
1552
  if (this.#selections !== void 0 || this.#matches !== void 0 || this.#markerRenderer !== void 0) this.#updateSelections(this.#selections ?? []);
968
- this.#markerRenderer?.removePopup();
1553
+ this.#markerRenderer?.removePopover();
969
1554
  });
970
1555
  }
971
1556
  #rerender(change, newLineAnnotations, renderRange = this.#renderRange, shouldUpdateBuffer) {
@@ -981,45 +1566,57 @@ var Editor = class {
981
1566
  const t2 = performance.now();
982
1567
  if (dirtyLines.size > 0) {
983
1568
  const children = contentEl.children;
984
- const dirtyLineIndexes = new Set(dirtyLines.keys());
985
- const startingLine = renderRange?.startingLine ?? 0;
986
- 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++) {
987
1572
  const child = children[i];
988
1573
  if (child !== void 0) {
989
1574
  const lineNumber = getLineNumberAttr(child);
990
1575
  const lineType = child.dataset.lineType;
991
1576
  if (lineNumber === void 0 || lineType === "change-deletion") continue;
992
1577
  const lineIndex = lineNumber - 1;
993
- if (dirtyLines.has(lineIndex)) {
1578
+ if (dirtyLineIndexes.has(lineIndex)) {
994
1579
  const tokens = dirtyLines.get(lineIndex);
995
- child.replaceChildren(...renderLineTokens(tokens, tokenizer.themeType));
1580
+ child.replaceChildren(...renderLineTokens(tokens));
996
1581
  dirtyLineIndexes.delete(lineIndex);
997
1582
  if (dirtyLineIndexes.size === 0) break;
998
1583
  }
999
1584
  }
1000
1585
  }
1001
- if (dirtyLineIndexes.size > 0) for (const lineIndex of dirtyLineIndexes) {
1002
- const tokens = dirtyLines.get(lineIndex);
1003
- const lineNumber = String(lineIndex + 1);
1004
- h("div", {
1005
- dataset: {
1006
- line: lineNumber,
1007
- lineType: "context",
1008
- lineIndex: lineIndex.toString()
1009
- },
1010
- children: renderLineTokens(tokens, tokenizer.themeType)
1011
- }, contentEl);
1012
- if (gutterEl !== void 0) h("div", {
1013
- dataset: {
1014
- lineType: "context",
1015
- columnNumber: lineNumber,
1016
- lineIndex: lineIndex.toString()
1017
- },
1018
- children: [h("span", {
1019
- dataset: { lineNumberContent: "" },
1020
- textContent: lineNumber
1021
- })]
1022
- }, 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
+ }
1023
1620
  }
1024
1621
  }
1025
1622
  if (change.lineDelta < 0) for (const children of [contentEl.children, gutterEl?.children ?? []]) for (let i = children.length - 1; i >= 0; i--) {
@@ -1040,9 +1637,9 @@ var Editor = class {
1040
1637
  contentEl.style.gridRow = "span " + gridRow;
1041
1638
  if (gutterEl !== void 0) gutterEl.style.gridRow = "span " + gridRow;
1042
1639
  }
1043
- fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange);
1640
+ fileInstance.updateRenderCache(dirtyLines, tokenizer.themeType, !didLineCountChange, didLineCountChange);
1044
1641
  if (didLineCountChange) fileInstance.applyDocumentChange(textDocument, newLineAnnotations, shouldUpdateBuffer);
1045
- if (this.#isDiff && (this.#diffSyle === "unified" || didLineCountChange)) this.#resetCache();
1642
+ if (didLineCountChange || this.#isDiff && this.#diffSyle === "unified") this.#resetCache();
1046
1643
  if (newLineAnnotations !== void 0) {
1047
1644
  this.#lineAnnotations = newLineAnnotations;
1048
1645
  renderLineAnnotations(newLineAnnotations, contentEl, gutterEl);
@@ -1062,7 +1659,7 @@ var Editor = class {
1062
1659
  case "insertCompositionText": break;
1063
1660
  case "insertLineBreak":
1064
1661
  case "insertParagraph":
1065
- this.#replaceSelectionText("\n");
1662
+ this.#replaceSelectionText(this.#textDocument?.eol ?? "\n");
1066
1663
  break;
1067
1664
  case "deleteContentBackward":
1068
1665
  this.#deleteSelectionText();
@@ -1074,9 +1671,6 @@ var Editor = class {
1074
1671
  case "deleteHardLineBackward":
1075
1672
  this.#deleteSoftLineBackward();
1076
1673
  break;
1077
- case "deleteHardLineForward":
1078
- this.#deleteHardLineForward();
1079
- break;
1080
1674
  case "deleteWordBackward":
1081
1675
  this.#deleteWordBackward();
1082
1676
  break;
@@ -1088,7 +1682,7 @@ var Editor = class {
1088
1682
  break;
1089
1683
  }
1090
1684
  }
1091
- #focus(position, preventScroll = true) {
1685
+ #focus(position, preventScroll = true, shouldFocus) {
1092
1686
  this.#contentHasFocus = true;
1093
1687
  if (position !== void 0) {
1094
1688
  this.#shouldIgnoreSelectionChange = true;
@@ -1097,13 +1691,115 @@ var Editor = class {
1097
1691
  end: position,
1098
1692
  direction: 0
1099
1693
  });
1100
- requestAnimationFrame(() => {
1694
+ queueRender(() => {
1695
+ if (shouldFocus?.() === false) {
1696
+ this.#shouldIgnoreSelectionChange = false;
1697
+ return;
1698
+ }
1101
1699
  this.#contentElement?.focus({ preventScroll });
1102
- requestAnimationFrame(() => {
1700
+ queueRender(() => {
1103
1701
  this.#shouldIgnoreSelectionChange = false;
1104
1702
  });
1105
1703
  });
1106
- } 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();
1107
1803
  }
1108
1804
  #setWindowSelection(selection) {
1109
1805
  const winSelection = window.getSelection();
@@ -1136,7 +1832,11 @@ var Editor = class {
1136
1832
  block: scrollPosition,
1137
1833
  inline: "nearest"
1138
1834
  });
1139
- 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
+ }
1140
1840
  } else {
1141
1841
  const pos = getCaretPosition(primarySelection);
1142
1842
  this.#scrollToLine(pos.line, pos.character, noFocus);
@@ -1166,10 +1866,14 @@ var Editor = class {
1166
1866
  block: "center",
1167
1867
  inline: "nearest"
1168
1868
  });
1169
- if (!noFocus) this.#focus({
1170
- line,
1171
- character: char
1172
- });
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
+ }
1173
1877
  this.#scrollingToLine = void 0;
1174
1878
  this.#scrollingToLineChar = void 0;
1175
1879
  this.#scrollingToLineFixed = false;
@@ -1183,7 +1887,7 @@ var Editor = class {
1183
1887
  block: "center",
1184
1888
  inline: "nearest"
1185
1889
  });
1186
- if (modelLinePosition.height > 0) {
1890
+ if (modelLinePosition.height > 0 && this.#isLineRenderable(line)) {
1187
1891
  this.#scrollingToLine = line;
1188
1892
  this.#scrollingToLineChar = char;
1189
1893
  this.#scrollingToLineNoFocus = noFocus;
@@ -1210,7 +1914,7 @@ var Editor = class {
1210
1914
  block: "center",
1211
1915
  inline: "nearest"
1212
1916
  });
1213
- if (this.#scrollingToLine === line && (yFix === 0 || this.#scrollingToLineFixed)) {
1917
+ if (this.#scrollingToLine === line && (yFix === 0 || this.#scrollingToLineFixed) || !this.#isLineRenderable(line)) {
1214
1918
  this.#scrollingToLine = void 0;
1215
1919
  this.#scrollingToLineChar = void 0;
1216
1920
  this.#scrollingToLineFixed = false;
@@ -1264,7 +1968,7 @@ var Editor = class {
1264
1968
  }
1265
1969
  this.#setDeletedTextSelectionActive(true);
1266
1970
  this.#updateSelections([]);
1267
- [...deletionsCode.querySelectorAll("[data-gutter] > [data-column-number]")][focusIndex]?.setAttribute("data-selected-line", "single");
1971
+ this.#setEditorActiveLineSafe(getLineNumberAttr(focusContent) ?? null, true, "deletions");
1268
1972
  const lo = Math.min(anchorIndex, focusIndex);
1269
1973
  const hi = Math.max(anchorIndex, focusIndex);
1270
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");
@@ -1302,19 +2006,18 @@ var Editor = class {
1302
2006
  if (pre == null) return;
1303
2007
  if (active) {
1304
2008
  pre.setAttribute("data-deleted-text-selection", "");
1305
- for (const highlighted of pre.querySelectorAll("[data-selected-line]")) highlighted.removeAttribute("data-selected-line");
2009
+ this.#setEditorActiveLineSafe(null);
1306
2010
  this.#deletedSelectionText = "";
1307
2011
  } else {
1308
2012
  pre.removeAttribute("data-deleted-text-selection");
1309
2013
  this.#deletedSelectionText = "";
1310
2014
  }
1311
2015
  }
1312
- #setSelectedLinesSafe(range, lineNumberOnly = false) {
2016
+ #setEditorActiveLineSafe(lineNumber, lineNumberOnly = false, side = "additions") {
1313
2017
  try {
1314
- this.#fileInstance?.setSelectedLines(range, {
1315
- notify: false,
1316
- activeLineSide: "additions",
1317
- lineNumberOnly
2018
+ this.#fileInstance?.setEditorActiveLine(lineNumber, {
2019
+ lineNumberOnly,
2020
+ side
1318
2021
  });
1319
2022
  } catch {}
1320
2023
  }
@@ -1328,7 +2031,7 @@ var Editor = class {
1328
2031
  #updateSelections(selections) {
1329
2032
  this.__postponeBgTokenizeToNextFrame();
1330
2033
  this.#primaryCaretElement = void 0;
1331
- this.#setSelectedLinesSafe(null);
2034
+ this.#setEditorActiveLineSafe(null);
1332
2035
  if (selections.length === 0 && this.#matches === void 0 && this.#markerRenderer === void 0) {
1333
2036
  this.#selections = void 0;
1334
2037
  this.#overlayElements?.forEach((el) => el.remove());
@@ -1348,10 +2051,7 @@ var Editor = class {
1348
2051
  this.#selections = normalizedSelections;
1349
2052
  const hasNonEmptySelection = normalizedSelections.some((selection) => !isCollapsedSelection(selection));
1350
2053
  const caretLine = getCaretPosition(primarySelection).line + 1;
1351
- this.#setSelectedLinesSafe({
1352
- start: caretLine,
1353
- end: caretLine
1354
- }, hasNonEmptySelection);
2054
+ this.#setEditorActiveLineSafe(caretLine, hasNonEmptySelection);
1355
2055
  for (const selection of normalizedSelections) {
1356
2056
  if (!isCollapsedSelection(selection)) this.#renderSelection(renderCtx, "selection", selection);
1357
2057
  this.#renderCaret(renderCtx, selection, selection === primarySelection);
@@ -1361,16 +2061,50 @@ var Editor = class {
1361
2061
  }
1362
2062
  const textDocument = this.#textDocument;
1363
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;
1364
2067
  const primarySelection = this.#selections?.at(-1);
1365
2068
  const primaryStartOffset = primarySelection !== void 0 ? textDocument.offsetAt(primarySelection.start) : -1;
1366
2069
  const primaryEndOffset = primarySelection !== void 0 ? textDocument.offsetAt(primarySelection.end) : -1;
1367
- for (const [startOffset, endOffset] of this.#matches) {
1368
- const range = {
1369
- start: textDocument.positionAt(startOffset),
1370
- end: textDocument.positionAt(endOffset)
1371
- };
1372
- const isFocused = primaryStartOffset === startOffset && primaryEndOffset === endOffset;
1373
- 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
+ }
1374
2108
  }
1375
2109
  }
1376
2110
  if (this.#markerRenderer !== void 0 && textDocument !== void 0) for (const marker of this.#markerRenderer.markers) this.#renderSelection(renderCtx, "marker", marker, markerSeverityDatasetKey(marker.severity));
@@ -1383,7 +2117,14 @@ var Editor = class {
1383
2117
  #renderSelection(renderCtx, type, range, extraDataset) {
1384
2118
  if (this.#textDocument === void 0) return;
1385
2119
  const { start, end } = range;
1386
- 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++) {
1387
2128
  if (!this.#isLineVisible(line)) continue;
1388
2129
  const isLastLine = line === end.line;
1389
2130
  const startChar = line === start.line ? start.character : 0;
@@ -1440,8 +2181,10 @@ var Editor = class {
1440
2181
  const rounded = (this.#options.roundedSelection ?? true) && type === "selection";
1441
2182
  const addRoundedCorner = (line, wrapLine, left, radius) => {
1442
2183
  const top = this.#getLineY(line) + wrapLine * lineHeight;
1443
- const cornerBg = this.#lineBackgroundColor(line);
1444
- 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}`;
1445
2188
  const dataset = {
1446
2189
  selectionCorner: "",
1447
2190
  [radius]: ""
@@ -1560,13 +2303,14 @@ var Editor = class {
1560
2303
  return;
1561
2304
  }
1562
2305
  if (this.#selectionAction === void 0) {
2306
+ if (!this.#canMountSelectionAction) return;
1563
2307
  const getActiveSelection = () => this.#selections?.at(-1) ?? primarySelection;
1564
2308
  const selectionActionElement = renderSelectionAction({
1565
2309
  textDocument,
1566
2310
  get selection() {
1567
2311
  return getActiveSelection();
1568
2312
  },
1569
- applyEdits: (edits) => this.applyEdits(edits, true),
2313
+ applyEdits: (edits) => this.applyEdits(edits),
1570
2314
  getSelectionText: () => this.#textDocument?.getText(getActiveSelection()) ?? "",
1571
2315
  replaceSelectionText: (text) => {
1572
2316
  this.#replaceSelectionText(text, [getActiveSelection()]);
@@ -1577,7 +2321,7 @@ var Editor = class {
1577
2321
  }
1578
2322
  });
1579
2323
  this.#selectionAction = new SelectionActionWidget(selectionActionElement, overlayElement, () => this.#updateSelectionActionPopover());
1580
- this.#getPopoverManager().resetPlacement();
2324
+ this.#getPopoverManager().resetPlacement(SELECTION_ACTION_POPOVER_PLACEMENT_KEY);
1581
2325
  }
1582
2326
  const lineHeight = this.#metrics.lineHeight;
1583
2327
  const isBackward = primarySelection.direction === -1;
@@ -1611,17 +2355,19 @@ var Editor = class {
1611
2355
  const atDocumentEdge = isBackward ? head.line < 3 : head.line >= lineCount - 3;
1612
2356
  const canUseFallback = this.#isLineVisible(fallback.anchor.line);
1613
2357
  const popoverManager = this.#getPopoverManager();
2358
+ const viewport = popoverManager.getPlacementBounds();
1614
2359
  const useFallback = canUseFallback && popoverManager.choosePlacement({
1615
2360
  preferred: preferredGeometry,
1616
2361
  fallback: fallbackGeometry,
1617
- viewport: popoverManager.getPlacementBounds(),
2362
+ viewport,
1618
2363
  popoverHeight,
1619
- atDocumentEdge
2364
+ atDocumentEdge,
2365
+ placementKey: SELECTION_ACTION_POPOVER_PLACEMENT_KEY
1620
2366
  }) === "fallback";
1621
- if (!canUseFallback) popoverManager.setPlacement("preferred");
2367
+ if (!canUseFallback) popoverManager.setPlacement("preferred", SELECTION_ACTION_POPOVER_PLACEMENT_KEY);
1622
2368
  const { placeAbove } = useFallback ? fallback : preferred;
1623
2369
  const { left, anchorTop } = useFallback ? fallbackGeometry : preferredGeometry;
1624
- this.#selectionAction.reposition(left, anchorTop, this.#getGutterWidth(), placeAbove);
2370
+ this.#selectionAction.reposition(left, anchorTop, this.#getGutterWidth(), placeAbove, viewport);
1625
2371
  }
1626
2372
  #openSearchPanel(mode) {
1627
2373
  if (this.#searchPanel !== void 0) {
@@ -1653,6 +2399,7 @@ var Editor = class {
1653
2399
  const scrollToMatch = ([startOffset, endOffset], retainFocus) => {
1654
2400
  const nextSelection = createSelectionFromAnchorAndFocusOffsets(textDocument, startOffset, endOffset);
1655
2401
  this.#updateSelections([nextSelection]);
2402
+ this.#revealLineIfCollapsed(getCaretPosition(nextSelection).line);
1656
2403
  this.#scrollToPrimaryCaret(true);
1657
2404
  this.#retainSearchPanelFocus = retainFocus;
1658
2405
  };
@@ -1719,6 +2466,18 @@ var Editor = class {
1719
2466
  if (textDocument === void 0 || selections === void 0) return "";
1720
2467
  return getSelectionText(textDocument, selections);
1721
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
+ }
1722
2481
  #cutSelectionText() {
1723
2482
  const textDocument = this.#textDocument;
1724
2483
  const selections = this.#selections;
@@ -1749,15 +2508,15 @@ var Editor = class {
1749
2508
  textDocument.setLastUndoSelectionsAfter(nextSelections);
1750
2509
  this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1751
2510
  }
1752
- #replaceSelectionText(text, selections = this.#selections, undoBoundary = false) {
2511
+ #replaceSelectionText(text, selections = this.#selections, undoBoundary = false, textOrder = "selection") {
1753
2512
  if (selections === void 0) return;
1754
2513
  const textDocument = this.#textDocument;
1755
2514
  const primarySelection = selections.at(-1);
1756
2515
  if (textDocument === void 0 || primarySelection === void 0) return;
1757
- 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, {
1758
2517
  start: textDocument.offsetAt(primarySelection.start),
1759
2518
  end: textDocument.offsetAt(primarySelection.end),
1760
- text: Array.isArray(text) ? text.join("\n") : text
2519
+ text: Array.isArray(text) ? text.join(textDocument.eol) : text
1761
2520
  }, this.#lineAnnotations, void 0, undoBoundary);
1762
2521
  if (change !== void 0) this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1763
2522
  }
@@ -1804,26 +2563,16 @@ var Editor = class {
1804
2563
  const { nextSelections, change } = applyTransposeToSelections(textDocument, selections, this.#lineAnnotations);
1805
2564
  if (change !== void 0) this.#applyChange(change, nextSelections, this.#applyChangeToLineAnnotations(change));
1806
2565
  }
1807
- #getFileRef() {
1808
- const fileInfo = this.#fileInfo;
1809
- const textDocument = this.#textDocument;
1810
- if (fileInfo === void 0 || textDocument === void 0) return;
1811
- const file = { ...fileInfo };
1812
- Object.defineProperty(file, "contents", {
1813
- enumerable: true,
1814
- get: () => textDocument.getText()
1815
- });
1816
- return file;
1817
- }
1818
2566
  #applyChange(change, newSelections, newLineAnnotations, options) {
1819
- const fileRef = this.#getFileRef();
2567
+ const fileRef = this.getFile();
1820
2568
  const onChange = this.#options.onChange;
1821
2569
  if (fileRef !== void 0 && onChange !== void 0) onChange(fileRef, newLineAnnotations ?? this.#lineAnnotations);
1822
2570
  if (change.lineDelta !== 0 || this.#isWrap) {
1823
2571
  for (const line of this.#lineYCache.keys()) if (line >= change.startLine) this.#lineYCache.delete(line);
1824
2572
  }
1825
- if (this.#isWrap) {
1826
- 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);
1827
2576
  }
1828
2577
  this.#lastAccessedCharX = void 0;
1829
2578
  let renderRange = this.#renderRange;
@@ -1834,7 +2583,7 @@ var Editor = class {
1834
2583
  if (primarySelection.end.line >= renderRangeEndLine) {
1835
2584
  const widenedTotalLines = primarySelection.end.line - renderRange.startingLine + 1;
1836
2585
  const maxWidenLines = this.#viewportWindowLines === void 0 || this.#viewportWindowLines === Infinity ? Infinity : this.#viewportWindowLines * MAX_EDIT_WIDEN_WINDOW_MULTIPLE;
1837
- if (change.startLine <= renderRangeEndLine && widenedTotalLines <= maxWidenLines) {
2586
+ if (change.startLine <= renderRangeEndLine && widenedTotalLines <= maxWidenLines && this.#isLineRenderable(primarySelection.end.line)) {
1838
2587
  if (primarySelection.end.line > renderRangeEndLine) shouldUpdateBuffer = true;
1839
2588
  renderRange = {
1840
2589
  ...renderRange,
@@ -1849,6 +2598,8 @@ var Editor = class {
1849
2598
  if (newSelections !== void 0) {
1850
2599
  this.#updateSelections(newSelections);
1851
2600
  if (options?.skipFocus !== true) {
2601
+ const revealTarget = newSelections.at(-1);
2602
+ if (revealTarget !== void 0) this.#revealLineIfCollapsed(getCaretPosition(revealTarget).line);
1852
2603
  if (this.#primaryCaretElement !== void 0) requestAnimationFrame(() => {
1853
2604
  this.#primaryCaretElement?.scrollIntoView({
1854
2605
  block: "nearest",
@@ -1872,39 +2623,31 @@ var Editor = class {
1872
2623
  }
1873
2624
  }
1874
2625
  }
1875
- #lineBackgroundColor(line) {
1876
- const lineElement = this.#getLineElement(line);
1877
- if (lineElement === void 0) return;
1878
- if (navigator.userAgent.includes("jsdom")) return;
1879
- const backgroundColor = getComputedStyle(lineElement, "::after").backgroundColor;
1880
- return backgroundColor === "" || backgroundColor === "transparent" || backgroundColor === "rgba(0, 0, 0, 0)" ? void 0 : backgroundColor;
1881
- }
1882
- #getRenderedEditableLineRange() {
2626
+ #getRenderedEditableLineRanges() {
1883
2627
  const contentElement = this.#contentElement;
1884
- if (contentElement === void 0) return;
1885
- let first;
1886
- let last;
2628
+ if (contentElement === void 0) return [];
2629
+ const ranges = [];
1887
2630
  for (const child of contentElement.children) {
1888
2631
  const el = child;
1889
2632
  const lineType = el.dataset.lineType;
1890
2633
  const lineNumber = getLineNumberAttr(el);
1891
2634
  if (lineNumber === void 0 || lineType === void 0 || !isLineEditable(lineType)) continue;
1892
2635
  const line = lineNumber - 1;
1893
- if (first === void 0 || line < first) first = line;
1894
- 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]);
1895
2639
  }
1896
- return first === void 0 || last === void 0 ? void 0 : {
1897
- first,
1898
- last
1899
- };
2640
+ return ranges;
1900
2641
  }
1901
2642
  #getLineElement(line) {
1902
2643
  let lineElement = this.#lineElementsCache.get(line);
1903
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;
1904
2647
  const contentElement = this.#contentElement;
1905
2648
  if (contentElement === void 0) return;
1906
- if (this.#renderRange !== void 0) {
1907
- const { startingLine } = this.#renderRange;
2649
+ if (renderRange !== void 0) {
2650
+ const { startingLine } = renderRange;
1908
2651
  const { children } = contentElement;
1909
2652
  for (let i = line - startingLine; i <= children.length; i++) {
1910
2653
  const child = children[i];
@@ -1995,6 +2738,7 @@ var Editor = class {
1995
2738
  #wrapLineText(line) {
1996
2739
  const cachedOffsets = this.#wrapLineOffsetsCache.get(line);
1997
2740
  if (cachedOffsets !== void 0) return cachedOffsets;
2741
+ if (this.#wrapLineOffsetsCache.size >= MAX_WRAP_OFFSETS_CACHE_LINES) this.#wrapLineOffsetsCache.clear();
1998
2742
  const lineText = this.#textDocument?.getLineText(line);
1999
2743
  if (lineText === void 0 || lineText.length === 0) {
2000
2744
  const offsets = new Uint32Array([0]);
@@ -2024,21 +2768,30 @@ var Editor = class {
2024
2768
  try {
2025
2769
  const unicodeOffsets = getUnicodeMeasurementOffsets(lineText);
2026
2770
  const wrapLineStartLeft = div.getBoundingClientRect().left + this.#metrics.ch;
2027
- let previousOffset = 0;
2028
- let lastTop = Number.NEGATIVE_INFINITY;
2029
- for (let i = 0, offsetIndex = 0; i < lineText.length;) {
2030
- const nextOffset = unicodeOffsets === void 0 ? i + 1 : unicodeOffsets[offsetIndex + 1];
2031
- range.setStart(textNode, i);
2032
- range.setEnd(textNode, nextOffset);
2033
- const { left, top } = range.getBoundingClientRect();
2034
- if (top > lastTop) {
2035
- const startsPastLineStart = isSafari() && starts.length > 0 && left - wrapLineStartLeft > this.#metrics.ch / 2;
2036
- starts.push(startsPastLineStart ? previousOffset : i);
2037
- 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;
2038
2788
  }
2039
- previousOffset = i;
2040
- i = nextOffset;
2041
- 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;
2042
2795
  }
2043
2796
  const offsets = new Uint32Array(starts.length + 1);
2044
2797
  for (let i = 0; i < starts.length; i++) offsets[i] = starts[i];