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