@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.
- package/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/components/agents/AgentActivityPanel.svelte +565 -0
- package/dist/components/agents/AgentActivityPanel.svelte.d.ts +24 -0
- package/dist/components/agents/AgentAvatar.svelte +417 -0
- package/dist/components/agents/AgentAvatar.svelte.d.ts +23 -0
- package/dist/components/agents/AgentCursor.svelte +224 -0
- package/dist/components/agents/AgentCursor.svelte.d.ts +35 -0
- package/dist/components/agents/AgentPresenceBar.svelte +261 -0
- package/dist/components/agents/AgentPresenceBar.svelte.d.ts +20 -0
- package/dist/components/agents/index.d.ts +4 -0
- package/dist/components/agents/index.js +5 -0
- package/dist/components/ai/AIConversationList.svelte +524 -0
- package/dist/components/ai/AIConversationList.svelte.d.ts +17 -0
- package/dist/components/ai/AIEditPreview.svelte +132 -0
- package/dist/components/ai/AIEditPreview.svelte.d.ts +8 -0
- package/dist/components/ai/AIInlineEdit.svelte +155 -0
- package/dist/components/ai/AIInlineEdit.svelte.d.ts +10 -0
- package/dist/components/ai/AIMessage.svelte +239 -0
- package/dist/components/ai/AIMessage.svelte.d.ts +13 -0
- package/dist/components/ai/AIMessageActions.svelte +176 -0
- package/dist/components/ai/AIMessageActions.svelte.d.ts +12 -0
- package/dist/components/ai/AIMessageContent.svelte +355 -0
- package/dist/components/ai/AIMessageContent.svelte.d.ts +7 -0
- package/dist/components/ai/AIPanel.svelte +561 -0
- package/dist/components/ai/AIPanel.svelte.d.ts +7 -0
- package/dist/components/ai/AISuggestionWidget.svelte +132 -0
- package/dist/components/ai/AISuggestionWidget.svelte.d.ts +10 -0
- package/dist/components/ai/AIToolCallDisplay.svelte +317 -0
- package/dist/components/ai/AIToolCallDisplay.svelte.d.ts +12 -0
- package/dist/components/ai/index.d.ts +9 -0
- package/dist/components/ai/index.js +10 -0
- package/dist/components/core/Avatar.svelte +110 -0
- package/dist/components/core/Avatar.svelte.d.ts +12 -0
- package/dist/components/core/Badge.svelte +98 -0
- package/dist/components/core/Badge.svelte.d.ts +11 -0
- package/dist/components/core/Button.svelte +175 -0
- package/dist/components/core/Button.svelte.d.ts +18 -0
- package/dist/components/core/ConnectionStatus.svelte +294 -0
- package/dist/components/core/ConnectionStatus.svelte.d.ts +20 -0
- package/dist/components/core/ContextMenu.svelte +176 -0
- package/dist/components/core/ContextMenu.svelte.d.ts +19 -0
- package/dist/components/core/ErrorBoundary.svelte +277 -0
- package/dist/components/core/ErrorBoundary.svelte.d.ts +23 -0
- package/dist/components/core/Icon.svelte +107 -0
- package/dist/components/core/Icon.svelte.d.ts +8 -0
- package/dist/components/core/Input.svelte +138 -0
- package/dist/components/core/Input.svelte.d.ts +20 -0
- package/dist/components/core/Kbd.svelte +34 -0
- package/dist/components/core/Kbd.svelte.d.ts +7 -0
- package/dist/components/core/ResizeHandle.svelte +200 -0
- package/dist/components/core/ResizeHandle.svelte.d.ts +23 -0
- package/dist/components/core/Spinner.svelte +35 -0
- package/dist/components/core/Spinner.svelte.d.ts +7 -0
- package/dist/components/core/Textarea.svelte +112 -0
- package/dist/components/core/Textarea.svelte.d.ts +18 -0
- package/dist/components/core/Tooltip.svelte +103 -0
- package/dist/components/core/Tooltip.svelte.d.ts +11 -0
- package/dist/components/core/index.d.ts +13 -0
- package/dist/components/core/index.js +14 -0
- package/dist/components/editor/AIFocusLayer.svelte +430 -0
- package/dist/components/editor/AIFocusLayer.svelte.d.ts +32 -0
- package/dist/components/editor/Breadcrumbs.svelte +435 -0
- package/dist/components/editor/Breadcrumbs.svelte.d.ts +33 -0
- package/dist/components/editor/BreakpointLayer.svelte +642 -0
- package/dist/components/editor/BreakpointLayer.svelte.d.ts +20 -0
- package/dist/components/editor/CognitiveLoadMeter.svelte +324 -0
- package/dist/components/editor/CognitiveLoadMeter.svelte.d.ts +18 -0
- package/dist/components/editor/CollaborativeEditor.svelte +218 -0
- package/dist/components/editor/CollaborativeEditor.svelte.d.ts +32 -0
- package/dist/components/editor/CommandPalette.svelte +434 -0
- package/dist/components/editor/CommandPalette.svelte.d.ts +11 -0
- package/dist/components/editor/ComplexityLayer.svelte +293 -0
- package/dist/components/editor/ComplexityLayer.svelte.d.ts +23 -0
- package/dist/components/editor/ConflictZoneLayer.svelte +441 -0
- package/dist/components/editor/ConflictZoneLayer.svelte.d.ts +25 -0
- package/dist/components/editor/ContextLens.svelte +262 -0
- package/dist/components/editor/ContextLens.svelte.d.ts +27 -0
- package/dist/components/editor/CustomEditor.svelte +1242 -0
- package/dist/components/editor/CustomEditor.svelte.d.ts +37 -0
- package/dist/components/editor/DebugConsole.svelte +646 -0
- package/dist/components/editor/DebugConsole.svelte.d.ts +41 -0
- package/dist/components/editor/EchoCursorLayer.svelte +363 -0
- package/dist/components/editor/EchoCursorLayer.svelte.d.ts +24 -0
- package/dist/components/editor/Editor.svelte +61 -0
- package/dist/components/editor/Editor.svelte.d.ts +22 -0
- package/dist/components/editor/EditorGutter.svelte +119 -0
- package/dist/components/editor/EditorGutter.svelte.d.ts +19 -0
- package/dist/components/editor/EditorLines.svelte +182 -0
- package/dist/components/editor/EditorLines.svelte.d.ts +43 -0
- package/dist/components/editor/EditorPane.svelte +134 -0
- package/dist/components/editor/EditorPane.svelte.d.ts +9 -0
- package/dist/components/editor/EditorSelections.svelte +186 -0
- package/dist/components/editor/EditorSelections.svelte.d.ts +25 -0
- package/dist/components/editor/EditorTabs.svelte +170 -0
- package/dist/components/editor/EditorTabs.svelte.d.ts +12 -0
- package/dist/components/editor/FileExplorer.svelte +811 -0
- package/dist/components/editor/FileExplorer.svelte.d.ts +67 -0
- package/dist/components/editor/FileIcon.svelte +110 -0
- package/dist/components/editor/FileIcon.svelte.d.ts +10 -0
- package/dist/components/editor/FindReplace.svelte +448 -0
- package/dist/components/editor/FindReplace.svelte.d.ts +40 -0
- package/dist/components/editor/GhostBracketLayer.svelte +391 -0
- package/dist/components/editor/GhostBracketLayer.svelte.d.ts +24 -0
- package/dist/components/editor/GitBlameLayer.svelte +436 -0
- package/dist/components/editor/GitBlameLayer.svelte.d.ts +18 -0
- package/dist/components/editor/InlineDiagnosticsLayer.svelte +540 -0
- package/dist/components/editor/InlineDiagnosticsLayer.svelte.d.ts +35 -0
- package/dist/components/editor/InlineDiffLayer.svelte +337 -0
- package/dist/components/editor/InlineDiffLayer.svelte.d.ts +31 -0
- package/dist/components/editor/MinimalEditor.svelte +75 -0
- package/dist/components/editor/MinimalEditor.svelte.d.ts +6 -0
- package/dist/components/editor/MinimalEditor2.svelte +84 -0
- package/dist/components/editor/MinimalEditor2.svelte.d.ts +6 -0
- package/dist/components/editor/Minimap.svelte +327 -0
- package/dist/components/editor/Minimap.svelte.d.ts +34 -0
- package/dist/components/editor/PluginPreviewSandbox.svelte +793 -0
- package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +49 -0
- package/dist/components/editor/ProblemsPanel.svelte +628 -0
- package/dist/components/editor/ProblemsPanel.svelte.d.ts +25 -0
- package/dist/components/editor/QuickActionsMenu.svelte +403 -0
- package/dist/components/editor/QuickActionsMenu.svelte.d.ts +18 -0
- package/dist/components/editor/SnippetPalette.svelte +530 -0
- package/dist/components/editor/SnippetPalette.svelte.d.ts +16 -0
- package/dist/components/editor/StructureMap.svelte +431 -0
- package/dist/components/editor/StructureMap.svelte.d.ts +37 -0
- package/dist/components/editor/SymbolOutline.svelte +722 -0
- package/dist/components/editor/SymbolOutline.svelte.d.ts +44 -0
- package/dist/components/editor/TimelineScrubber.svelte +470 -0
- package/dist/components/editor/TimelineScrubber.svelte.d.ts +40 -0
- package/dist/components/editor/TokenRenderer.svelte +69 -0
- package/dist/components/editor/TokenRenderer.svelte.d.ts +15 -0
- package/dist/components/editor/constants.d.ts +32 -0
- package/dist/components/editor/constants.js +36 -0
- package/dist/components/editor/core/ai-awareness.d.ts +176 -0
- package/dist/components/editor/core/ai-awareness.js +210 -0
- package/dist/components/editor/core/bracket-healer.d.ts +189 -0
- package/dist/components/editor/core/bracket-healer.js +406 -0
- package/dist/components/editor/core/breakpoints.d.ts +203 -0
- package/dist/components/editor/core/breakpoints.js +414 -0
- package/dist/components/editor/core/commands.d.ts +108 -0
- package/dist/components/editor/core/commands.js +246 -0
- package/dist/components/editor/core/complexity-analyzer.d.ts +123 -0
- package/dist/components/editor/core/complexity-analyzer.js +376 -0
- package/dist/components/editor/core/conflict-predictor.d.ts +135 -0
- package/dist/components/editor/core/conflict-predictor.js +316 -0
- package/dist/components/editor/core/crdt-binding.d.ts +118 -0
- package/dist/components/editor/core/crdt-binding.js +286 -0
- package/dist/components/editor/core/diagnostics.d.ts +210 -0
- package/dist/components/editor/core/diagnostics.js +335 -0
- package/dist/components/editor/core/echo-cursor.d.ts +201 -0
- package/dist/components/editor/core/echo-cursor.js +267 -0
- package/dist/components/editor/core/folding.d.ts +124 -0
- package/dist/components/editor/core/folding.js +672 -0
- package/dist/components/editor/core/ghost-pair.d.ts +122 -0
- package/dist/components/editor/core/ghost-pair.js +221 -0
- package/dist/components/editor/core/git-blame.d.ts +170 -0
- package/dist/components/editor/core/git-blame.js +324 -0
- package/dist/components/editor/core/index.d.ts +26 -0
- package/dist/components/editor/core/index.js +24 -0
- package/dist/components/editor/core/keybindings.d.ts +79 -0
- package/dist/components/editor/core/keybindings.js +357 -0
- package/dist/components/editor/core/multi-cursor.d.ts +196 -0
- package/dist/components/editor/core/multi-cursor.js +521 -0
- package/dist/components/editor/core/navigation.d.ts +107 -0
- package/dist/components/editor/core/navigation.js +408 -0
- package/dist/components/editor/core/quick-actions.d.ts +189 -0
- package/dist/components/editor/core/quick-actions.js +427 -0
- package/dist/components/editor/core/search.d.ts +88 -0
- package/dist/components/editor/core/search.js +192 -0
- package/dist/components/editor/core/semantic-analyzer.d.ts +77 -0
- package/dist/components/editor/core/semantic-analyzer.js +424 -0
- package/dist/components/editor/core/snippet-manager.d.ts +202 -0
- package/dist/components/editor/core/snippet-manager.js +565 -0
- package/dist/components/editor/core/state.d.ts +367 -0
- package/dist/components/editor/core/state.js +900 -0
- package/dist/components/editor/core/timeline.d.ts +204 -0
- package/dist/components/editor/core/timeline.js +349 -0
- package/dist/components/editor/editor-find.d.ts +56 -0
- package/dist/components/editor/editor-find.js +148 -0
- package/dist/components/editor/editor-input.d.ts +77 -0
- package/dist/components/editor/editor-input.js +445 -0
- package/dist/components/editor/editor-multicursor.d.ts +21 -0
- package/dist/components/editor/editor-multicursor.js +196 -0
- package/dist/components/editor/editor-scroll.d.ts +14 -0
- package/dist/components/editor/editor-scroll.js +34 -0
- package/dist/components/editor/index.d.ts +15 -0
- package/dist/components/editor/index.js +21 -0
- package/dist/components/editor/languages.d.ts +62 -0
- package/dist/components/editor/languages.js +285 -0
- package/dist/components/editor/theme.d.ts +88 -0
- package/dist/components/editor/theme.js +139 -0
- package/dist/components/editor/tokenizer/base.d.ts +40 -0
- package/dist/components/editor/tokenizer/base.js +203 -0
- package/dist/components/editor/tokenizer/index.d.ts +56 -0
- package/dist/components/editor/tokenizer/index.js +215 -0
- package/dist/components/editor/tokenizer/languages/css.d.ts +17 -0
- package/dist/components/editor/tokenizer/languages/css.js +194 -0
- package/dist/components/editor/tokenizer/languages/go.d.ts +17 -0
- package/dist/components/editor/tokenizer/languages/go.js +220 -0
- package/dist/components/editor/tokenizer/languages/html.d.ts +24 -0
- package/dist/components/editor/tokenizer/languages/html.js +145 -0
- package/dist/components/editor/tokenizer/languages/javascript.d.ts +56 -0
- package/dist/components/editor/tokenizer/languages/javascript.js +452 -0
- package/dist/components/editor/tokenizer/languages/json.d.ts +12 -0
- package/dist/components/editor/tokenizer/languages/json.js +91 -0
- package/dist/components/editor/tokenizer/languages/markdown.d.ts +16 -0
- package/dist/components/editor/tokenizer/languages/markdown.js +156 -0
- package/dist/components/editor/tokenizer/languages/python.d.ts +20 -0
- package/dist/components/editor/tokenizer/languages/python.js +227 -0
- package/dist/components/editor/tokenizer/languages/svelte.d.ts +40 -0
- package/dist/components/editor/tokenizer/languages/svelte.js +326 -0
- package/dist/components/editor/tokenizer/types.d.ts +86 -0
- package/dist/components/editor/tokenizer/types.js +4 -0
- package/dist/components/layout/IDELayout.svelte +274 -0
- package/dist/components/layout/IDELayout.svelte.d.ts +29 -0
- package/dist/components/layout/StatusBar.svelte +511 -0
- package/dist/components/layout/StatusBar.svelte.d.ts +47 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.js +3 -0
- package/dist/components/lsp/AutocompleteWidget.svelte +364 -0
- package/dist/components/lsp/AutocompleteWidget.svelte.d.ts +33 -0
- package/dist/components/lsp/DiagnosticMarker.svelte +166 -0
- package/dist/components/lsp/DiagnosticMarker.svelte.d.ts +19 -0
- package/dist/components/lsp/DiagnosticsPanel.svelte +388 -0
- package/dist/components/lsp/DiagnosticsPanel.svelte.d.ts +21 -0
- package/dist/components/lsp/HoverTooltip.svelte +274 -0
- package/dist/components/lsp/HoverTooltip.svelte.d.ts +24 -0
- package/dist/components/lsp/LSPEditor.svelte +486 -0
- package/dist/components/lsp/LSPEditor.svelte.d.ts +39 -0
- package/dist/components/lsp/SignatureHelpWidget.svelte +216 -0
- package/dist/components/lsp/SignatureHelpWidget.svelte.d.ts +22 -0
- package/dist/components/lsp/index.d.ts +6 -0
- package/dist/components/lsp/index.js +7 -0
- package/dist/components/plugins/PluginCard.svelte +153 -0
- package/dist/components/plugins/PluginCard.svelte.d.ts +19 -0
- package/dist/components/plugins/PluginPanel.svelte +280 -0
- package/dist/components/plugins/PluginPanel.svelte.d.ts +8 -0
- package/dist/components/plugins/PluginProposalForm.svelte +250 -0
- package/dist/components/plugins/PluginProposalForm.svelte.d.ts +6 -0
- package/dist/components/plugins/PluginStatusBadge.svelte +14 -0
- package/dist/components/plugins/PluginStatusBadge.svelte.d.ts +8 -0
- package/dist/components/plugins/index.d.ts +4 -0
- package/dist/components/plugins/index.js +5 -0
- package/dist/components/vfs/LockConflictDialog.svelte +705 -0
- package/dist/components/vfs/LockConflictDialog.svelte.d.ts +21 -0
- package/dist/components/vfs/LockIndicator.svelte +194 -0
- package/dist/components/vfs/LockIndicator.svelte.d.ts +29 -0
- package/dist/components/vfs/LockOverlay.svelte +344 -0
- package/dist/components/vfs/LockOverlay.svelte.d.ts +17 -0
- package/dist/components/vfs/VersionConflictDialog.svelte +549 -0
- package/dist/components/vfs/VersionConflictDialog.svelte.d.ts +24 -0
- package/dist/components/vfs/index.d.ts +4 -0
- package/dist/components/vfs/index.js +5 -0
- package/dist/crdt/awareness.d.ts +42 -0
- package/dist/crdt/awareness.js +109 -0
- package/dist/crdt/document.d.ts +101 -0
- package/dist/crdt/document.js +187 -0
- package/dist/crdt/index.d.ts +9 -0
- package/dist/crdt/index.js +8 -0
- package/dist/crdt/provider.d.ts +85 -0
- package/dist/crdt/provider.js +150 -0
- package/dist/crdt/types.d.ts +61 -0
- package/dist/crdt/types.js +4 -0
- package/dist/crdt/undo.d.ts +34 -0
- package/dist/crdt/undo.js +70 -0
- package/dist/index.d.ts +277 -0
- package/dist/index.js +280 -0
- package/dist/plugins/index.d.ts +103 -0
- package/dist/plugins/index.js +153 -0
- package/dist/services/error-handling.d.ts +95 -0
- package/dist/services/error-handling.js +413 -0
- package/dist/services/ide-integration.d.ts +83 -0
- package/dist/services/ide-integration.js +367 -0
- package/dist/services/lsp-client.d.ts +69 -0
- package/dist/services/lsp-client.js +667 -0
- package/dist/services/mock-ai.d.ts +37 -0
- package/dist/services/mock-ai.js +318 -0
- package/dist/services/optimistic.d.ts +141 -0
- package/dist/services/optimistic.js +367 -0
- package/dist/services/vfs-client.d.ts +81 -0
- package/dist/services/vfs-client.js +348 -0
- package/dist/stores/agents.svelte.d.ts +85 -0
- package/dist/stores/agents.svelte.js +459 -0
- package/dist/stores/ai-persistence.svelte.d.ts +76 -0
- package/dist/stores/ai-persistence.svelte.js +334 -0
- package/dist/stores/ai.svelte.d.ts +140 -0
- package/dist/stores/ai.svelte.js +383 -0
- package/dist/stores/collaboration.svelte.d.ts +164 -0
- package/dist/stores/collaboration.svelte.js +334 -0
- package/dist/stores/editor.svelte.d.ts +131 -0
- package/dist/stores/editor.svelte.js +250 -0
- package/dist/stores/index.d.ts +10 -0
- package/dist/stores/index.js +29 -0
- package/dist/stores/layout.svelte.d.ts +171 -0
- package/dist/stores/layout.svelte.js +351 -0
- package/dist/stores/plugin.svelte.d.ts +121 -0
- package/dist/stores/plugin.svelte.js +410 -0
- package/dist/stores/vfs.svelte.d.ts +123 -0
- package/dist/stores/vfs.svelte.js +680 -0
- package/dist/styles/theme.css +623 -0
- package/dist/types/agents.d.ts +127 -0
- package/dist/types/agents.js +5 -0
- package/dist/types/ai.d.ts +137 -0
- package/dist/types/ai.js +4 -0
- package/dist/types/crdt.d.ts +222 -0
- package/dist/types/crdt.js +5 -0
- package/dist/types/editor.d.ts +52 -0
- package/dist/types/editor.js +18 -0
- package/dist/types/events.d.ts +133 -0
- package/dist/types/events.js +4 -0
- package/dist/types/filesystem.d.ts +77 -0
- package/dist/types/filesystem.js +4 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.js +12 -0
- package/dist/types/lsp.d.ts +691 -0
- package/dist/types/lsp.js +108 -0
- package/dist/types/plugin.d.ts +239 -0
- package/dist/types/plugin.js +5 -0
- package/dist/types/vfs.d.ts +191 -0
- package/dist/types/vfs.js +18 -0
- package/dist/utils/format.d.ts +55 -0
- package/dist/utils/format.js +152 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.js +4 -0
- package/dist/utils/keybindings.d.ts +33 -0
- package/dist/utils/keybindings.js +171 -0
- package/dist/utils/language.d.ts +27 -0
- package/dist/utils/language.js +222 -0
- 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>
|