@nocturnium/svelte-ide 1.0.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 (330) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/components/agents/AgentActivityPanel.svelte +565 -0
  4. package/dist/components/agents/AgentActivityPanel.svelte.d.ts +24 -0
  5. package/dist/components/agents/AgentAvatar.svelte +417 -0
  6. package/dist/components/agents/AgentAvatar.svelte.d.ts +23 -0
  7. package/dist/components/agents/AgentCursor.svelte +224 -0
  8. package/dist/components/agents/AgentCursor.svelte.d.ts +35 -0
  9. package/dist/components/agents/AgentPresenceBar.svelte +261 -0
  10. package/dist/components/agents/AgentPresenceBar.svelte.d.ts +20 -0
  11. package/dist/components/agents/index.d.ts +4 -0
  12. package/dist/components/agents/index.js +5 -0
  13. package/dist/components/ai/AIConversationList.svelte +524 -0
  14. package/dist/components/ai/AIConversationList.svelte.d.ts +17 -0
  15. package/dist/components/ai/AIEditPreview.svelte +132 -0
  16. package/dist/components/ai/AIEditPreview.svelte.d.ts +8 -0
  17. package/dist/components/ai/AIInlineEdit.svelte +155 -0
  18. package/dist/components/ai/AIInlineEdit.svelte.d.ts +10 -0
  19. package/dist/components/ai/AIMessage.svelte +239 -0
  20. package/dist/components/ai/AIMessage.svelte.d.ts +13 -0
  21. package/dist/components/ai/AIMessageActions.svelte +176 -0
  22. package/dist/components/ai/AIMessageActions.svelte.d.ts +12 -0
  23. package/dist/components/ai/AIMessageContent.svelte +355 -0
  24. package/dist/components/ai/AIMessageContent.svelte.d.ts +7 -0
  25. package/dist/components/ai/AIPanel.svelte +561 -0
  26. package/dist/components/ai/AIPanel.svelte.d.ts +7 -0
  27. package/dist/components/ai/AISuggestionWidget.svelte +132 -0
  28. package/dist/components/ai/AISuggestionWidget.svelte.d.ts +10 -0
  29. package/dist/components/ai/AIToolCallDisplay.svelte +317 -0
  30. package/dist/components/ai/AIToolCallDisplay.svelte.d.ts +12 -0
  31. package/dist/components/ai/index.d.ts +9 -0
  32. package/dist/components/ai/index.js +10 -0
  33. package/dist/components/core/Avatar.svelte +110 -0
  34. package/dist/components/core/Avatar.svelte.d.ts +12 -0
  35. package/dist/components/core/Badge.svelte +98 -0
  36. package/dist/components/core/Badge.svelte.d.ts +11 -0
  37. package/dist/components/core/Button.svelte +175 -0
  38. package/dist/components/core/Button.svelte.d.ts +18 -0
  39. package/dist/components/core/ConnectionStatus.svelte +294 -0
  40. package/dist/components/core/ConnectionStatus.svelte.d.ts +20 -0
  41. package/dist/components/core/ContextMenu.svelte +176 -0
  42. package/dist/components/core/ContextMenu.svelte.d.ts +19 -0
  43. package/dist/components/core/ErrorBoundary.svelte +277 -0
  44. package/dist/components/core/ErrorBoundary.svelte.d.ts +23 -0
  45. package/dist/components/core/Icon.svelte +107 -0
  46. package/dist/components/core/Icon.svelte.d.ts +8 -0
  47. package/dist/components/core/Input.svelte +138 -0
  48. package/dist/components/core/Input.svelte.d.ts +20 -0
  49. package/dist/components/core/Kbd.svelte +34 -0
  50. package/dist/components/core/Kbd.svelte.d.ts +7 -0
  51. package/dist/components/core/ResizeHandle.svelte +200 -0
  52. package/dist/components/core/ResizeHandle.svelte.d.ts +23 -0
  53. package/dist/components/core/Spinner.svelte +35 -0
  54. package/dist/components/core/Spinner.svelte.d.ts +7 -0
  55. package/dist/components/core/Textarea.svelte +112 -0
  56. package/dist/components/core/Textarea.svelte.d.ts +18 -0
  57. package/dist/components/core/Tooltip.svelte +103 -0
  58. package/dist/components/core/Tooltip.svelte.d.ts +11 -0
  59. package/dist/components/core/index.d.ts +13 -0
  60. package/dist/components/core/index.js +14 -0
  61. package/dist/components/editor/AIFocusLayer.svelte +430 -0
  62. package/dist/components/editor/AIFocusLayer.svelte.d.ts +32 -0
  63. package/dist/components/editor/Breadcrumbs.svelte +435 -0
  64. package/dist/components/editor/Breadcrumbs.svelte.d.ts +33 -0
  65. package/dist/components/editor/BreakpointLayer.svelte +642 -0
  66. package/dist/components/editor/BreakpointLayer.svelte.d.ts +20 -0
  67. package/dist/components/editor/CognitiveLoadMeter.svelte +324 -0
  68. package/dist/components/editor/CognitiveLoadMeter.svelte.d.ts +18 -0
  69. package/dist/components/editor/CollaborativeEditor.svelte +218 -0
  70. package/dist/components/editor/CollaborativeEditor.svelte.d.ts +32 -0
  71. package/dist/components/editor/CommandPalette.svelte +434 -0
  72. package/dist/components/editor/CommandPalette.svelte.d.ts +11 -0
  73. package/dist/components/editor/ComplexityLayer.svelte +293 -0
  74. package/dist/components/editor/ComplexityLayer.svelte.d.ts +23 -0
  75. package/dist/components/editor/ConflictZoneLayer.svelte +441 -0
  76. package/dist/components/editor/ConflictZoneLayer.svelte.d.ts +25 -0
  77. package/dist/components/editor/ContextLens.svelte +262 -0
  78. package/dist/components/editor/ContextLens.svelte.d.ts +27 -0
  79. package/dist/components/editor/CustomEditor.svelte +1242 -0
  80. package/dist/components/editor/CustomEditor.svelte.d.ts +37 -0
  81. package/dist/components/editor/DebugConsole.svelte +646 -0
  82. package/dist/components/editor/DebugConsole.svelte.d.ts +41 -0
  83. package/dist/components/editor/EchoCursorLayer.svelte +363 -0
  84. package/dist/components/editor/EchoCursorLayer.svelte.d.ts +24 -0
  85. package/dist/components/editor/Editor.svelte +61 -0
  86. package/dist/components/editor/Editor.svelte.d.ts +22 -0
  87. package/dist/components/editor/EditorGutter.svelte +119 -0
  88. package/dist/components/editor/EditorGutter.svelte.d.ts +19 -0
  89. package/dist/components/editor/EditorLines.svelte +182 -0
  90. package/dist/components/editor/EditorLines.svelte.d.ts +43 -0
  91. package/dist/components/editor/EditorPane.svelte +134 -0
  92. package/dist/components/editor/EditorPane.svelte.d.ts +9 -0
  93. package/dist/components/editor/EditorSelections.svelte +186 -0
  94. package/dist/components/editor/EditorSelections.svelte.d.ts +25 -0
  95. package/dist/components/editor/EditorTabs.svelte +170 -0
  96. package/dist/components/editor/EditorTabs.svelte.d.ts +12 -0
  97. package/dist/components/editor/FileExplorer.svelte +811 -0
  98. package/dist/components/editor/FileExplorer.svelte.d.ts +67 -0
  99. package/dist/components/editor/FileIcon.svelte +110 -0
  100. package/dist/components/editor/FileIcon.svelte.d.ts +10 -0
  101. package/dist/components/editor/FindReplace.svelte +448 -0
  102. package/dist/components/editor/FindReplace.svelte.d.ts +40 -0
  103. package/dist/components/editor/GhostBracketLayer.svelte +391 -0
  104. package/dist/components/editor/GhostBracketLayer.svelte.d.ts +24 -0
  105. package/dist/components/editor/GitBlameLayer.svelte +436 -0
  106. package/dist/components/editor/GitBlameLayer.svelte.d.ts +18 -0
  107. package/dist/components/editor/InlineDiagnosticsLayer.svelte +540 -0
  108. package/dist/components/editor/InlineDiagnosticsLayer.svelte.d.ts +35 -0
  109. package/dist/components/editor/InlineDiffLayer.svelte +337 -0
  110. package/dist/components/editor/InlineDiffLayer.svelte.d.ts +31 -0
  111. package/dist/components/editor/MinimalEditor.svelte +75 -0
  112. package/dist/components/editor/MinimalEditor.svelte.d.ts +6 -0
  113. package/dist/components/editor/MinimalEditor2.svelte +84 -0
  114. package/dist/components/editor/MinimalEditor2.svelte.d.ts +6 -0
  115. package/dist/components/editor/Minimap.svelte +327 -0
  116. package/dist/components/editor/Minimap.svelte.d.ts +34 -0
  117. package/dist/components/editor/PluginPreviewSandbox.svelte +793 -0
  118. package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +49 -0
  119. package/dist/components/editor/ProblemsPanel.svelte +628 -0
  120. package/dist/components/editor/ProblemsPanel.svelte.d.ts +25 -0
  121. package/dist/components/editor/QuickActionsMenu.svelte +403 -0
  122. package/dist/components/editor/QuickActionsMenu.svelte.d.ts +18 -0
  123. package/dist/components/editor/SnippetPalette.svelte +530 -0
  124. package/dist/components/editor/SnippetPalette.svelte.d.ts +16 -0
  125. package/dist/components/editor/StructureMap.svelte +431 -0
  126. package/dist/components/editor/StructureMap.svelte.d.ts +37 -0
  127. package/dist/components/editor/SymbolOutline.svelte +722 -0
  128. package/dist/components/editor/SymbolOutline.svelte.d.ts +44 -0
  129. package/dist/components/editor/TimelineScrubber.svelte +470 -0
  130. package/dist/components/editor/TimelineScrubber.svelte.d.ts +40 -0
  131. package/dist/components/editor/TokenRenderer.svelte +69 -0
  132. package/dist/components/editor/TokenRenderer.svelte.d.ts +15 -0
  133. package/dist/components/editor/constants.d.ts +32 -0
  134. package/dist/components/editor/constants.js +36 -0
  135. package/dist/components/editor/core/ai-awareness.d.ts +176 -0
  136. package/dist/components/editor/core/ai-awareness.js +210 -0
  137. package/dist/components/editor/core/bracket-healer.d.ts +189 -0
  138. package/dist/components/editor/core/bracket-healer.js +406 -0
  139. package/dist/components/editor/core/breakpoints.d.ts +203 -0
  140. package/dist/components/editor/core/breakpoints.js +414 -0
  141. package/dist/components/editor/core/commands.d.ts +108 -0
  142. package/dist/components/editor/core/commands.js +246 -0
  143. package/dist/components/editor/core/complexity-analyzer.d.ts +123 -0
  144. package/dist/components/editor/core/complexity-analyzer.js +376 -0
  145. package/dist/components/editor/core/conflict-predictor.d.ts +135 -0
  146. package/dist/components/editor/core/conflict-predictor.js +316 -0
  147. package/dist/components/editor/core/crdt-binding.d.ts +118 -0
  148. package/dist/components/editor/core/crdt-binding.js +286 -0
  149. package/dist/components/editor/core/diagnostics.d.ts +210 -0
  150. package/dist/components/editor/core/diagnostics.js +335 -0
  151. package/dist/components/editor/core/echo-cursor.d.ts +201 -0
  152. package/dist/components/editor/core/echo-cursor.js +267 -0
  153. package/dist/components/editor/core/folding.d.ts +124 -0
  154. package/dist/components/editor/core/folding.js +672 -0
  155. package/dist/components/editor/core/ghost-pair.d.ts +122 -0
  156. package/dist/components/editor/core/ghost-pair.js +221 -0
  157. package/dist/components/editor/core/git-blame.d.ts +170 -0
  158. package/dist/components/editor/core/git-blame.js +324 -0
  159. package/dist/components/editor/core/index.d.ts +26 -0
  160. package/dist/components/editor/core/index.js +24 -0
  161. package/dist/components/editor/core/keybindings.d.ts +79 -0
  162. package/dist/components/editor/core/keybindings.js +357 -0
  163. package/dist/components/editor/core/multi-cursor.d.ts +196 -0
  164. package/dist/components/editor/core/multi-cursor.js +521 -0
  165. package/dist/components/editor/core/navigation.d.ts +107 -0
  166. package/dist/components/editor/core/navigation.js +408 -0
  167. package/dist/components/editor/core/quick-actions.d.ts +189 -0
  168. package/dist/components/editor/core/quick-actions.js +427 -0
  169. package/dist/components/editor/core/search.d.ts +88 -0
  170. package/dist/components/editor/core/search.js +192 -0
  171. package/dist/components/editor/core/semantic-analyzer.d.ts +77 -0
  172. package/dist/components/editor/core/semantic-analyzer.js +424 -0
  173. package/dist/components/editor/core/snippet-manager.d.ts +202 -0
  174. package/dist/components/editor/core/snippet-manager.js +565 -0
  175. package/dist/components/editor/core/state.d.ts +367 -0
  176. package/dist/components/editor/core/state.js +900 -0
  177. package/dist/components/editor/core/timeline.d.ts +204 -0
  178. package/dist/components/editor/core/timeline.js +349 -0
  179. package/dist/components/editor/editor-find.d.ts +56 -0
  180. package/dist/components/editor/editor-find.js +148 -0
  181. package/dist/components/editor/editor-input.d.ts +77 -0
  182. package/dist/components/editor/editor-input.js +445 -0
  183. package/dist/components/editor/editor-multicursor.d.ts +21 -0
  184. package/dist/components/editor/editor-multicursor.js +196 -0
  185. package/dist/components/editor/editor-scroll.d.ts +14 -0
  186. package/dist/components/editor/editor-scroll.js +34 -0
  187. package/dist/components/editor/index.d.ts +15 -0
  188. package/dist/components/editor/index.js +21 -0
  189. package/dist/components/editor/languages.d.ts +62 -0
  190. package/dist/components/editor/languages.js +285 -0
  191. package/dist/components/editor/theme.d.ts +88 -0
  192. package/dist/components/editor/theme.js +139 -0
  193. package/dist/components/editor/tokenizer/base.d.ts +40 -0
  194. package/dist/components/editor/tokenizer/base.js +203 -0
  195. package/dist/components/editor/tokenizer/index.d.ts +56 -0
  196. package/dist/components/editor/tokenizer/index.js +215 -0
  197. package/dist/components/editor/tokenizer/languages/css.d.ts +17 -0
  198. package/dist/components/editor/tokenizer/languages/css.js +194 -0
  199. package/dist/components/editor/tokenizer/languages/go.d.ts +17 -0
  200. package/dist/components/editor/tokenizer/languages/go.js +220 -0
  201. package/dist/components/editor/tokenizer/languages/html.d.ts +24 -0
  202. package/dist/components/editor/tokenizer/languages/html.js +145 -0
  203. package/dist/components/editor/tokenizer/languages/javascript.d.ts +56 -0
  204. package/dist/components/editor/tokenizer/languages/javascript.js +452 -0
  205. package/dist/components/editor/tokenizer/languages/json.d.ts +12 -0
  206. package/dist/components/editor/tokenizer/languages/json.js +91 -0
  207. package/dist/components/editor/tokenizer/languages/markdown.d.ts +16 -0
  208. package/dist/components/editor/tokenizer/languages/markdown.js +156 -0
  209. package/dist/components/editor/tokenizer/languages/python.d.ts +20 -0
  210. package/dist/components/editor/tokenizer/languages/python.js +227 -0
  211. package/dist/components/editor/tokenizer/languages/svelte.d.ts +40 -0
  212. package/dist/components/editor/tokenizer/languages/svelte.js +326 -0
  213. package/dist/components/editor/tokenizer/types.d.ts +86 -0
  214. package/dist/components/editor/tokenizer/types.js +4 -0
  215. package/dist/components/layout/IDELayout.svelte +274 -0
  216. package/dist/components/layout/IDELayout.svelte.d.ts +29 -0
  217. package/dist/components/layout/StatusBar.svelte +511 -0
  218. package/dist/components/layout/StatusBar.svelte.d.ts +47 -0
  219. package/dist/components/layout/index.d.ts +2 -0
  220. package/dist/components/layout/index.js +3 -0
  221. package/dist/components/lsp/AutocompleteWidget.svelte +364 -0
  222. package/dist/components/lsp/AutocompleteWidget.svelte.d.ts +33 -0
  223. package/dist/components/lsp/DiagnosticMarker.svelte +166 -0
  224. package/dist/components/lsp/DiagnosticMarker.svelte.d.ts +19 -0
  225. package/dist/components/lsp/DiagnosticsPanel.svelte +388 -0
  226. package/dist/components/lsp/DiagnosticsPanel.svelte.d.ts +21 -0
  227. package/dist/components/lsp/HoverTooltip.svelte +274 -0
  228. package/dist/components/lsp/HoverTooltip.svelte.d.ts +24 -0
  229. package/dist/components/lsp/LSPEditor.svelte +486 -0
  230. package/dist/components/lsp/LSPEditor.svelte.d.ts +39 -0
  231. package/dist/components/lsp/SignatureHelpWidget.svelte +216 -0
  232. package/dist/components/lsp/SignatureHelpWidget.svelte.d.ts +22 -0
  233. package/dist/components/lsp/index.d.ts +6 -0
  234. package/dist/components/lsp/index.js +7 -0
  235. package/dist/components/plugins/PluginCard.svelte +153 -0
  236. package/dist/components/plugins/PluginCard.svelte.d.ts +19 -0
  237. package/dist/components/plugins/PluginPanel.svelte +280 -0
  238. package/dist/components/plugins/PluginPanel.svelte.d.ts +8 -0
  239. package/dist/components/plugins/PluginProposalForm.svelte +250 -0
  240. package/dist/components/plugins/PluginProposalForm.svelte.d.ts +6 -0
  241. package/dist/components/plugins/PluginStatusBadge.svelte +14 -0
  242. package/dist/components/plugins/PluginStatusBadge.svelte.d.ts +8 -0
  243. package/dist/components/plugins/index.d.ts +4 -0
  244. package/dist/components/plugins/index.js +5 -0
  245. package/dist/components/vfs/LockConflictDialog.svelte +705 -0
  246. package/dist/components/vfs/LockConflictDialog.svelte.d.ts +21 -0
  247. package/dist/components/vfs/LockIndicator.svelte +194 -0
  248. package/dist/components/vfs/LockIndicator.svelte.d.ts +29 -0
  249. package/dist/components/vfs/LockOverlay.svelte +344 -0
  250. package/dist/components/vfs/LockOverlay.svelte.d.ts +17 -0
  251. package/dist/components/vfs/VersionConflictDialog.svelte +549 -0
  252. package/dist/components/vfs/VersionConflictDialog.svelte.d.ts +24 -0
  253. package/dist/components/vfs/index.d.ts +4 -0
  254. package/dist/components/vfs/index.js +5 -0
  255. package/dist/crdt/awareness.d.ts +42 -0
  256. package/dist/crdt/awareness.js +109 -0
  257. package/dist/crdt/document.d.ts +101 -0
  258. package/dist/crdt/document.js +187 -0
  259. package/dist/crdt/index.d.ts +9 -0
  260. package/dist/crdt/index.js +8 -0
  261. package/dist/crdt/provider.d.ts +85 -0
  262. package/dist/crdt/provider.js +150 -0
  263. package/dist/crdt/types.d.ts +61 -0
  264. package/dist/crdt/types.js +4 -0
  265. package/dist/crdt/undo.d.ts +34 -0
  266. package/dist/crdt/undo.js +70 -0
  267. package/dist/index.d.ts +277 -0
  268. package/dist/index.js +280 -0
  269. package/dist/plugins/index.d.ts +103 -0
  270. package/dist/plugins/index.js +153 -0
  271. package/dist/services/error-handling.d.ts +95 -0
  272. package/dist/services/error-handling.js +413 -0
  273. package/dist/services/ide-integration.d.ts +83 -0
  274. package/dist/services/ide-integration.js +367 -0
  275. package/dist/services/lsp-client.d.ts +69 -0
  276. package/dist/services/lsp-client.js +667 -0
  277. package/dist/services/mock-ai.d.ts +37 -0
  278. package/dist/services/mock-ai.js +318 -0
  279. package/dist/services/optimistic.d.ts +141 -0
  280. package/dist/services/optimistic.js +367 -0
  281. package/dist/services/vfs-client.d.ts +81 -0
  282. package/dist/services/vfs-client.js +348 -0
  283. package/dist/stores/agents.svelte.d.ts +85 -0
  284. package/dist/stores/agents.svelte.js +459 -0
  285. package/dist/stores/ai-persistence.svelte.d.ts +76 -0
  286. package/dist/stores/ai-persistence.svelte.js +334 -0
  287. package/dist/stores/ai.svelte.d.ts +140 -0
  288. package/dist/stores/ai.svelte.js +383 -0
  289. package/dist/stores/collaboration.svelte.d.ts +164 -0
  290. package/dist/stores/collaboration.svelte.js +334 -0
  291. package/dist/stores/editor.svelte.d.ts +131 -0
  292. package/dist/stores/editor.svelte.js +250 -0
  293. package/dist/stores/index.d.ts +10 -0
  294. package/dist/stores/index.js +29 -0
  295. package/dist/stores/layout.svelte.d.ts +171 -0
  296. package/dist/stores/layout.svelte.js +351 -0
  297. package/dist/stores/plugin.svelte.d.ts +121 -0
  298. package/dist/stores/plugin.svelte.js +410 -0
  299. package/dist/stores/vfs.svelte.d.ts +123 -0
  300. package/dist/stores/vfs.svelte.js +680 -0
  301. package/dist/styles/theme.css +623 -0
  302. package/dist/types/agents.d.ts +127 -0
  303. package/dist/types/agents.js +5 -0
  304. package/dist/types/ai.d.ts +137 -0
  305. package/dist/types/ai.js +4 -0
  306. package/dist/types/crdt.d.ts +222 -0
  307. package/dist/types/crdt.js +5 -0
  308. package/dist/types/editor.d.ts +52 -0
  309. package/dist/types/editor.js +18 -0
  310. package/dist/types/events.d.ts +133 -0
  311. package/dist/types/events.js +4 -0
  312. package/dist/types/filesystem.d.ts +77 -0
  313. package/dist/types/filesystem.js +4 -0
  314. package/dist/types/index.d.ts +9 -0
  315. package/dist/types/index.js +12 -0
  316. package/dist/types/lsp.d.ts +691 -0
  317. package/dist/types/lsp.js +108 -0
  318. package/dist/types/plugin.d.ts +239 -0
  319. package/dist/types/plugin.js +5 -0
  320. package/dist/types/vfs.d.ts +191 -0
  321. package/dist/types/vfs.js +18 -0
  322. package/dist/utils/format.d.ts +55 -0
  323. package/dist/utils/format.js +152 -0
  324. package/dist/utils/index.d.ts +3 -0
  325. package/dist/utils/index.js +4 -0
  326. package/dist/utils/keybindings.d.ts +33 -0
  327. package/dist/utils/keybindings.js +171 -0
  328. package/dist/utils/language.d.ts +27 -0
  329. package/dist/utils/language.js +222 -0
  330. package/package.json +178 -0
