@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,1242 @@
1
+ <script lang="ts">
2
+ import { onMount, tick, untrack } from 'svelte';
3
+ import type { EditorPreferences } from '../../types';
4
+ import {
5
+ createEditorState,
6
+ createNavigation,
7
+ createKeyboardHandler,
8
+ createDefaultKeybindings,
9
+ createFoldManager,
10
+ type EditorState,
11
+ type Selection,
12
+ type SearchMatch,
13
+ type FoldManager,
14
+ type FoldRegion,
15
+ type Cursor
16
+ } from './core';
17
+ import FindReplace from './FindReplace.svelte';
18
+ import { createEditorFind, type EditorFind } from './editor-find';
19
+ import { selectNextOccurrence, selectAllOccurrences } from './editor-multicursor';
20
+ import { createEditorInput } from './editor-input';
21
+ import { createEditorScroll } from './editor-scroll';
22
+ import ComplexityLayer from './ComplexityLayer.svelte';
23
+ import AIFocusLayer from './AIFocusLayer.svelte';
24
+ import EditorSelections from './EditorSelections.svelte';
25
+ import EditorLines from './EditorLines.svelte';
26
+ import CommandPalette from './CommandPalette.svelte';
27
+ import { getComplexityAnalyzer, type ComplexityMetrics } from './core/complexity-analyzer';
28
+ import { registerSemanticFoldCommands } from './core/commands';
29
+ import { getSemanticAnalyzer } from './core/semantic-analyzer';
30
+ import type { AIAwareness } from './core/ai-awareness';
31
+ import {
32
+ CURSOR_BLINK_MS,
33
+ ONCHANGE_DEBOUNCE_MS,
34
+ DEFAULT_CHAR_WIDTH,
35
+ DEFAULT_LINE_HEIGHT,
36
+ DEFAULT_GUTTER_WIDTH,
37
+ CONTENT_PADDING,
38
+ GUTTER_PADDING,
39
+ FALLBACK_VIEWPORT_HEIGHT,
40
+ DEFAULT_FONT_SIZE,
41
+ DEFAULT_WORD_WRAP_COLUMN
42
+ } from './constants';
43
+
44
+ interface Props {
45
+ content: string;
46
+ language?: string;
47
+ readonly?: boolean;
48
+ preferences?: Partial<EditorPreferences>;
49
+ class?: string;
50
+ /** Enable code folding */
51
+ folding?: boolean;
52
+ /** Enable multi-cursor editing (default: true) */
53
+ multiCursor?: boolean;
54
+ /** Maximum number of cursors (default: 100) */
55
+ maxCursors?: number;
56
+ /** Enable complexity highlighting (default: true) */
57
+ complexityHighlighting?: boolean;
58
+ /** Minimum complexity score to show highlighting (default: 50) */
59
+ complexityThreshold?: number;
60
+ /** AI agents for Ghost Pair visualization */
61
+ aiAgents?: AIAwareness[];
62
+ /** Show AI cursor labels (default: true) */
63
+ showAILabels?: boolean;
64
+ /** Show AI focus regions (default: true) */
65
+ showAIFocusRegions?: boolean;
66
+ onChange?: (content: string) => void;
67
+ onCursorChange?: (line: number, column: number) => void;
68
+ /** Callback when cursors change (for multi-cursor) */
69
+ onCursorsChange?: (cursors: readonly Cursor[]) => void;
70
+ /** Callback when complexity metrics change */
71
+ onComplexityChange?: (metrics: ComplexityMetrics | null) => void;
72
+ onSave?: () => void;
73
+ }
74
+
75
+ let {
76
+ content = $bindable(),
77
+ language = 'plaintext',
78
+ readonly = false,
79
+ preferences = {},
80
+ class: className = '',
81
+ folding = true,
82
+ multiCursor = true,
83
+ maxCursors = 100,
84
+ complexityHighlighting = true,
85
+ complexityThreshold = 50,
86
+ aiAgents = [],
87
+ showAILabels = true,
88
+ showAIFocusRegions = true,
89
+ onChange,
90
+ onCursorChange,
91
+ onCursorsChange,
92
+ onComplexityChange,
93
+ onSave
94
+ }: Props = $props();
95
+
96
+ // Default preferences
97
+ const defaultPrefs: EditorPreferences = {
98
+ fontSize: DEFAULT_FONT_SIZE,
99
+ fontFamily: 'JetBrains Mono',
100
+ tabSize: 2,
101
+ insertSpaces: false,
102
+ wordWrap: 'off',
103
+ wordWrapColumn: DEFAULT_WORD_WRAP_COLUMN,
104
+ lineNumbers: 'on',
105
+ minimap: false,
106
+ bracketMatching: true,
107
+ autoCloseBrackets: true,
108
+ highlightActiveLine: true,
109
+ renderWhitespace: 'none',
110
+ theme: 'nocturnium'
111
+ };
112
+
113
+ const mergedPrefs = $derived({ ...defaultPrefs, ...preferences });
114
+
115
+ // Editor state
116
+ let editorState = $state<EditorState>(null!);
117
+ let navigation: ReturnType<typeof createNavigation>;
118
+ let keyboardHandler: ReturnType<typeof createKeyboardHandler>;
119
+
120
+ // Fold manager
121
+ let foldManager: FoldManager = createFoldManager();
122
+ let foldRegions = $state<readonly FoldRegion[]>([]);
123
+ let visibleLineIndices = $state<number[]>([]);
124
+ let unsubscribeFold: (() => void) | null = null;
125
+
126
+ // Complexity analyzer
127
+ const complexityAnalyzer = getComplexityAnalyzer();
128
+ let complexityMetrics = $state<ComplexityMetrics | null>(null);
129
+ let complexityUpdateTimeout: ReturnType<typeof setTimeout> | null = null;
130
+
131
+ // DOM refs
132
+ let container: HTMLDivElement;
133
+ let editorContent = $state<HTMLDivElement>(null!);
134
+ let hiddenInput: HTMLTextAreaElement;
135
+ let measureSpan: HTMLSpanElement;
136
+
137
+ // Computed measurements
138
+ let charWidth = $state(DEFAULT_CHAR_WIDTH);
139
+ let lineHeight = $state(DEFAULT_LINE_HEIGHT);
140
+
141
+ // Gutter width will be calculated after lines is defined
142
+ let gutterWidth = $state(DEFAULT_GUTTER_WIDTH);
143
+
144
+ // Scroll state
145
+ let scrollTop = $state(0);
146
+ let scrollLeft = $state(0);
147
+
148
+ // Reactive viewport height of the scrollable content area. Drives line
149
+ // virtualization (only rows within scrollTop +/- viewport + overscan render).
150
+ let viewportHeight = $state(FALLBACK_VIEWPORT_HEIGHT);
151
+
152
+ // Selection rendering
153
+ let cursorVisible = $state(true);
154
+ let cursorBlinkInterval: ReturnType<typeof setInterval>;
155
+
156
+ // Track previous content prop to detect external changes only
157
+ // svelte-ignore state_referenced_locally
158
+ let previousContentProp = $state(content);
159
+
160
+ // Track the content the editor itself last emitted (via onChange / bind:content
161
+ // write-back). When the parent round-trips this value back into the `content`
162
+ // prop, we must treat it as an echo and NOT call setContent — doing so would
163
+ // rebuild the document model every keystroke, wiping undo/redo and collapsing
164
+ // multi-cursor. Only a genuine external change (content !== what we emitted and
165
+ // !== the current editor content) should re-apply via setContent.
166
+ // svelte-ignore state_referenced_locally
167
+ let lastEmittedContent = content;
168
+
169
+ // Track last cursor position to avoid unnecessary blink resets
170
+ let lastCursorLine = -1;
171
+ let lastCursorColumn = -1;
172
+
173
+ // Input handler module (created in initEditor)
174
+ let inputHandlers = $state(null as unknown as ReturnType<typeof createEditorInput>);
175
+
176
+ // Scroll management module
177
+ const editorScroll = createEditorScroll({
178
+ getEditorContent: () => editorContent,
179
+ getSelection: () => selection,
180
+ getMeasurements: () => ({ lineHeight, charWidth, gutterWidth, contentPadding: CONTENT_PADDING })
181
+ });
182
+ const { scrollCursorIntoView } = editorScroll;
183
+
184
+ // Cleanup functions for subscriptions
185
+ let unsubscribeContent: (() => void) | null = null;
186
+ let unsubscribeSelection: (() => void) | null = null;
187
+ let unsubscribeCursors: (() => void) | null = null;
188
+
189
+ // Debounce timeout for onChange
190
+ let onChangeTimeout: ReturnType<typeof setTimeout> | null = null;
191
+
192
+ // Debounce timeout for fold region detection
193
+ let foldUpdateTimeout: ReturnType<typeof setTimeout> | null = null;
194
+
195
+ // Reactive state from editor
196
+ let lines = $state<readonly ReturnType<typeof editorState.getLine>[]>([]);
197
+ let selection = $state<Selection>({ anchor: { line: 0, column: 0 }, head: { line: 0, column: 0 } });
198
+ let hasSelection = $state(false);
199
+ let cursors = $state<readonly Cursor[]>([]);
200
+ let hasMultipleCursors = $state(false);
201
+
202
+ // Update gutter width when lines change
203
+ $effect(() => {
204
+ const lineCount = lines.length || 1;
205
+ const digits = Math.max(2, String(lineCount).length);
206
+ gutterWidth = digits * charWidth + GUTTER_PADDING;
207
+ });
208
+
209
+ // Find/Replace state
210
+ let showFindReplace = $state(false);
211
+ let findReplaceComponent = $state<FindReplace | null>(null);
212
+ let editorFind: EditorFind;
213
+
214
+ // Reactive wrappers for find/replace state (drive template reactivity via $state)
215
+ let searchQuery = $state('');
216
+ let replaceTextState = $state('');
217
+ let searchCaseSensitive = $state(false);
218
+ let searchUseRegex = $state(false);
219
+ let searchMatches = $state<SearchMatch[]>([]);
220
+ let currentMatchIndex = $state(-1);
221
+
222
+ /** Sync reactive $state from the editorFind module after any mutation */
223
+ function syncFindState(): void {
224
+ searchQuery = editorFind.query;
225
+ replaceTextState = editorFind.replaceText;
226
+ searchCaseSensitive = editorFind.caseSensitive;
227
+ searchUseRegex = editorFind.useRegex;
228
+ searchMatches = editorFind.matches;
229
+ currentMatchIndex = editorFind.currentIndex;
230
+ }
231
+
232
+ // Command palette state
233
+ let showCommandPalette = $state(false);
234
+
235
+ // Screen reader dynamic announcements
236
+ let srAnnouncement = $state('');
237
+
238
+ // Helper to make screen reader announcements
239
+ function announce(message: string) {
240
+ // Clear first to ensure repeated messages are announced
241
+ srAnnouncement = '';
242
+ requestAnimationFrame(() => {
243
+ srAnnouncement = message;
244
+ });
245
+ }
246
+
247
+ // Debounced onChange to avoid excessive callbacks
248
+ function debouncedOnChange(newContent: string) {
249
+ if (onChangeTimeout) {
250
+ clearTimeout(onChangeTimeout);
251
+ }
252
+ onChangeTimeout = setTimeout(() => {
253
+ try {
254
+ onChange?.(newContent);
255
+ } catch (error) {
256
+ // Report error to console for debugging
257
+ console.error('[CustomEditor] onChange callback failed:', error);
258
+ } finally {
259
+ // Always clear timeout reference, even if onChange throws
260
+ onChangeTimeout = null;
261
+ }
262
+ }, ONCHANGE_DEBOUNCE_MS);
263
+ }
264
+
265
+ // Initialize editor
266
+ function initEditor() {
267
+ // Clean up old subscriptions before creating new ones
268
+ unsubscribeContent?.();
269
+ unsubscribeSelection?.();
270
+ unsubscribeCursors?.();
271
+ unsubscribeFold?.();
272
+ unsubscribeContent = null;
273
+ unsubscribeSelection = null;
274
+ unsubscribeCursors = null;
275
+ unsubscribeFold = null;
276
+
277
+ editorState = createEditorState({
278
+ content,
279
+ language,
280
+ tabSize: mergedPrefs.tabSize,
281
+ insertSpaces: mergedPrefs.insertSpaces,
282
+ maxCursors: multiCursor ? maxCursors : 1
283
+ });
284
+
285
+ initEditorFind();
286
+
287
+ navigation = createNavigation(editorState);
288
+ keyboardHandler = createKeyboardHandler();
289
+
290
+ // Set up keybindings
291
+ const keybindings = createDefaultKeybindings(editorState, navigation, {
292
+ onSave,
293
+ readonly,
294
+ pageSize: Math.floor((container?.clientHeight ?? FALLBACK_VIEWPORT_HEIGHT) / lineHeight)
295
+ });
296
+ keyboardHandler.setKeybindings(keybindings);
297
+
298
+ // Subscribe to state changes and store unsubscribe functions
299
+ unsubscribeContent = editorState.onContentChange(() => {
300
+ lines = editorState.lines;
301
+ const newContent = editorState.getContent();
302
+ // Record what we emit so the round-trip back through the `content` prop is
303
+ // recognised as an echo (see external-change $effect below).
304
+ lastEmittedContent = newContent;
305
+ // Write back to the bindable prop so `bind:content` consumers stay in sync.
306
+ content = newContent;
307
+ previousContentProp = newContent;
308
+ debouncedOnChange(newContent);
309
+ // Update fold regions when content changes (debounced for performance)
310
+ if (folding) {
311
+ debouncedUpdateFoldRegions();
312
+ }
313
+ // Update complexity metrics when content changes (debounced for performance)
314
+ if (complexityHighlighting) {
315
+ debouncedUpdateComplexity();
316
+ }
317
+ });
318
+
319
+ unsubscribeSelection = editorState.onSelectionChange((sel) => {
320
+ selection = sel;
321
+ hasSelection = editorState.hasSelection;
322
+ onCursorChange?.(sel.head.line + 1, sel.head.column + 1);
323
+
324
+ // Only reset blink if cursor position actually changed
325
+ if (sel.head.line !== lastCursorLine || sel.head.column !== lastCursorColumn) {
326
+ lastCursorLine = sel.head.line;
327
+ lastCursorColumn = sel.head.column;
328
+ resetCursorBlink();
329
+ }
330
+ });
331
+
332
+ // Subscribe to cursor changes (multi-cursor)
333
+ unsubscribeCursors = editorState.onCursorChange((allCursors) => {
334
+ cursors = allCursors;
335
+ hasMultipleCursors = allCursors.length > 1;
336
+ onCursorsChange?.(allCursors);
337
+ });
338
+
339
+ // Subscribe to fold changes
340
+ unsubscribeFold = foldManager.onChange(() => {
341
+ foldRegions = foldManager.getRegions();
342
+ visibleLineIndices = foldManager.getVisibleLines(editorState.lineCount);
343
+ });
344
+
345
+ // Initial state
346
+ lines = editorState.lines;
347
+ selection = editorState.selection;
348
+ cursors = editorState.allCursors;
349
+ hasMultipleCursors = cursors.length > 1;
350
+
351
+ // Initialize folding
352
+ if (folding) {
353
+ updateFoldRegions();
354
+ }
355
+
356
+ // Initialize complexity analysis
357
+ if (complexityHighlighting) {
358
+ updateComplexityMetrics();
359
+ }
360
+
361
+ // Create input handler module
362
+ inputHandlers = createEditorInput({
363
+ getEditorState: () => editorState,
364
+ getNavigation: () => navigation,
365
+ getKeyboardHandler: () => keyboardHandler,
366
+ getFoldManager: () => foldManager,
367
+
368
+ getEditorContent: () => editorContent,
369
+ getHiddenInput: () => hiddenInput,
370
+ getMeasureSpan: () => measureSpan,
371
+
372
+ isReadonly: () => readonly,
373
+ isMultiCursor: () => multiCursor,
374
+ isFolding: () => folding,
375
+ hasMultipleCursors: () => hasMultipleCursors,
376
+ getSelection: () => selection,
377
+ getCursors: () => cursors,
378
+ getScrollPosition: () => ({ scrollTop, scrollLeft }),
379
+ getMeasurements: () => ({ charWidth, lineHeight, gutterWidth }),
380
+
381
+ onOpenFind: (withReplace) => openFind(withReplace),
382
+ onCloseFind: () => closeFind(),
383
+ onFindNext: () => handleFindNext(),
384
+ onFindPrev: () => handleFindPrev(),
385
+ onFoldCurrent: () => handleFoldCurrent(),
386
+ onUnfoldCurrent: () => handleUnfoldCurrent(),
387
+ onFoldAll: () => foldManager.collapseAll(),
388
+ onUnfoldAll: () => foldManager.expandAll(),
389
+ onSelectNextOccurrence: () => selectNextOccurrence(editorState),
390
+ onSelectAllOccurrences: () => selectAllOccurrences(editorState),
391
+ onShowCommandPalette: () => { showCommandPalette = true; },
392
+ onSave,
393
+ announce,
394
+ resetCursorBlink,
395
+ stopCursorBlink: () => {
396
+ if (cursorBlinkInterval) {
397
+ clearInterval(cursorBlinkInterval);
398
+ }
399
+ },
400
+
401
+ setCharWidth: (w) => { charWidth = w; },
402
+ setLineHeight: (h) => { lineHeight = h; },
403
+ setScrollTop: (v) => { scrollTop = v; },
404
+ setScrollLeft: (v) => { scrollLeft = v; },
405
+ setCursorVisible: (v) => { cursorVisible = v; },
406
+
407
+ isShowFindReplace: () => showFindReplace,
408
+ getSearchQuery: () => searchQuery,
409
+ getSearchMatchCount: () => searchMatches.length,
410
+ });
411
+ }
412
+
413
+ // Update fold regions
414
+ function updateFoldRegions() {
415
+ if (!editorState || !folding) return;
416
+ foldManager.updateRegions(editorState.lines, language);
417
+ foldRegions = foldManager.getRegions();
418
+ visibleLineIndices = foldManager.getVisibleLines(editorState.lineCount);
419
+ }
420
+
421
+ // Debounced fold region update for performance on large files
422
+ function debouncedUpdateFoldRegions() {
423
+ if (foldUpdateTimeout) {
424
+ clearTimeout(foldUpdateTimeout);
425
+ }
426
+ foldUpdateTimeout = setTimeout(() => {
427
+ updateFoldRegions();
428
+ foldUpdateTimeout = null;
429
+ }, 250); // Longer debounce for fold detection
430
+ }
431
+
432
+ // Update complexity metrics
433
+ function updateComplexityMetrics() {
434
+ if (!editorState || !complexityHighlighting) {
435
+ complexityMetrics = null;
436
+ return;
437
+ }
438
+ const metrics = complexityAnalyzer.analyze(editorState.lines, language);
439
+ complexityMetrics = metrics;
440
+ onComplexityChange?.(metrics);
441
+ }
442
+
443
+ // Debounced complexity update for performance
444
+ function debouncedUpdateComplexity() {
445
+ if (complexityUpdateTimeout) {
446
+ clearTimeout(complexityUpdateTimeout);
447
+ }
448
+ complexityUpdateTimeout = setTimeout(() => {
449
+ updateComplexityMetrics();
450
+ complexityUpdateTimeout = null;
451
+ }, 300); // Slightly longer debounce for complexity analysis
452
+ }
453
+
454
+ // Fold/unfold handlers
455
+ function handleFoldCurrent() {
456
+ if (!folding || !editorState) return;
457
+ const currentLine = selection.head.line;
458
+ // Find fold region at or containing current line
459
+ const region = foldManager.getRegionAtLine(currentLine);
460
+ if (region) {
461
+ foldManager.collapse(currentLine);
462
+ }
463
+ }
464
+
465
+ function handleUnfoldCurrent() {
466
+ if (!folding || !editorState) return;
467
+ const currentLine = selection.head.line;
468
+ foldManager.expand(currentLine);
469
+ }
470
+
471
+ function handleFoldIndicatorClick(lineNumber: number, e: MouseEvent) {
472
+ e.preventDefault();
473
+ e.stopPropagation();
474
+ const wasCollapsed = foldManager.isFoldCollapsed(lineNumber);
475
+ foldManager.toggleFold(lineNumber);
476
+ const hiddenLines = foldManager.getHiddenLineCount(lineNumber);
477
+ if (wasCollapsed) {
478
+ announce(`Expanded fold at line ${lineNumber + 1}, ${hiddenLines} lines now visible`);
479
+ } else {
480
+ announce(`Collapsed fold at line ${lineNumber + 1}, ${hiddenLines} lines hidden`);
481
+ }
482
+ }
483
+
484
+ // Memoized visible lines for rendering (filters out folded lines).
485
+ // The array position of each entry is its VISUAL ROW (folded lines are
486
+ // compacted out), which maps to a vertical offset of visualRow * lineHeight.
487
+ let visibleLines = $derived.by(() => {
488
+ if (!folding || visibleLineIndices.length === 0) {
489
+ return lines.map((line, index) => ({ line, index }));
490
+ }
491
+ return visibleLineIndices.map(index => ({
492
+ line: lines[index],
493
+ index
494
+ }));
495
+ });
496
+
497
+ /** Extra rows rendered above/below the viewport to avoid blank edges while scrolling. */
498
+ const ROW_OVERSCAN = 8;
499
+
500
+ // Total scrollable content height (in px) for the full document, accounting
501
+ // for folded/hidden lines. Drives the spacer so the scrollbar stays correct.
502
+ let totalContentHeight = $derived(Math.max(visibleLines.length, 1) * lineHeight);
503
+
504
+ // Windowed slice of visibleLines: only the rows intersecting the viewport
505
+ // (plus overscan) are rendered to the DOM. Each entry keeps its absolute
506
+ // visualRow so it can be positioned at its true offset (visualRow * lineHeight),
507
+ // independent of the slice. This caps DOM nodes at ~viewport-worth regardless
508
+ // of document size (10k+ lines still render only the visible window).
509
+ let windowedLines = $derived.by(() => {
510
+ const rowCount = visibleLines.length;
511
+ if (rowCount === 0) return [];
512
+
513
+ const rowsPerViewport = Math.ceil((viewportHeight || FALLBACK_VIEWPORT_HEIGHT) / lineHeight);
514
+ const firstRow = Math.max(0, Math.floor(scrollTop / lineHeight) - ROW_OVERSCAN);
515
+ const lastRow = Math.min(rowCount - 1, firstRow + rowsPerViewport + ROW_OVERSCAN * 2);
516
+
517
+ const slice: Array<{ line: (typeof visibleLines)[number]['line']; index: number; visualRow: number }> = [];
518
+ for (let visualRow = firstRow; visualRow <= lastRow; visualRow++) {
519
+ const entry = visibleLines[visualRow];
520
+ slice.push({ line: entry.line, index: entry.index, visualRow });
521
+ }
522
+ return slice;
523
+ });
524
+
525
+ // Check if a line has a fold indicator
526
+ function hasFoldIndicator(lineIndex: number): boolean {
527
+ if (!folding) return false;
528
+ return foldManager.hasFoldIndicator(lineIndex);
529
+ }
530
+
531
+ // Check if fold is collapsed at a line
532
+ function isFoldCollapsed(lineIndex: number): boolean {
533
+ if (!folding) return false;
534
+ return foldManager.isFoldCollapsed(lineIndex);
535
+ }
536
+
537
+ // Get number of hidden lines for a collapsed fold
538
+ function getHiddenLineCount(lineIndex: number): number {
539
+ if (!folding) return 0;
540
+ return foldManager.getHiddenLineCount(lineIndex);
541
+ }
542
+
543
+ // Multi-cursor selection helpers are in ./editor-multicursor.ts
544
+
545
+ // Cursor blink management
546
+ function resetCursorBlink() {
547
+ cursorVisible = true;
548
+ if (cursorBlinkInterval) {
549
+ clearInterval(cursorBlinkInterval);
550
+ }
551
+ cursorBlinkInterval = setInterval(() => {
552
+ cursorVisible = !cursorVisible;
553
+ }, CURSOR_BLINK_MS);
554
+ }
555
+
556
+ // ============================================
557
+ // Find/Replace functions
558
+ // ============================================
559
+
560
+ function initEditorFind(): void {
561
+ editorFind = createEditorFind({
562
+ getLines: () => editorState.lines,
563
+ getLineCount: () => editorState.lineCount,
564
+ getSelection: () => editorState.selection,
565
+ setSelection: (anchor, head) => editorState.setSelection(anchor, head),
566
+ setCursor: (pos) => editorState.setCursor(pos),
567
+ setContent: (content) => editorState.setContent(content),
568
+ scrollCursorIntoView: () => scrollCursorIntoView(),
569
+ focusEditor: () => hiddenInput?.focus(),
570
+ isReadonly: () => readonly
571
+ });
572
+ }
573
+
574
+ function openFind(withReplace = false) {
575
+ showFindReplace = true;
576
+
577
+ // Pre-fill with selected text if any
578
+ if (editorState?.hasSelection) {
579
+ const selectedText = editorState.getSelectedText();
580
+ // Only use selection if it's a single line
581
+ if (!selectedText.includes('\n')) {
582
+ editorFind.setQuery(selectedText);
583
+ syncFindState();
584
+ }
585
+ }
586
+
587
+ tick().then(() => {
588
+ findReplaceComponent?.focus();
589
+ });
590
+ }
591
+
592
+ function closeFind() {
593
+ showFindReplace = false;
594
+ hiddenInput?.focus();
595
+ }
596
+
597
+ function handleQueryChange(query: string) {
598
+ editorFind.setQuery(query);
599
+ syncFindState();
600
+ }
601
+
602
+ function handleFindNext() {
603
+ editorFind.findNext();
604
+ syncFindState();
605
+ }
606
+
607
+ function handleFindPrev() {
608
+ editorFind.findPrev();
609
+ syncFindState();
610
+ }
611
+
612
+ function handleReplace() {
613
+ editorFind.replace();
614
+ syncFindState();
615
+ }
616
+
617
+ function handleReplaceAll() {
618
+ editorFind.replaceAll();
619
+ syncFindState();
620
+ }
621
+
622
+ // Memoized match highlight rectangles (only for visible matches)
623
+ let matchRects = $derived.by(() => {
624
+ if (searchMatches.length === 0) return [];
625
+
626
+ const viewportHeight = untrack(() => editorContent?.clientHeight ?? FALLBACK_VIEWPORT_HEIGHT);
627
+
628
+ return editorFind.computeMatchRects(
629
+ { scrollTop, viewportHeight },
630
+ { lineHeight, charWidth, gutterWidth, contentPadding: CONTENT_PADDING }
631
+ );
632
+ });
633
+
634
+
635
+ // Watch for content prop changes (external changes only, not user typing)
636
+ $effect(() => {
637
+ // Only react to a GENUINE external change to the `content` prop (e.g. a file
638
+ // switch or a programmatic set), never to an echo of our own emission.
639
+ //
640
+ // An echo happens when the parent feeds our onChange / bind:content value back
641
+ // into the `content` prop. We detect it three ways:
642
+ // 1. content === previousContentProp — prop hasn't actually changed.
643
+ // 2. content === lastEmittedContent — it's exactly what we just emitted.
644
+ // 3. content === editorState.getContent() — the editor already holds this text.
645
+ // Any of these means calling setContent would needlessly rebuild the document,
646
+ // wiping undo/redo history and collapsing multi-cursor selections.
647
+ if (
648
+ editorState &&
649
+ content !== previousContentProp &&
650
+ content !== lastEmittedContent &&
651
+ content !== untrack(() => editorState.getContent())
652
+ ) {
653
+ // Preserve scroll position when external content changes
654
+ const savedScrollTop = untrack(() => editorContent?.scrollTop ?? 0);
655
+ const savedScrollLeft = untrack(() => editorContent?.scrollLeft ?? 0);
656
+
657
+ editorState.setContent(content);
658
+ previousContentProp = content; // Update the tracked prop value
659
+ lastEmittedContent = content; // Keep echo-tracking in sync after external set
660
+
661
+ // Restore scroll position after content update (clamped to new content bounds)
662
+ const contentEl = untrack(() => editorContent);
663
+ if (contentEl) {
664
+ // Use requestAnimationFrame to ensure DOM has updated
665
+ const rafId = requestAnimationFrame(() => {
666
+ if (contentEl) {
667
+ contentEl.scrollTop = Math.min(savedScrollTop, contentEl.scrollHeight - contentEl.clientHeight);
668
+ contentEl.scrollLeft = Math.min(savedScrollLeft, contentEl.scrollWidth - contentEl.clientWidth);
669
+ }
670
+ });
671
+ // Cleanup: cancel pending RAF if effect re-runs
672
+ return () => cancelAnimationFrame(rafId);
673
+ }
674
+ }
675
+ });
676
+
677
+ // Watch for language changes
678
+ $effect(() => {
679
+ if (editorState && language !== editorState.language) {
680
+ editorState.setLanguage(language);
681
+ }
682
+ });
683
+
684
+ // Watch for selection changes to scroll into view
685
+ $effect(() => {
686
+ if (selection) {
687
+ // Use untrack for the scroll function to prevent unnecessary re-runs
688
+ untrack(() => scrollCursorIntoView());
689
+ }
690
+ });
691
+
692
+
693
+ // Ensure cursor is on a visible line after fold changes
694
+ $effect(() => {
695
+ if (folding && editorState && foldManager.isLineHidden(selection.head.line)) {
696
+ // Find the fold region containing the cursor and move to its start
697
+ for (const region of foldRegions) {
698
+ if (region.collapsed &&
699
+ selection.head.line > region.startLine &&
700
+ selection.head.line <= region.endLine) {
701
+ editorState.setCursor({ line: region.startLine, column: 0 });
702
+ break;
703
+ }
704
+ }
705
+ }
706
+ });
707
+
708
+ // ResizeObserver for container size changes
709
+ let resizeObserver: ResizeObserver | null = null;
710
+
711
+ // Cleanup for semantic fold commands
712
+ let unregisterSemanticFoldCommands: (() => void) | null = null;
713
+
714
+ onMount(() => {
715
+ // SSR guard - ensure we're in browser environment
716
+ if (typeof window === 'undefined') return;
717
+
718
+ initEditor();
719
+ inputHandlers.measureCharacter();
720
+ resetCursorBlink();
721
+
722
+ // Measure the initial viewport height for line virtualization.
723
+ if (editorContent) {
724
+ viewportHeight = editorContent.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
725
+ }
726
+
727
+ // Register semantic fold commands for the command palette
728
+ unregisterSemanticFoldCommands = registerSemanticFoldCommands(editorState, {
729
+ onFoldLines: (startLine, _endLine) => {
730
+ // FoldManager.collapse expects the start line of a fold region
731
+ foldManager?.collapse(startLine);
732
+ },
733
+ onUnfoldLines: (startLine, _endLine) => {
734
+ foldManager?.expand(startLine);
735
+ },
736
+ onFoldAll: () => {
737
+ foldManager?.collapseAll();
738
+ },
739
+ onUnfoldAll: () => {
740
+ foldManager?.expandAll();
741
+ },
742
+ onApplyPreset: (preset) => {
743
+ // First expand all
744
+ foldManager?.expandAll();
745
+ // Then collapse categories that should be hidden
746
+ const analyzer = getSemanticAnalyzer();
747
+ const regions = analyzer.analyze(editorState.lines, language);
748
+ for (const category of preset.hide) {
749
+ const categoryRegions = analyzer.getByCategory(regions, category);
750
+ for (const region of categoryRegions) {
751
+ foldManager?.collapse(region.startLine);
752
+ }
753
+ }
754
+ },
755
+ getLanguage: () => language
756
+ });
757
+
758
+ // Note: Global mouse events are now attached/detached per-drag in handleMouseDown/handleMouseUp
759
+ // This prevents conflicts between multiple editor instances
760
+
761
+ // Watch for container resize to recalculate character measurements and the
762
+ // content viewport height (the latter drives line virtualization).
763
+ resizeObserver = new ResizeObserver(() => {
764
+ inputHandlers.measureCharacter();
765
+ if (editorContent) {
766
+ viewportHeight = editorContent.clientHeight || FALLBACK_VIEWPORT_HEIGHT;
767
+ }
768
+ });
769
+ if (container) {
770
+ resizeObserver.observe(container);
771
+ }
772
+
773
+ return () => {
774
+ // Clean up resize observer
775
+ resizeObserver?.disconnect();
776
+ resizeObserver = null;
777
+
778
+ // Clean up any active global event listeners
779
+ inputHandlers.cleanupGlobalListeners();
780
+
781
+ // Clean up cursor blink interval
782
+ if (cursorBlinkInterval) {
783
+ clearInterval(cursorBlinkInterval);
784
+ }
785
+
786
+ // Clean up editor state subscriptions
787
+ unsubscribeContent?.();
788
+ unsubscribeSelection?.();
789
+ unsubscribeCursors?.();
790
+ unsubscribeFold?.();
791
+ unsubscribeContent = null;
792
+ unsubscribeSelection = null;
793
+ unsubscribeCursors = null;
794
+ unsubscribeFold = null;
795
+
796
+ // Clean up debounce timeout
797
+ if (onChangeTimeout) {
798
+ clearTimeout(onChangeTimeout);
799
+ onChangeTimeout = null;
800
+ }
801
+
802
+ // Clean up fold update timeout
803
+ if (foldUpdateTimeout) {
804
+ clearTimeout(foldUpdateTimeout);
805
+ foldUpdateTimeout = null;
806
+ }
807
+
808
+ // Clean up complexity update timeout
809
+ if (complexityUpdateTimeout) {
810
+ clearTimeout(complexityUpdateTimeout);
811
+ complexityUpdateTimeout = null;
812
+ }
813
+
814
+ // Clean up semantic fold command registration
815
+ unregisterSemanticFoldCommands?.();
816
+ unregisterSemanticFoldCommands = null;
817
+ };
818
+ });
819
+ </script>
820
+
821
+ <div
822
+ bind:this={container}
823
+ class="custom-editor {className}"
824
+ style="
825
+ --editor-font-size: {mergedPrefs.fontSize}px;
826
+ --editor-font-family: {mergedPrefs.fontFamily};
827
+ --editor-line-height: {lineHeight}px;
828
+ --editor-char-width: {charWidth}px;
829
+ --editor-gutter-width: {gutterWidth}px;
830
+ "
831
+ >
832
+ <!-- Find/Replace bar -->
833
+ {#if showFindReplace}
834
+ <FindReplace
835
+ bind:this={findReplaceComponent}
836
+ query={searchQuery}
837
+ replaceText={replaceTextState}
838
+ caseSensitive={searchCaseSensitive}
839
+ useRegex={searchUseRegex}
840
+ matchCount={searchMatches.length}
841
+ currentMatch={currentMatchIndex >= 0 ? currentMatchIndex + 1 : 0}
842
+ onQueryChange={handleQueryChange}
843
+ onReplaceTextChange={(text) => { editorFind.setReplaceText(text); syncFindState(); }}
844
+ onCaseSensitiveChange={(value) => { editorFind.setCaseSensitive(value); syncFindState(); }}
845
+ onUseRegexChange={(value) => { editorFind.setUseRegex(value); syncFindState(); }}
846
+ onFindNext={handleFindNext}
847
+ onFindPrev={handleFindPrev}
848
+ onReplace={handleReplace}
849
+ onReplaceAll={handleReplaceAll}
850
+ onClose={closeFind}
851
+ />
852
+ {/if}
853
+
854
+ <!-- Hidden input for capturing keyboard events -->
855
+ <textarea
856
+ bind:this={hiddenInput}
857
+ class="custom-editor__input"
858
+ autocomplete="off"
859
+ spellcheck={false}
860
+ tabindex={0}
861
+ onkeydown={inputHandlers.handleKeyDown}
862
+ oninput={inputHandlers.handleInput}
863
+ onpaste={inputHandlers.handlePaste}
864
+ oncopy={inputHandlers.handleCopy}
865
+ oncut={inputHandlers.handleCut}
866
+ onfocus={inputHandlers.handleFocus}
867
+ onblur={inputHandlers.handleBlur}
868
+ oncompositionstart={inputHandlers.handleCompositionStart}
869
+ oncompositionend={inputHandlers.handleCompositionEnd}
870
+ ></textarea>
871
+
872
+ <!-- Measure span for character dimensions -->
873
+ <span bind:this={measureSpan} class="custom-editor__measure">M</span>
874
+
875
+ <!-- Screen reader status (announces cursor position changes) -->
876
+ <div class="custom-editor__sr-status" aria-live="polite" aria-atomic="true">
877
+ {readonly ? 'Read-only. ' : ''}Line {selection.head.line + 1}, Column {selection.head.column + 1}{hasSelection ? `, ${Math.abs(selection.head.line - selection.anchor.line) + 1} lines selected` : ''}{hasMultipleCursors ? `, ${cursors.length} cursors active` : ''}{searchMatches.length > 0 ? `, ${searchMatches.length} search matches, match ${currentMatchIndex + 1} of ${searchMatches.length}` : ''}
878
+ </div>
879
+
880
+ <!-- Dynamic screen reader announcements for actions -->
881
+ <div class="custom-editor__sr-status" aria-live="assertive" aria-atomic="true">
882
+ {srAnnouncement}
883
+ </div>
884
+
885
+ <!-- Hidden keyboard shortcuts help for screen readers -->
886
+ <div id="editor-help" class="custom-editor__sr-status">
887
+ Keyboard shortcuts: Ctrl+F to find, Ctrl+H to replace, Ctrl+D to add cursor at next occurrence, Ctrl+Shift+L to select all occurrences, Escape to clear.{folding ? ' Ctrl+Shift+[ to fold, Ctrl+Shift+] to unfold.' : ''}
888
+ </div>
889
+
890
+ <!-- Main editor content -->
891
+ <div
892
+ bind:this={editorContent}
893
+ class="custom-editor__content"
894
+ class:custom-editor__content--readonly={readonly}
895
+ onmousedown={inputHandlers.handleMouseDown}
896
+ onscroll={inputHandlers.handleScroll}
897
+ role="textbox"
898
+ aria-multiline="true"
899
+ aria-readonly={readonly}
900
+ aria-label={readonly ? 'Read-only code editor' : 'Code editor'}
901
+ aria-describedby="editor-help"
902
+ tabindex={0}
903
+ >
904
+ <!-- Complexity highlighting layer (bottommost) -->
905
+ {#if complexityHighlighting && complexityMetrics}
906
+ <ComplexityLayer
907
+ metrics={complexityMetrics}
908
+ lineHeight={lineHeight}
909
+ gutterWidth={gutterWidth}
910
+ minScore={complexityThreshold}
911
+ enabled={complexityHighlighting}
912
+ />
913
+ {/if}
914
+
915
+ <!-- AI Focus Layer (Ghost Pair visualization) -->
916
+ {#if aiAgents.length > 0}
917
+ <AIFocusLayer
918
+ agents={aiAgents}
919
+ lineHeight={lineHeight}
920
+ charWidth={charWidth}
921
+ gutterWidth={gutterWidth}
922
+ contentPadding={CONTENT_PADDING}
923
+ showLabels={showAILabels}
924
+ showFocusRegions={showAIFocusRegions}
925
+ enabled={true}
926
+ />
927
+ {/if}
928
+
929
+ <!-- Gutter background (extends full height of content, matching the virtualized spacer) -->
930
+ {#if mergedPrefs.lineNumbers !== 'off'}
931
+ <div class="custom-editor__gutter-bg" style="width: {gutterWidth}px; height: {totalContentHeight}px;"></div>
932
+ {/if}
933
+
934
+ <!-- Search match highlights (below selection) -->
935
+ {#if showFindReplace && searchMatches.length > 0}
936
+ <div class="custom-editor__matches">
937
+ {#each matchRects as rect}
938
+ <div
939
+ class="custom-editor__match"
940
+ class:custom-editor__match--current={rect.isCurrent}
941
+ style="top: {rect.top}px; left: {rect.left}px; width: {rect.width}px; height: {rect.height}px;"
942
+ ></div>
943
+ {/each}
944
+ </div>
945
+ {/if}
946
+
947
+ <!-- Selection layer + cursors -->
948
+ <EditorSelections
949
+ {cursors}
950
+ {cursorVisible}
951
+ readonly={readonly}
952
+ {lineHeight}
953
+ {charWidth}
954
+ {gutterWidth}
955
+ contentPadding={CONTENT_PADDING}
956
+ {scrollTop}
957
+ {viewportHeight}
958
+ getLine={(n) => editorState.getLine(n)}
959
+ lineCount={editorState?.lineCount ?? 0}
960
+ />
961
+
962
+ <!-- Lines (virtualized: only the windowed slice is rendered) -->
963
+ <EditorLines
964
+ windowedLines={windowedLines}
965
+ totalHeight={totalContentHeight}
966
+ {lineHeight}
967
+ activeLine={selection.head.line}
968
+ highlightActiveLine={mergedPrefs.highlightActiveLine}
969
+ lineNumbers={mergedPrefs.lineNumbers}
970
+ {gutterWidth}
971
+ tabSize={mergedPrefs.tabSize}
972
+ {folding}
973
+ {hasFoldIndicator}
974
+ {isFoldCollapsed}
975
+ {getHiddenLineCount}
976
+ onFoldIndicatorClick={handleFoldIndicatorClick}
977
+ onExpandFold={(index) => foldManager.expand(index)}
978
+ />
979
+
980
+ </div>
981
+ </div>
982
+
983
+ <!-- Command Palette -->
984
+ <CommandPalette
985
+ bind:open={showCommandPalette}
986
+ onClose={() => editorContent?.focus()}
987
+ />
988
+
989
+ <style>
990
+ .custom-editor {
991
+ position: relative;
992
+ width: 100%;
993
+ height: 100%;
994
+ overflow: hidden;
995
+ background: var(--ide-bg-primary);
996
+ font-family: var(--editor-font-family), var(--ide-font-mono);
997
+ font-size: var(--editor-font-size);
998
+ }
999
+
1000
+ .custom-editor__input {
1001
+ position: absolute;
1002
+ top: 0;
1003
+ left: 0;
1004
+ width: 1px;
1005
+ height: 1px;
1006
+ padding: 0;
1007
+ margin: 0;
1008
+ border: none;
1009
+ outline: none;
1010
+ background: transparent;
1011
+ color: transparent;
1012
+ resize: none;
1013
+ overflow: hidden;
1014
+ white-space: pre;
1015
+ z-index: -1;
1016
+ opacity: 0;
1017
+ }
1018
+
1019
+ .custom-editor__measure {
1020
+ position: absolute;
1021
+ visibility: hidden;
1022
+ white-space: pre;
1023
+ font-family: var(--editor-font-family), var(--ide-font-mono);
1024
+ font-size: var(--editor-font-size);
1025
+ line-height: 1.4;
1026
+ }
1027
+
1028
+ /* Screen reader only - visually hidden but accessible */
1029
+ .custom-editor__sr-status {
1030
+ position: absolute;
1031
+ width: 1px;
1032
+ height: 1px;
1033
+ padding: 0;
1034
+ margin: -1px;
1035
+ overflow: hidden;
1036
+ clip: rect(0, 0, 0, 0);
1037
+ white-space: nowrap;
1038
+ border: 0;
1039
+ }
1040
+
1041
+ .custom-editor__content {
1042
+ position: relative;
1043
+ width: 100%;
1044
+ height: 100%;
1045
+ overflow: auto;
1046
+ cursor: default;
1047
+ }
1048
+
1049
+ .custom-editor__content--readonly {
1050
+ cursor: default;
1051
+ }
1052
+
1053
+ .custom-editor__matches {
1054
+ position: absolute;
1055
+ top: 0;
1056
+ left: 0;
1057
+ right: 0;
1058
+ pointer-events: none;
1059
+ z-index: 0;
1060
+ }
1061
+
1062
+ .custom-editor__match {
1063
+ position: absolute;
1064
+ background: rgba(255, 213, 0, 0.25);
1065
+ border: 1px solid rgba(255, 213, 0, 0.5);
1066
+ border-radius: 2px;
1067
+ box-sizing: border-box;
1068
+ }
1069
+
1070
+ .custom-editor__match--current {
1071
+ background: rgba(255, 213, 0, 0.45);
1072
+ border-color: rgba(255, 213, 0, 0.8);
1073
+ }
1074
+
1075
+
1076
+
1077
+ /* Gutter background - extends full height of content */
1078
+ .custom-editor__gutter-bg {
1079
+ position: absolute;
1080
+ top: 0;
1081
+ left: 0;
1082
+ min-height: 100%;
1083
+ background: var(--ide-bg-secondary);
1084
+ border-right: 1px solid var(--ide-border);
1085
+ z-index: 1;
1086
+ pointer-events: none;
1087
+ }
1088
+
1089
+
1090
+ /* Token styles */
1091
+ :global(.token-comment),
1092
+ :global(.token-comment-line),
1093
+ :global(.token-comment-block),
1094
+ :global(.token-comment-doc) {
1095
+ color: var(--ide-text-muted);
1096
+ font-style: italic;
1097
+ }
1098
+
1099
+ :global(.token-string),
1100
+ :global(.token-string-template) {
1101
+ color: var(--color-nocturnium-aurora-green);
1102
+ }
1103
+
1104
+ :global(.token-string-regex) {
1105
+ color: var(--color-nocturnium-aurora-pink);
1106
+ }
1107
+
1108
+ :global(.token-string-escape) {
1109
+ color: var(--color-nocturnium-aurora-yellow);
1110
+ }
1111
+
1112
+ :global(.token-number),
1113
+ :global(.token-number-integer),
1114
+ :global(.token-number-float),
1115
+ :global(.token-number-hex),
1116
+ :global(.token-number-binary) {
1117
+ color: var(--color-nocturnium-aurora-yellow);
1118
+ }
1119
+
1120
+ :global(.token-keyword),
1121
+ :global(.token-keyword-control),
1122
+ :global(.token-keyword-operator),
1123
+ :global(.token-keyword-definition),
1124
+ :global(.token-keyword-module),
1125
+ :global(.token-keyword-storage) {
1126
+ color: var(--color-nocturnium-aurora-purple);
1127
+ }
1128
+
1129
+ :global(.token-operator),
1130
+ :global(.token-operator-arithmetic),
1131
+ :global(.token-operator-comparison),
1132
+ :global(.token-operator-logical),
1133
+ :global(.token-operator-assignment) {
1134
+ color: var(--ide-text-primary);
1135
+ }
1136
+
1137
+ :global(.token-variable) {
1138
+ color: var(--ide-text-primary);
1139
+ }
1140
+
1141
+ :global(.token-variable-definition) {
1142
+ color: var(--color-nocturnium-wave);
1143
+ }
1144
+
1145
+ :global(.token-variable-parameter) {
1146
+ color: color-mix(in srgb, var(--color-nocturnium-wave) 80%, var(--ide-text-muted));
1147
+ }
1148
+
1149
+ :global(.token-function),
1150
+ :global(.token-function-definition),
1151
+ :global(.token-function-call) {
1152
+ color: var(--color-nocturnium-aurora-blue);
1153
+ }
1154
+
1155
+ :global(.token-property),
1156
+ :global(.token-property-definition) {
1157
+ color: var(--color-nocturnium-wave);
1158
+ }
1159
+
1160
+ :global(.token-type),
1161
+ :global(.token-type-class),
1162
+ :global(.token-type-interface),
1163
+ :global(.token-type-namespace),
1164
+ :global(.token-type-builtin) {
1165
+ color: var(--color-nocturnium-teal);
1166
+ }
1167
+
1168
+ :global(.token-constant),
1169
+ :global(.token-constant-boolean),
1170
+ :global(.token-constant-null),
1171
+ :global(.token-constant-builtin) {
1172
+ color: var(--color-nocturnium-aurora-yellow);
1173
+ }
1174
+
1175
+ :global(.token-punctuation),
1176
+ :global(.token-punctuation-bracket),
1177
+ :global(.token-punctuation-brace),
1178
+ :global(.token-punctuation-paren),
1179
+ :global(.token-punctuation-separator),
1180
+ :global(.token-punctuation-accessor) {
1181
+ color: var(--ide-text-secondary);
1182
+ }
1183
+
1184
+ /* Svelte template braces - make them stand out */
1185
+ :global(.token-punctuation-brace) {
1186
+ color: var(--color-nocturnium-aurora-yellow);
1187
+ font-weight: 600;
1188
+ }
1189
+
1190
+ :global(.token-tag),
1191
+ :global(.token-tag-name) {
1192
+ color: var(--color-nocturnium-aurora-pink);
1193
+ }
1194
+
1195
+ :global(.token-tag-attribute) {
1196
+ color: var(--color-nocturnium-aurora-yellow);
1197
+ }
1198
+
1199
+ :global(.token-tag-attribute-value) {
1200
+ color: var(--color-nocturnium-aurora-green);
1201
+ }
1202
+
1203
+ :global(.token-tag-punctuation) {
1204
+ color: var(--ide-text-secondary);
1205
+ }
1206
+
1207
+ :global(.token-markup-heading) {
1208
+ color: var(--color-nocturnium-aurora-blue);
1209
+ font-weight: bold;
1210
+ }
1211
+
1212
+ :global(.token-markup-bold) {
1213
+ font-weight: bold;
1214
+ }
1215
+
1216
+ :global(.token-markup-italic) {
1217
+ font-style: italic;
1218
+ }
1219
+
1220
+ :global(.token-markup-link) {
1221
+ color: var(--color-nocturnium-wave);
1222
+ text-decoration: underline;
1223
+ }
1224
+
1225
+ :global(.token-markup-code) {
1226
+ color: var(--color-nocturnium-aurora-green);
1227
+ background: var(--ide-bg-tertiary);
1228
+ }
1229
+
1230
+ :global(.token-markup-quote) {
1231
+ color: var(--ide-text-secondary);
1232
+ font-style: italic;
1233
+ }
1234
+
1235
+ :global(.token-markup-list) {
1236
+ color: var(--color-nocturnium-ember);
1237
+ }
1238
+
1239
+ :global(.token-invalid) {
1240
+ color: var(--ide-error);
1241
+ }
1242
+ </style>