@@ -0,0 +1,900 @@
1
+ /**
2
+ * Core editor state management
3
+ *
4
+ * This module provides the fundamental state management for the custom editor,
5
+ * handling document content, cursor positions, selections, and history.
6
+ * Supports multi-cursor editing with automatic cursor merging.
7
+ */
8
+ import { getTokenizer, tokenize } from '../tokenizer';
9
+ import { HISTORY_GROUP_TIMEOUT_MS, MAX_HISTORY_SIZE } from '../constants';
10
+ import { CursorManager, createCursorManager, getSelectionStart, getSelectionEnd, isSelectionEmpty, comparePositions } from './multi-cursor';
11
+ /**
12
+ * Core editor state class
13
+ */
14
+ export class EditorState {
15
+ /** Document lines */
16
+ _lines = [];
17
+ /** Multi-cursor manager */
18
+ _cursorManager;
19
+ /** Language for syntax highlighting */
20
+ _language;
21
+ /** Tokenizer state for incremental tokenization */
22
+ _tokenizerStates = [];
23
+ /** Undo history */
24
+ _undoStack = [];
25
+ /** Redo history */
26
+ _redoStack = [];
27
+ /** Maximum history size */
28
+ maxHistorySize;
29
+ /** Tab settings */
30
+ tabSize;
31
+ insertSpaces;
32
+ /** Maximum listeners per type to prevent memory leaks */
33
+ static MAX_LISTENERS = 100;
34
+ /** Change listeners */
35
+ changeListeners = new Set();
36
+ /** Selection change listeners */
37
+ selectionListeners = new Set();
38
+ /** Cursor change listeners (for multi-cursor) */
39
+ cursorListeners = new Set();
40
+ /** Undo grouping: timestamp of last history save */
41
+ lastHistoryTimestamp = 0;
42
+ /** Undo grouping: type of last change */
43
+ lastHistoryType = null;
44
+ /** Undo grouping: time window in ms for grouping consecutive edits */
45
+ historyGroupTimeout = HISTORY_GROUP_TIMEOUT_MS;
46
+ /** Cached content string (invalidated on changes) */
47
+ _contentCache = null;
48
+ /** Flag to suppress notifications during atomic operations (e.g., CRDT sync) */
49
+ _suppressNotifications = false;
50
+ constructor(config = {}) {
51
+ this._language = config.language ?? 'plaintext';
52
+ this.maxHistorySize = config.maxHistorySize ?? MAX_HISTORY_SIZE;
53
+ this.tabSize = config.tabSize ?? 2;
54
+ this.insertSpaces = config.insertSpaces ?? false;
55
+ // Initialize cursor manager
56
+ this._cursorManager = createCursorManager({
57
+ maxCursors: config.maxCursors
58
+ });
59
+ // Set initial content
60
+ this.setContent(config.content ?? '');
61
+ }
62
+ // ============================================
63
+ // Content Access
64
+ // ============================================
65
+ /**
66
+ * Get all lines
67
+ */
68
+ get lines() {
69
+ return this._lines;
70
+ }
71
+ /**
72
+ * Get line count
73
+ */
74
+ get lineCount() {
75
+ return this._lines.length;
76
+ }
77
+ /**
78
+ * Get a specific line
79
+ */
80
+ getLine(lineNumber) {
81
+ return this._lines[lineNumber];
82
+ }
83
+ /**
84
+ * Get document content as string (cached for performance)
85
+ */
86
+ getContent() {
87
+ if (this._contentCache === null) {
88
+ this._contentCache = this._lines.map((l) => l.text).join('\n');
89
+ }
90
+ return this._contentCache;
91
+ }
92
+ /**
93
+ * Invalidate content cache (call when lines change)
94
+ */
95
+ invalidateContentCache() {
96
+ this._contentCache = null;
97
+ }
98
+ /**
99
+ * Set document content (replaces everything)
100
+ */
101
+ setContent(content) {
102
+ const oldContent = this.getContent();
103
+ // Normalize line endings: CRLF and CR to LF
104
+ const normalized = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
105
+ const textLines = normalized.split('\n');
106
+ this._lines = textLines.map((text, i) => ({
107
+ number: i,
108
+ text
109
+ }));
110
+ // Reset tokenizer states
111
+ this._tokenizerStates = new Array(this._lines.length).fill(undefined);
112
+ // Tokenize all lines
113
+ this.retokenize(0, this._lines.length);
114
+ // Emit change event
115
+ if (oldContent !== content) {
116
+ this.emitChange({
117
+ type: 'replace',
118
+ from: { line: 0, column: 0 },
119
+ to: this.endPosition(),
120
+ text: content,
121
+ removed: oldContent
122
+ });
123
+ }
124
+ }
125
+ /**
126
+ * Get current language
127
+ */
128
+ get language() {
129
+ return this._language;
130
+ }
131
+ /**
132
+ * Set language (triggers re-tokenization)
133
+ */
134
+ setLanguage(language) {
135
+ if (this._language !== language) {
136
+ this._language = language;
137
+ this._tokenizerStates = new Array(this._lines.length).fill(undefined);
138
+ this.retokenize(0, this._lines.length);
139
+ }
140
+ }
141
+ // ============================================
142
+ // Selection (backward compatible - uses primary cursor)
143
+ // ============================================
144
+ /**
145
+ * Get current selection (primary cursor)
146
+ */
147
+ get selection() {
148
+ return this._cursorManager.getPrimary().selection;
149
+ }
150
+ /**
151
+ * Get cursor position (primary cursor head)
152
+ */
153
+ get cursor() {
154
+ return this._cursorManager.getPrimary().selection.head;
155
+ }
156
+ /**
157
+ * Check if primary cursor has a selection
158
+ */
159
+ get hasSelection() {
160
+ return !isSelectionEmpty(this._cursorManager.getPrimary().selection);
161
+ }
162
+ /**
163
+ * Get normalized selection (start before end) for primary cursor
164
+ */
165
+ get normalizedSelection() {
166
+ const selection = this._cursorManager.getPrimary().selection;
167
+ return {
168
+ start: getSelectionStart(selection),
169
+ end: getSelectionEnd(selection)
170
+ };
171
+ }
172
+ /**
173
+ * Set cursor position (clears selection, affects primary cursor only)
174
+ */
175
+ setCursor(position) {
176
+ const clamped = this.clampPosition(position);
177
+ this._cursorManager.setSingleCursor(clamped);
178
+ this.emitSelectionChange();
179
+ this.emitCursorChange();
180
+ }
181
+ /**
182
+ * Set selection range (affects primary cursor only, clears secondary cursors)
183
+ */
184
+ setSelection(anchor, head) {
185
+ this._cursorManager.setSingleSelection(this.clampPosition(anchor), this.clampPosition(head));
186
+ this.emitSelectionChange();
187
+ this.emitCursorChange();
188
+ }
189
+ /**
190
+ * Extend selection to position (keeps anchor, primary cursor only)
191
+ */
192
+ extendSelection(head) {
193
+ const primary = this._cursorManager.getPrimary();
194
+ this._cursorManager.extendSelection(primary.id, this.clampPosition(head));
195
+ this.emitSelectionChange();
196
+ this.emitCursorChange();
197
+ }
198
+ /**
199
+ * Select all
200
+ */
201
+ selectAll() {
202
+ this.setSelection({ line: 0, column: 0 }, this.endPosition());
203
+ }
204
+ /**
205
+ * Get selected text (primary cursor)
206
+ */
207
+ getSelectedText() {
208
+ return this.getTextInSelection(this._cursorManager.getPrimary().selection);
209
+ }
210
+ /**
211
+ * Get selected text from all cursors
212
+ * Joins multiple selections with newlines (VS Code behavior)
213
+ */
214
+ getSelectedTextFromAllCursors() {
215
+ const selections = [];
216
+ for (const cursor of this._cursorManager.getSortedCursors()) {
217
+ if (!isSelectionEmpty(cursor.selection)) {
218
+ selections.push(this.getTextInSelection(cursor.selection));
219
+ }
220
+ }
221
+ return selections.join('\n');
222
+ }
223
+ /**
224
+ * Check if any cursor has a selection
225
+ */
226
+ get hasAnySelection() {
227
+ for (const cursor of this._cursorManager.getCursors()) {
228
+ if (!isSelectionEmpty(cursor.selection)) {
229
+ return true;
230
+ }
231
+ }
232
+ return false;
233
+ }
234
+ /**
235
+ * Get text within a selection range
236
+ */
237
+ getTextInSelection(selection) {
238
+ if (isSelectionEmpty(selection)) {
239
+ return '';
240
+ }
241
+ const start = getSelectionStart(selection);
242
+ const end = getSelectionEnd(selection);
243
+ if (start.line === end.line) {
244
+ return this._lines[start.line].text.slice(start.column, end.column);
245
+ }
246
+ const lines = [];
247
+ lines.push(this._lines[start.line].text.slice(start.column));
248
+ for (let i = start.line + 1; i < end.line; i++) {
249
+ lines.push(this._lines[i].text);
250
+ }
251
+ lines.push(this._lines[end.line].text.slice(0, end.column));
252
+ return lines.join('\n');
253
+ }
254
+ // ============================================
255
+ // Multi-Cursor API
256
+ // ============================================
257
+ /**
258
+ * Get cursor manager for advanced multi-cursor operations
259
+ */
260
+ get cursorManager() {
261
+ return this._cursorManager;
262
+ }
263
+ /**
264
+ * Get all cursors
265
+ */
266
+ get allCursors() {
267
+ return this._cursorManager.getCursors();
268
+ }
269
+ /**
270
+ * Get primary cursor
271
+ */
272
+ get primaryCursor() {
273
+ return this._cursorManager.getPrimary();
274
+ }
275
+ /**
276
+ * Check if there are multiple cursors
277
+ */
278
+ get hasMultipleCursors() {
279
+ return this._cursorManager.hasMultiple;
280
+ }
281
+ /**
282
+ * Add a new cursor at position
283
+ */
284
+ addCursor(position, makePrimary = false) {
285
+ const clamped = this.clampPosition(position);
286
+ const cursor = this._cursorManager.addCursor(clamped, makePrimary);
287
+ this.emitCursorChange();
288
+ return cursor;
289
+ }
290
+ /**
291
+ * Add a cursor with selection
292
+ */
293
+ addCursorWithSelection(anchor, head, makePrimary = false) {
294
+ const cursor = this._cursorManager.addCursorWithSelection(this.clampPosition(anchor), this.clampPosition(head), makePrimary);
295
+ this.emitCursorChange();
296
+ return cursor;
297
+ }
298
+ /**
299
+ * Add cursor above primary cursor
300
+ */
301
+ addCursorAbove() {
302
+ const primary = this._cursorManager.getPrimary();
303
+ const pos = primary.selection.head;
304
+ if (pos.line <= 0)
305
+ return false;
306
+ const newLine = pos.line - 1;
307
+ const lineText = this._lines[newLine]?.text ?? '';
308
+ const newColumn = Math.min(pos.column, lineText.length);
309
+ this._cursorManager.addCursor({ line: newLine, column: newColumn });
310
+ this.emitCursorChange();
311
+ return true;
312
+ }
313
+ /**
314
+ * Add cursor below primary cursor
315
+ */
316
+ addCursorBelow() {
317
+ const primary = this._cursorManager.getPrimary();
318
+ const pos = primary.selection.head;
319
+ if (pos.line >= this._lines.length - 1)
320
+ return false;
321
+ const newLine = pos.line + 1;
322
+ const lineText = this._lines[newLine]?.text ?? '';
323
+ const newColumn = Math.min(pos.column, lineText.length);
324
+ this._cursorManager.addCursor({ line: newLine, column: newColumn });
325
+ this.emitCursorChange();
326
+ return true;
327
+ }
328
+ /**
329
+ * Remove last added secondary cursor
330
+ */
331
+ removeLastCursor() {
332
+ const result = this._cursorManager.removeLastSecondary();
333
+ if (result) {
334
+ this.emitCursorChange();
335
+ }
336
+ return result;
337
+ }
338
+ /**
339
+ * Clear all secondary cursors
340
+ */
341
+ clearSecondaryCursors() {
342
+ this._cursorManager.clearSecondary();
343
+ this.emitCursorChange();
344
+ }
345
+ // ============================================
346
+ // Editing Operations (Multi-Cursor Aware)
347
+ // ============================================
348
+ /**
349
+ * Insert text at all cursor positions
350
+ */
351
+ insert(text) {
352
+ this.saveHistory('insert');
353
+ // Get cursors in reverse order (bottom to top) to maintain position validity
354
+ const cursors = this._cursorManager.getSortedCursorsReverse();
355
+ // Store cursor updates to apply after all insertions
356
+ const updates = [];
357
+ for (const cursor of cursors) {
358
+ let currentPos = cursor.selection.head;
359
+ // Delete selection first if exists
360
+ if (!isSelectionEmpty(cursor.selection)) {
361
+ const start = getSelectionStart(cursor.selection);
362
+ const end = getSelectionEnd(cursor.selection);
363
+ this.deleteRangeInternal(start, end);
364
+ currentPos = start;
365
+ }
366
+ // Insert text
367
+ const newPos = this.insertAtInternal(currentPos, text);
368
+ updates.push({ id: cursor.id, position: newPos });
369
+ }
370
+ // Apply all cursor updates using batch update to avoid multiple merge/emit calls
371
+ this._cursorManager.batchUpdateCursors(updates);
372
+ this.emitChange({ type: 'insert', from: this.cursor, text });
373
+ this.emitSelectionChange();
374
+ this.emitCursorChange();
375
+ }
376
+ /**
377
+ * Insert text at a specific position (internal, doesn't update cursor)
378
+ * @returns New position after insert
379
+ */
380
+ insertAtInternal(position, text) {
381
+ const pos = this.clampPosition(position);
382
+ const line = this._lines[pos.line];
383
+ const textLines = text.split('\n');
384
+ if (textLines.length === 1) {
385
+ // Single line insert
386
+ line.text = line.text.slice(0, pos.column) + text + line.text.slice(pos.column);
387
+ // Re-tokenize from the edited line to the end of the document: the
388
+ // tokenizer carries multi-line state (block comments, template
389
+ // literals), so typing `/*` must propagate downward. retokenize()
390
+ // early-exits once the carried state re-converges, so this is cheap
391
+ // in the common single-line case.
392
+ this.retokenize(pos.line, this._lines.length);
393
+ return { line: pos.line, column: pos.column + text.length };
394
+ }
395
+ else {
396
+ // Multi-line insert
397
+ const before = line.text.slice(0, pos.column);
398
+ const after = line.text.slice(pos.column);
399
+ // Modify first line
400
+ line.text = before + textLines[0];
401
+ // Insert middle lines
402
+ const newLines = textLines.slice(1, -1).map((t, i) => ({
403
+ number: pos.line + i + 1,
404
+ text: t
405
+ }));
406
+ // Add last line
407
+ newLines.push({
408
+ number: pos.line + textLines.length - 1,
409
+ text: textLines[textLines.length - 1] + after
410
+ });
411
+ // Splice in new lines
412
+ this._lines.splice(pos.line + 1, 0, ...newLines);
413
+ this._tokenizerStates.splice(pos.line + 1, 0, ...new Array(newLines.length).fill(undefined));
414
+ // Renumber lines
415
+ this.renumberLines(pos.line + 1);
416
+ // Retokenize affected lines
417
+ this.retokenize(pos.line, pos.line + textLines.length);
418
+ // Return new position
419
+ const lastLineIdx = textLines.length - 1;
420
+ return {
421
+ line: pos.line + lastLineIdx,
422
+ column: textLines[lastLineIdx].length
423
+ };
424
+ }
425
+ }
426
+ /**
427
+ * Insert text at a specific position (public API - single cursor)
428
+ */
429
+ insertAt(position, text) {
430
+ this.saveHistory('insert');
431
+ const newPos = this.insertAtInternal(position, text);
432
+ this.setCursor(newPos);
433
+ this.emitChange({ type: 'insert', from: position, text });
434
+ }
435
+ /**
436
+ * Delete the current selection (all cursors)
437
+ */
438
+ deleteSelection() {
439
+ const cursors = this._cursorManager.getSortedCursorsReverse();
440
+ let hasAnySelection = false;
441
+ for (const cursor of cursors) {
442
+ if (!isSelectionEmpty(cursor.selection)) {
443
+ hasAnySelection = true;
444
+ }
445
+ }
446
+ if (!hasAnySelection) {
447
+ return;
448
+ }
449
+ this.saveHistory('delete');
450
+ const updates = [];
451
+ for (const cursor of cursors) {
452
+ if (!isSelectionEmpty(cursor.selection)) {
453
+ const start = getSelectionStart(cursor.selection);
454
+ const end = getSelectionEnd(cursor.selection);
455
+ this.deleteRangeInternal(start, end);
456
+ updates.push({ id: cursor.id, position: start });
457
+ }
458
+ }
459
+ // Apply all cursor updates using batch update
460
+ this._cursorManager.batchUpdateCursors(updates);
461
+ this.emitChange({ type: 'delete', from: this.cursor, to: this.cursor });
462
+ this.emitSelectionChange();
463
+ this.emitCursorChange();
464
+ }
465
+ /**
466
+ * Delete a range of text (internal, doesn't update cursor)
467
+ */
468
+ deleteRangeInternal(from, to) {
469
+ if (from.line === to.line) {
470
+ // Single line deletion
471
+ const line = this._lines[from.line];
472
+ line.text = line.text.slice(0, from.column) + line.text.slice(to.column);
473
+ // Re-tokenize to end of document so removing a multi-line construct
474
+ // delimiter (e.g. a closing `*/`) re-propagates state to lines below.
475
+ this.retokenize(from.line, this._lines.length);
476
+ }
477
+ else {
478
+ // Multi-line deletion
479
+ const firstLine = this._lines[from.line];
480
+ const lastLine = this._lines[to.line];
481
+ // Merge first and last lines
482
+ firstLine.text = firstLine.text.slice(0, from.column) + lastLine.text.slice(to.column);
483
+ // Remove intermediate lines
484
+ const removedCount = to.line - from.line;
485
+ this._lines.splice(from.line + 1, removedCount);
486
+ this._tokenizerStates.splice(from.line + 1, removedCount);
487
+ // Renumber lines
488
+ this.renumberLines(from.line + 1);
489
+ // Retokenize from affected line to end
490
+ this.retokenize(from.line, this._lines.length);
491
+ }
492
+ }
493
+ /**
494
+ * Delete a range of text (public API - single cursor)
495
+ */
496
+ deleteRange(from, to) {
497
+ this.deleteRangeInternal(from, to);
498
+ this.setCursor(from);
499
+ }
500
+ /**
501
+ * Delete character before all cursors (backspace)
502
+ */
503
+ deleteBackward() {
504
+ // Check if any cursor has a selection
505
+ const cursors = this._cursorManager.getSortedCursorsReverse();
506
+ let hasAnySelection = false;
507
+ for (const cursor of cursors) {
508
+ if (!isSelectionEmpty(cursor.selection)) {
509
+ hasAnySelection = true;
510
+ break;
511
+ }
512
+ }
513
+ if (hasAnySelection) {
514
+ this.deleteSelection();
515
+ return;
516
+ }
517
+ this.saveHistory('delete');
518
+ const updates = [];
519
+ for (const cursor of cursors) {
520
+ const { line, column } = cursor.selection.head;
521
+ if (column > 0) {
522
+ // Delete within line
523
+ const currentLine = this._lines[line];
524
+ currentLine.text = currentLine.text.slice(0, column - 1) + currentLine.text.slice(column);
525
+ // Re-tokenize to end of document so deleting a multi-line construct
526
+ // delimiter re-propagates state to lines below.
527
+ this.retokenize(line, this._lines.length);
528
+ updates.push({ id: cursor.id, position: { line, column: column - 1 } });
529
+ }
530
+ else if (line > 0) {
531
+ // Join with previous line
532
+ const prevLine = this._lines[line - 1];
533
+ const currentLine = this._lines[line];
534
+ const newColumn = prevLine.text.length;
535
+ prevLine.text += currentLine.text;
536
+ this._lines.splice(line, 1);
537
+ this._tokenizerStates.splice(line, 1);
538
+ this.renumberLines(line);
539
+ this.retokenize(line - 1, this._lines.length);
540
+ updates.push({ id: cursor.id, position: { line: line - 1, column: newColumn } });
541
+ }
542
+ }
543
+ // Apply all cursor updates using batch update
544
+ this._cursorManager.batchUpdateCursors(updates);
545
+ this.emitChange({ type: 'delete', from: this.cursor, to: this.cursor });
546
+ this.emitSelectionChange();
547
+ this.emitCursorChange();
548
+ }
549
+ /**
550
+ * Delete character after all cursors (delete key)
551
+ */
552
+ deleteForward() {
553
+ // Check if any cursor has a selection
554
+ const cursors = this._cursorManager.getSortedCursorsReverse();
555
+ let hasAnySelection = false;
556
+ for (const cursor of cursors) {
557
+ if (!isSelectionEmpty(cursor.selection)) {
558
+ hasAnySelection = true;
559
+ break;
560
+ }
561
+ }
562
+ if (hasAnySelection) {
563
+ this.deleteSelection();
564
+ return;
565
+ }
566
+ this.saveHistory('delete');
567
+ for (const cursor of cursors) {
568
+ const { line, column } = cursor.selection.head;
569
+ const currentLine = this._lines[line];
570
+ if (column < currentLine.text.length) {
571
+ // Delete within line
572
+ currentLine.text = currentLine.text.slice(0, column) + currentLine.text.slice(column + 1);
573
+ // Re-tokenize to end of document so deleting a multi-line construct
574
+ // delimiter re-propagates state to lines below.
575
+ this.retokenize(line, this._lines.length);
576
+ }
577
+ else if (line < this._lines.length - 1) {
578
+ // Join with next line
579
+ const nextLine = this._lines[line + 1];
580
+ currentLine.text += nextLine.text;
581
+ this._lines.splice(line + 1, 1);
582
+ this._tokenizerStates.splice(line + 1, 1);
583
+ this.renumberLines(line + 1);
584
+ this.retokenize(line, this._lines.length);
585
+ }
586
+ }
587
+ this.emitChange({ type: 'delete', from: this.cursor, to: this.cursor });
588
+ this.emitSelectionChange();
589
+ }
590
+ /**
591
+ * Insert a new line (enter key)
592
+ */
593
+ insertNewline() {
594
+ this.insert('\n');
595
+ }
596
+ /**
597
+ * Insert tab
598
+ */
599
+ insertTab() {
600
+ const tabStr = this.insertSpaces ? ' '.repeat(this.tabSize) : '\t';
601
+ this.insert(tabStr);
602
+ }
603
+ // ============================================
604
+ // History (Undo/Redo)
605
+ // ============================================
606
+ /**
607
+ * Save current state to history with optional grouping
608
+ * @param changeType - Type of change for grouping consecutive edits
609
+ */
610
+ saveHistory(changeType) {
611
+ const now = Date.now();
612
+ // Check if we should merge with the last history entry
613
+ const shouldMerge = (changeType &&
614
+ changeType === this.lastHistoryType &&
615
+ this._undoStack.length > 0 &&
616
+ now - this.lastHistoryTimestamp < this.historyGroupTimeout);
617
+ // Save cursor state
618
+ const cursorState = this._cursorManager.clone();
619
+ const entry = {
620
+ content: this.getContent(),
621
+ selection: this.deepCopySelection(this.selection),
622
+ cursors: cursorState.cursors,
623
+ primaryCursorId: cursorState.primaryId,
624
+ timestamp: now
625
+ };
626
+ if (shouldMerge) {
627
+ // Update the last entry instead of creating a new one
628
+ this._undoStack[this._undoStack.length - 1] = entry;
629
+ }
630
+ else {
631
+ this._undoStack.push(entry);
632
+ // Limit history size
633
+ if (this._undoStack.length > this.maxHistorySize) {
634
+ this._undoStack.shift();
635
+ }
636
+ }
637
+ // Update grouping state
638
+ this.lastHistoryTimestamp = now;
639
+ this.lastHistoryType = changeType ?? null;
640
+ // Clear redo stack on new edit
641
+ this._redoStack = [];
642
+ }
643
+ /**
644
+ * Undo last change
645
+ */
646
+ undo() {
647
+ const entry = this._undoStack.pop();
648
+ if (!entry) {
649
+ return false;
650
+ }
651
+ // Save current state to redo stack
652
+ const currentCursorState = this._cursorManager.clone();
653
+ this._redoStack.push({
654
+ content: this.getContent(),
655
+ selection: this.deepCopySelection(this.selection),
656
+ cursors: currentCursorState.cursors,
657
+ primaryCursorId: currentCursorState.primaryId,
658
+ timestamp: Date.now()
659
+ });
660
+ // Restore state
661
+ this.setContent(entry.content);
662
+ // Restore cursor state
663
+ if (entry.cursors && entry.primaryCursorId) {
664
+ this._cursorManager.restore(entry.cursors, entry.primaryCursorId);
665
+ }
666
+ else {
667
+ // Fallback for old history entries
668
+ this._cursorManager.setSingleSelection(entry.selection.anchor, entry.selection.head);
669
+ }
670
+ this.emitSelectionChange();
671
+ this.emitCursorChange();
672
+ return true;
673
+ }
674
+ /**
675
+ * Redo last undone change
676
+ */
677
+ redo() {
678
+ const entry = this._redoStack.pop();
679
+ if (!entry) {
680
+ return false;
681
+ }
682
+ // Save current state to undo stack (with size limit)
683
+ const currentCursorState = this._cursorManager.clone();
684
+ this._undoStack.push({
685
+ content: this.getContent(),
686
+ selection: this.deepCopySelection(this.selection),
687
+ cursors: currentCursorState.cursors,
688
+ primaryCursorId: currentCursorState.primaryId,
689
+ timestamp: Date.now()
690
+ });
691
+ // Enforce history size limit
692
+ while (this._undoStack.length > this.maxHistorySize) {
693
+ this._undoStack.shift();
694
+ }
695
+ // Restore state
696
+ this.setContent(entry.content);
697
+ // Restore cursor state
698
+ if (entry.cursors && entry.primaryCursorId) {
699
+ this._cursorManager.restore(entry.cursors, entry.primaryCursorId);
700
+ }
701
+ else {
702
+ // Fallback for old history entries
703
+ this._cursorManager.setSingleSelection(entry.selection.anchor, entry.selection.head);
704
+ }
705
+ this.emitSelectionChange();
706
+ this.emitCursorChange();
707
+ return true;
708
+ }
709
+ /**
710
+ * Deep copy a selection to prevent reference issues in history
711
+ */
712
+ deepCopySelection(sel) {
713
+ return {
714
+ anchor: { line: sel.anchor.line, column: sel.anchor.column },
715
+ head: { line: sel.head.line, column: sel.head.column }
716
+ };
717
+ }
718
+ /**
719
+ * Check if undo is available
720
+ */
721
+ get canUndo() {
722
+ return this._undoStack.length > 0;
723
+ }
724
+ /**
725
+ * Check if redo is available
726
+ */
727
+ get canRedo() {
728
+ return this._redoStack.length > 0;
729
+ }
730
+ // ============================================
731
+ // Tokenization
732
+ // ============================================
733
+ /**
734
+ * Compare two tokenizer states for equality
735
+ */
736
+ tokenizerStatesEqual(a, b) {
737
+ if (a === b)
738
+ return true;
739
+ if (!a || !b)
740
+ return a === b;
741
+ return (a.inBlockComment === b.inBlockComment &&
742
+ a.inTemplateLiteral === b.inTemplateLiteral &&
743
+ a.inMultilineString === b.inMultilineString &&
744
+ a.stringDelimiter === b.stringDelimiter &&
745
+ a.templateDepth === b.templateDepth);
746
+ }
747
+ /**
748
+ * Re-tokenize a range of lines with early-exit optimization
749
+ */
750
+ retokenize(startLine, endLine) {
751
+ const tokenizer = getTokenizer(this._language);
752
+ let state = startLine > 0 ? this._tokenizerStates[startLine - 1] : tokenizer.getInitialState();
753
+ for (let i = startLine; i < endLine && i < this._lines.length; i++) {
754
+ const line = this._lines[i];
755
+ const oldState = this._tokenizerStates[i];
756
+ const tokenized = tokenizer.tokenizeLine(line.text, i + 1, state);
757
+ line.tokens = tokenized;
758
+ state = tokenized.state;
759
+ // Early exit: if state unchanged from what was stored, subsequent lines are unchanged
760
+ if (i > startLine && this.tokenizerStatesEqual(oldState, state)) {
761
+ break;
762
+ }
763
+ this._tokenizerStates[i] = state;
764
+ }
765
+ }
766
+ // ============================================
767
+ // Helpers
768
+ // ============================================
769
+ /**
770
+ * Clamp a position to valid document bounds
771
+ */
772
+ clampPosition(pos) {
773
+ const line = Math.max(0, Math.min(pos.line, this._lines.length - 1));
774
+ const lineText = this._lines[line]?.text ?? '';
775
+ const column = Math.max(0, Math.min(pos.column, lineText.length));
776
+ return { line, column };
777
+ }
778
+ /**
779
+ * Get end position of document
780
+ */
781
+ endPosition() {
782
+ if (this._lines.length === 0) {
783
+ return { line: 0, column: 0 };
784
+ }
785
+ const lastLine = this._lines.length - 1;
786
+ return { line: lastLine, column: this._lines[lastLine].text.length };
787
+ }
788
+ /**
789
+ * Renumber lines from a starting point
790
+ */
791
+ renumberLines(startFrom) {
792
+ for (let i = startFrom; i < this._lines.length; i++) {
793
+ this._lines[i].number = i;
794
+ }
795
+ }
796
+ // ============================================
797
+ // Event Listeners
798
+ // ============================================
799
+ /**
800
+ * Helper to add a listener with overflow protection
801
+ * In development mode, throws an error to catch memory leaks early.
802
+ * In production, warns and evicts the oldest listener.
803
+ */
804
+ addListener(listeners, callback, name) {
805
+ if (listeners.size >= EditorState.MAX_LISTENERS) {
806
+ const msg = `[EditorState] Maximum ${name} listener count (${EditorState.MAX_LISTENERS}) exceeded. Possible memory leak - ensure listeners are unsubscribed.`;
807
+ // Fail fast in development to catch bugs early
808
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
809
+ throw new Error(msg);
810
+ }
811
+ // In production, warn and auto-cleanup oldest listener
812
+ console.warn(msg);
813
+ const firstListener = listeners.values().next().value;
814
+ if (firstListener) {
815
+ listeners.delete(firstListener);
816
+ }
817
+ }
818
+ listeners.add(callback);
819
+ return () => listeners.delete(callback);
820
+ }
821
+ /**
822
+ * Add change listener
823
+ */
824
+ onContentChange(callback) {
825
+ return this.addListener(this.changeListeners, callback, 'change');
826
+ }
827
+ /**
828
+ * Add selection change listener
829
+ */
830
+ onSelectionChange(callback) {
831
+ return this.addListener(this.selectionListeners, callback, 'selection');
832
+ }
833
+ /**
834
+ * Add cursor change listener (for multi-cursor updates)
835
+ */
836
+ onCursorChange(callback) {
837
+ return this.addListener(this.cursorListeners, callback, 'cursor');
838
+ }
839
+ emitChange(event) {
840
+ // Invalidate cache when content changes
841
+ this.invalidateContentCache();
842
+ if (this._suppressNotifications)
843
+ return;
844
+ for (const listener of this.changeListeners) {
845
+ try {
846
+ listener(event);
847
+ }
848
+ catch (error) {
849
+ console.error('[EditorState] Change listener failed:', error);
850
+ }
851
+ }
852
+ }
853
+ emitSelectionChange() {
854
+ if (this._suppressNotifications)
855
+ return;
856
+ const selection = this.selection;
857
+ for (const listener of this.selectionListeners) {
858
+ try {
859
+ listener(selection);
860
+ }
861
+ catch (error) {
862
+ console.error('[EditorState] Selection listener failed:', error);
863
+ }
864
+ }
865
+ }
866
+ emitCursorChange() {
867
+ if (this._suppressNotifications)
868
+ return;
869
+ const cursors = this._cursorManager.getCursors();
870
+ for (const listener of this.cursorListeners) {
871
+ try {
872
+ listener(cursors);
873
+ }
874
+ catch (error) {
875
+ console.error('[EditorState] Cursor listener failed:', error);
876
+ }
877
+ }
878
+ }
879
+ /**
880
+ * Run a function with notifications suppressed.
881
+ * Useful for atomic operations like CRDT sync where we want to
882
+ * update content and selection without triggering intermediate events.
883
+ */
884
+ runWithoutNotifications(fn) {
885
+ const wasSupressed = this._suppressNotifications;
886
+ this._suppressNotifications = true;
887
+ try {
888
+ return fn();
889
+ }
890
+ finally {
891
+ this._suppressNotifications = wasSupressed;
892
+ }
893
+ }
894
+ }
895
+ /**
896
+ * Create editor state
897
+ */
898
+ export function createEditorState(config) {
899
+ return new EditorState(config);
900
+ }