@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,672 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code folding detection and management
|
|
3
|
+
*
|
|
4
|
+
* Supports multiple folding strategies:
|
|
5
|
+
* - Indentation-based (Python, YAML)
|
|
6
|
+
* - Bracket-based (JavaScript, TypeScript, JSON, etc.)
|
|
7
|
+
* - Region markers (#region / #endregion)
|
|
8
|
+
*/
|
|
9
|
+
const DEFAULT_CONFIG = {
|
|
10
|
+
brackets: true,
|
|
11
|
+
indentation: true,
|
|
12
|
+
regions: true,
|
|
13
|
+
comments: true,
|
|
14
|
+
minLines: 2
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Bracket pairs for different languages
|
|
18
|
+
* Note: HTML/XML use tag-based folding, not simple bracket matching
|
|
19
|
+
*/
|
|
20
|
+
const BRACKET_PAIRS = {
|
|
21
|
+
default: [
|
|
22
|
+
['{', '}'],
|
|
23
|
+
['[', ']'],
|
|
24
|
+
['(', ')']
|
|
25
|
+
]
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* HTML5 void elements that don't need closing tags
|
|
29
|
+
*/
|
|
30
|
+
const HTML_VOID_ELEMENTS = new Set([
|
|
31
|
+
'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
|
|
32
|
+
'link', 'meta', 'param', 'source', 'track', 'wbr'
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Extract tag name from an opening or closing tag
|
|
36
|
+
* Returns null if not a valid tag
|
|
37
|
+
*/
|
|
38
|
+
function extractTagName(text, startPos) {
|
|
39
|
+
if (text[startPos] !== '<')
|
|
40
|
+
return null;
|
|
41
|
+
let pos = startPos + 1;
|
|
42
|
+
const isClosing = text[pos] === '/';
|
|
43
|
+
if (isClosing)
|
|
44
|
+
pos++;
|
|
45
|
+
// Skip whitespace
|
|
46
|
+
while (pos < text.length && /\s/.test(text[pos]))
|
|
47
|
+
pos++;
|
|
48
|
+
// Extract tag name (letters, digits, hyphens, underscores, colons for namespaces)
|
|
49
|
+
let name = '';
|
|
50
|
+
while (pos < text.length && /[a-zA-Z0-9\-_:]/.test(text[pos])) {
|
|
51
|
+
name += text[pos].toLowerCase();
|
|
52
|
+
pos++;
|
|
53
|
+
}
|
|
54
|
+
if (!name)
|
|
55
|
+
return null;
|
|
56
|
+
// Find the end of the tag
|
|
57
|
+
let inAttrString = false;
|
|
58
|
+
let attrStringChar = '';
|
|
59
|
+
while (pos < text.length) {
|
|
60
|
+
const char = text[pos];
|
|
61
|
+
if (!inAttrString) {
|
|
62
|
+
if (char === '"' || char === "'") {
|
|
63
|
+
inAttrString = true;
|
|
64
|
+
attrStringChar = char;
|
|
65
|
+
}
|
|
66
|
+
else if (char === '>') {
|
|
67
|
+
const isSelfClosing = text[pos - 1] === '/' || HTML_VOID_ELEMENTS.has(name);
|
|
68
|
+
return { name, isClosing, endPos: pos, isSelfClosing };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else if (char === attrStringChar) {
|
|
72
|
+
// Check for escaped quotes
|
|
73
|
+
let backslashes = 0;
|
|
74
|
+
let checkPos = pos - 1;
|
|
75
|
+
while (checkPos >= 0 && text[checkPos] === '\\') {
|
|
76
|
+
backslashes++;
|
|
77
|
+
checkPos--;
|
|
78
|
+
}
|
|
79
|
+
if (backslashes % 2 === 0) {
|
|
80
|
+
inAttrString = false;
|
|
81
|
+
attrStringChar = '';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
pos++;
|
|
85
|
+
}
|
|
86
|
+
return null; // Tag not properly closed on this line
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Detect fold regions based on HTML/XML tag matching
|
|
90
|
+
* Properly pairs opening tags with their corresponding closing tags
|
|
91
|
+
*/
|
|
92
|
+
function detectHtmlTagFolds(lines, minLines = 2) {
|
|
93
|
+
const regions = [];
|
|
94
|
+
const tagStack = [];
|
|
95
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
96
|
+
const lineText = lines[lineNum].text;
|
|
97
|
+
let inComment = false;
|
|
98
|
+
let pos = 0;
|
|
99
|
+
while (pos < lineText.length) {
|
|
100
|
+
// Skip HTML comments
|
|
101
|
+
if (lineText.slice(pos, pos + 4) === '<!--') {
|
|
102
|
+
const endComment = lineText.indexOf('-->', pos + 4);
|
|
103
|
+
if (endComment >= 0) {
|
|
104
|
+
pos = endComment + 3;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
inComment = true;
|
|
109
|
+
break;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (lineText[pos] === '<') {
|
|
113
|
+
const tagInfo = extractTagName(lineText, pos);
|
|
114
|
+
if (tagInfo) {
|
|
115
|
+
if (tagInfo.isClosing) {
|
|
116
|
+
// Find matching opening tag (search from top of stack)
|
|
117
|
+
for (let i = tagStack.length - 1; i >= 0; i--) {
|
|
118
|
+
if (tagStack[i].name === tagInfo.name) {
|
|
119
|
+
const openTag = tagStack[i];
|
|
120
|
+
// Remove all tags from this point up (handles malformed HTML)
|
|
121
|
+
tagStack.splice(i);
|
|
122
|
+
// Create fold region if spans multiple lines
|
|
123
|
+
if (lineNum - openTag.line >= minLines) {
|
|
124
|
+
regions.push({
|
|
125
|
+
startLine: openTag.line,
|
|
126
|
+
endLine: lineNum,
|
|
127
|
+
level: openTag.level,
|
|
128
|
+
type: 'bracket',
|
|
129
|
+
collapsed: false
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
else if (!tagInfo.isSelfClosing) {
|
|
137
|
+
// Opening tag - push to stack
|
|
138
|
+
tagStack.push({
|
|
139
|
+
name: tagInfo.name,
|
|
140
|
+
line: lineNum,
|
|
141
|
+
column: pos,
|
|
142
|
+
level: tagStack.length
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
pos = tagInfo.endPos + 1;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
pos++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return regions;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Region markers for different languages
|
|
156
|
+
*/
|
|
157
|
+
const REGION_MARKERS = {
|
|
158
|
+
default: {
|
|
159
|
+
start: /^\s*(?:\/\/|#|--|\/\*)\s*#?region\b/i,
|
|
160
|
+
end: /^\s*(?:\/\/|#|--|\/\*)\s*#?endregion\b/i
|
|
161
|
+
},
|
|
162
|
+
python: {
|
|
163
|
+
start: /^\s*#\s*region\b/i,
|
|
164
|
+
end: /^\s*#\s*endregion\b/i
|
|
165
|
+
},
|
|
166
|
+
html: {
|
|
167
|
+
start: /^\s*<!--\s*#?region\b/i,
|
|
168
|
+
end: /^\s*<!--\s*#?endregion\b/i
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Get the indentation level of a line (number of leading spaces/tabs)
|
|
173
|
+
*/
|
|
174
|
+
function getIndentLevel(text, tabSize = 4) {
|
|
175
|
+
let level = 0;
|
|
176
|
+
for (const char of text) {
|
|
177
|
+
if (char === ' ') {
|
|
178
|
+
level++;
|
|
179
|
+
}
|
|
180
|
+
else if (char === '\t') {
|
|
181
|
+
level += tabSize;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return level;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if a line is effectively empty (whitespace only)
|
|
191
|
+
*/
|
|
192
|
+
function isEmptyLine(text) {
|
|
193
|
+
return text.trim().length === 0;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Detect fold regions based on indentation
|
|
197
|
+
*
|
|
198
|
+
* Single pass: maintain a stack of "open" indentation blocks. When a non-empty
|
|
199
|
+
* line is less-or-equally indented than the block on top of the stack, that
|
|
200
|
+
* block closes at the last non-empty line seen before it. This replaces the
|
|
201
|
+
* previous O(n^2) approach (which re-scanned forward from every line).
|
|
202
|
+
*/
|
|
203
|
+
function detectIndentationFolds(lines, tabSize = 4, minLines = 2) {
|
|
204
|
+
const regions = [];
|
|
205
|
+
const lineCount = lines.length;
|
|
206
|
+
const stack = [];
|
|
207
|
+
let lastNonEmptyLine = -1;
|
|
208
|
+
const closeBlock = (block) => {
|
|
209
|
+
// The block spans from its header down to the last non-empty line that
|
|
210
|
+
// belonged to it (lastNonEmptyLine, captured before the dedent).
|
|
211
|
+
if (lastNonEmptyLine - block.startLine >= minLines) {
|
|
212
|
+
regions.push({
|
|
213
|
+
startLine: block.startLine,
|
|
214
|
+
endLine: lastNonEmptyLine,
|
|
215
|
+
level: block.indent,
|
|
216
|
+
type: 'indentation',
|
|
217
|
+
collapsed: false
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
for (let i = 0; i < lineCount; i++) {
|
|
222
|
+
const line = lines[i];
|
|
223
|
+
// Empty lines do not change indentation structure; they are absorbed into
|
|
224
|
+
// the surrounding block (handled because we only close on dedent at a
|
|
225
|
+
// non-empty line, and endLine tracks lastNonEmptyLine).
|
|
226
|
+
if (isEmptyLine(line.text))
|
|
227
|
+
continue;
|
|
228
|
+
const indent = getIndentLevel(line.text, tabSize);
|
|
229
|
+
// Close any blocks whose header is at >= this indentation: this line is a
|
|
230
|
+
// sibling or an outdent, so those blocks ended at lastNonEmptyLine.
|
|
231
|
+
while (stack.length > 0 && indent <= stack[stack.length - 1].indent) {
|
|
232
|
+
closeBlock(stack.pop());
|
|
233
|
+
}
|
|
234
|
+
// This line becomes a potential block header for deeper-indented lines.
|
|
235
|
+
stack.push({ startLine: i, indent });
|
|
236
|
+
lastNonEmptyLine = i;
|
|
237
|
+
}
|
|
238
|
+
// Close any remaining open blocks at end of document.
|
|
239
|
+
while (stack.length > 0) {
|
|
240
|
+
closeBlock(stack.pop());
|
|
241
|
+
}
|
|
242
|
+
return regions;
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Detect fold regions based on bracket matching
|
|
246
|
+
*/
|
|
247
|
+
function detectBracketFolds(lines, language = 'default', minLines = 2) {
|
|
248
|
+
const regions = [];
|
|
249
|
+
const pairs = BRACKET_PAIRS[language] || BRACKET_PAIRS.default;
|
|
250
|
+
const stack = [];
|
|
251
|
+
// Block-comment state must persist across lines: a `/*` on one line keeps us
|
|
252
|
+
// "inside a comment" until a matching `*/` is found, even many lines later.
|
|
253
|
+
let inBlockComment = false;
|
|
254
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
255
|
+
const lineText = lines[lineNum].text;
|
|
256
|
+
let inString = false;
|
|
257
|
+
let stringChar = '';
|
|
258
|
+
let inLineComment = false;
|
|
259
|
+
for (let col = 0; col < lineText.length; col++) {
|
|
260
|
+
const char = lineText[col];
|
|
261
|
+
// Inside a block comment: only look for the terminating `*/`.
|
|
262
|
+
if (inBlockComment) {
|
|
263
|
+
if (char === '*' && lineText[col + 1] === '/') {
|
|
264
|
+
inBlockComment = false;
|
|
265
|
+
col++; // consume the '/'
|
|
266
|
+
}
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
// Handle string detection with proper escape handling
|
|
270
|
+
// Count preceding backslashes: odd = escaped, even = not escaped
|
|
271
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
272
|
+
let backslashCount = 0;
|
|
273
|
+
let checkPos = col - 1;
|
|
274
|
+
while (checkPos >= 0 && lineText[checkPos] === '\\') {
|
|
275
|
+
backslashCount++;
|
|
276
|
+
checkPos--;
|
|
277
|
+
}
|
|
278
|
+
// Quote is escaped only if preceded by odd number of backslashes
|
|
279
|
+
const isEscaped = backslashCount % 2 === 1;
|
|
280
|
+
if (!isEscaped) {
|
|
281
|
+
if (!inString) {
|
|
282
|
+
inString = true;
|
|
283
|
+
stringChar = char;
|
|
284
|
+
}
|
|
285
|
+
else if (char === stringChar) {
|
|
286
|
+
inString = false;
|
|
287
|
+
stringChar = '';
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
if (inString)
|
|
293
|
+
continue;
|
|
294
|
+
// Handle block comment start (outside strings).
|
|
295
|
+
if (char === '/' && lineText[col + 1] === '*') {
|
|
296
|
+
inBlockComment = true;
|
|
297
|
+
col++; // consume the '*'
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
// Handle line comments
|
|
301
|
+
if (char === '/' && lineText[col + 1] === '/') {
|
|
302
|
+
inLineComment = true;
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
if (inLineComment)
|
|
306
|
+
continue;
|
|
307
|
+
// Check for opening brackets
|
|
308
|
+
for (const [open, close] of pairs) {
|
|
309
|
+
if (char === open) {
|
|
310
|
+
stack.push({
|
|
311
|
+
char: open,
|
|
312
|
+
closingChar: close,
|
|
313
|
+
line: lineNum,
|
|
314
|
+
column: col
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
else if (char === close && stack.length > 0) {
|
|
318
|
+
// Only match if top of stack is the correct closing bracket
|
|
319
|
+
// This ensures proper nesting (e.g., `{ ( } )` won't match incorrectly)
|
|
320
|
+
const topBracket = stack[stack.length - 1];
|
|
321
|
+
if (topBracket.closingChar === close) {
|
|
322
|
+
stack.pop();
|
|
323
|
+
// Create fold region if spans multiple lines
|
|
324
|
+
if (lineNum - topBracket.line >= minLines) {
|
|
325
|
+
regions.push({
|
|
326
|
+
startLine: topBracket.line,
|
|
327
|
+
endLine: lineNum,
|
|
328
|
+
level: stack.length, // Use current stack depth for level
|
|
329
|
+
type: 'bracket',
|
|
330
|
+
collapsed: false
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
// If mismatch, ignore the closing bracket (unbalanced brackets)
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
return regions;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Detect fold regions based on region markers
|
|
343
|
+
*/
|
|
344
|
+
function detectRegionFolds(lines, language = 'default') {
|
|
345
|
+
const regions = [];
|
|
346
|
+
const markers = REGION_MARKERS[language] || REGION_MARKERS.default;
|
|
347
|
+
const stack = [];
|
|
348
|
+
let level = 0;
|
|
349
|
+
for (let i = 0; i < lines.length; i++) {
|
|
350
|
+
const lineText = lines[i].text;
|
|
351
|
+
if (markers.start.test(lineText)) {
|
|
352
|
+
stack.push({ line: i, level: level++ });
|
|
353
|
+
}
|
|
354
|
+
else if (markers.end.test(lineText) && stack.length > 0) {
|
|
355
|
+
const start = stack.pop();
|
|
356
|
+
level--;
|
|
357
|
+
regions.push({
|
|
358
|
+
startLine: start.line,
|
|
359
|
+
endLine: i,
|
|
360
|
+
level: start.level,
|
|
361
|
+
type: 'region',
|
|
362
|
+
collapsed: false
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return regions;
|
|
367
|
+
}
|
|
368
|
+
/**
|
|
369
|
+
* Detect fold regions for multi-line comments
|
|
370
|
+
*/
|
|
371
|
+
function detectCommentFolds(lines, minLines = 2) {
|
|
372
|
+
const regions = [];
|
|
373
|
+
let commentStart = -1;
|
|
374
|
+
for (let i = 0; i < lines.length; i++) {
|
|
375
|
+
const lineText = lines[i].text.trim();
|
|
376
|
+
// Block comment start
|
|
377
|
+
if (lineText.startsWith('/*') && !lineText.includes('*/')) {
|
|
378
|
+
commentStart = i;
|
|
379
|
+
}
|
|
380
|
+
// Block comment end
|
|
381
|
+
else if (commentStart >= 0 && lineText.includes('*/')) {
|
|
382
|
+
if (i - commentStart >= minLines) {
|
|
383
|
+
regions.push({
|
|
384
|
+
startLine: commentStart,
|
|
385
|
+
endLine: i,
|
|
386
|
+
level: 0,
|
|
387
|
+
type: 'comment',
|
|
388
|
+
collapsed: false
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
commentStart = -1;
|
|
392
|
+
}
|
|
393
|
+
// HTML comment
|
|
394
|
+
else if (lineText.startsWith('<!--') && !lineText.includes('-->')) {
|
|
395
|
+
commentStart = i;
|
|
396
|
+
}
|
|
397
|
+
else if (commentStart >= 0 && lineText.includes('-->')) {
|
|
398
|
+
if (i - commentStart >= minLines) {
|
|
399
|
+
regions.push({
|
|
400
|
+
startLine: commentStart,
|
|
401
|
+
endLine: i,
|
|
402
|
+
level: 0,
|
|
403
|
+
type: 'comment',
|
|
404
|
+
collapsed: false
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
commentStart = -1;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
return regions;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Detect all fold regions in a document
|
|
414
|
+
*/
|
|
415
|
+
export function detectFoldRegions(lines, language = 'plaintext', config = {}) {
|
|
416
|
+
const cfg = { ...DEFAULT_CONFIG, ...config };
|
|
417
|
+
const allRegions = [];
|
|
418
|
+
// Detect different types of fold regions
|
|
419
|
+
if (cfg.brackets) {
|
|
420
|
+
const langLower = language.toLowerCase();
|
|
421
|
+
// Pure HTML/XML use tag-based folding only
|
|
422
|
+
const pureMarkupLanguages = ['html', 'xml', 'xhtml', 'svg'];
|
|
423
|
+
// Mixed languages use both tag and bracket folding
|
|
424
|
+
const mixedLanguages = ['vue', 'svelte', 'jsx', 'tsx'];
|
|
425
|
+
if (pureMarkupLanguages.includes(langLower)) {
|
|
426
|
+
allRegions.push(...detectHtmlTagFolds(lines, cfg.minLines));
|
|
427
|
+
}
|
|
428
|
+
else if (mixedLanguages.includes(langLower)) {
|
|
429
|
+
// Mixed languages: detect both HTML tags and JS/TS braces
|
|
430
|
+
allRegions.push(...detectHtmlTagFolds(lines, cfg.minLines));
|
|
431
|
+
allRegions.push(...detectBracketFolds(lines, 'default', cfg.minLines));
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
allRegions.push(...detectBracketFolds(lines, langLower, cfg.minLines));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
if (cfg.indentation) {
|
|
438
|
+
// For languages like Python, prioritize indentation
|
|
439
|
+
const indentLanguages = ['python', 'yaml', 'yml', 'haml', 'slim', 'pug', 'jade'];
|
|
440
|
+
if (indentLanguages.includes(language.toLowerCase())) {
|
|
441
|
+
allRegions.push(...detectIndentationFolds(lines, 4, cfg.minLines));
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
if (cfg.regions) {
|
|
445
|
+
allRegions.push(...detectRegionFolds(lines, language));
|
|
446
|
+
}
|
|
447
|
+
if (cfg.comments) {
|
|
448
|
+
allRegions.push(...detectCommentFolds(lines, cfg.minLines));
|
|
449
|
+
}
|
|
450
|
+
// Sort by start line, then by length (longer regions first for nesting)
|
|
451
|
+
allRegions.sort((a, b) => {
|
|
452
|
+
if (a.startLine !== b.startLine) {
|
|
453
|
+
return a.startLine - b.startLine;
|
|
454
|
+
}
|
|
455
|
+
return (b.endLine - b.startLine) - (a.endLine - a.startLine);
|
|
456
|
+
});
|
|
457
|
+
// Remove overlapping regions (keep the first/largest one)
|
|
458
|
+
const filteredRegions = [];
|
|
459
|
+
const coveredLines = new Set();
|
|
460
|
+
for (const region of allRegions) {
|
|
461
|
+
// Check if this region's start line is already covered
|
|
462
|
+
if (coveredLines.has(region.startLine)) {
|
|
463
|
+
continue;
|
|
464
|
+
}
|
|
465
|
+
filteredRegions.push(region);
|
|
466
|
+
// Mark start line as covered (we can still have nested folds with different start lines)
|
|
467
|
+
coveredLines.add(region.startLine);
|
|
468
|
+
}
|
|
469
|
+
return filteredRegions;
|
|
470
|
+
}
|
|
471
|
+
/**
|
|
472
|
+
* Fold state manager
|
|
473
|
+
*/
|
|
474
|
+
export class FoldManager {
|
|
475
|
+
regions = [];
|
|
476
|
+
collapsedLines = new Set();
|
|
477
|
+
listeners = new Set();
|
|
478
|
+
/**
|
|
479
|
+
* Update fold regions based on document content
|
|
480
|
+
*/
|
|
481
|
+
updateRegions(lines, language, config) {
|
|
482
|
+
// Preserve collapsed state for regions that still exist
|
|
483
|
+
const wasCollapsed = new Set();
|
|
484
|
+
for (const region of this.regions) {
|
|
485
|
+
if (region.collapsed) {
|
|
486
|
+
wasCollapsed.add(region.startLine);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
this.regions = detectFoldRegions(lines, language, config);
|
|
490
|
+
// Restore collapsed state
|
|
491
|
+
for (const region of this.regions) {
|
|
492
|
+
if (wasCollapsed.has(region.startLine)) {
|
|
493
|
+
region.collapsed = true;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
this.updateCollapsedLines();
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Get all fold regions
|
|
500
|
+
*/
|
|
501
|
+
getRegions() {
|
|
502
|
+
return this.regions;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get fold region at a specific line (if it starts there)
|
|
506
|
+
*/
|
|
507
|
+
getRegionAtLine(line) {
|
|
508
|
+
return this.regions.find(r => r.startLine === line);
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Check if a line is hidden (inside a collapsed fold)
|
|
512
|
+
*/
|
|
513
|
+
isLineHidden(line) {
|
|
514
|
+
return this.collapsedLines.has(line);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Check if a line has a fold indicator
|
|
518
|
+
*/
|
|
519
|
+
hasFoldIndicator(line) {
|
|
520
|
+
return this.regions.some(r => r.startLine === line);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Check if the fold at this line is collapsed
|
|
524
|
+
*/
|
|
525
|
+
isFoldCollapsed(line) {
|
|
526
|
+
const region = this.getRegionAtLine(line);
|
|
527
|
+
return region?.collapsed ?? false;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Toggle fold at a line
|
|
531
|
+
*/
|
|
532
|
+
toggleFold(line) {
|
|
533
|
+
const region = this.getRegionAtLine(line);
|
|
534
|
+
if (!region)
|
|
535
|
+
return false;
|
|
536
|
+
region.collapsed = !region.collapsed;
|
|
537
|
+
this.updateCollapsedLines();
|
|
538
|
+
this.emit();
|
|
539
|
+
return true;
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* Collapse fold at a line
|
|
543
|
+
*/
|
|
544
|
+
collapse(line) {
|
|
545
|
+
const region = this.getRegionAtLine(line);
|
|
546
|
+
if (!region || region.collapsed)
|
|
547
|
+
return false;
|
|
548
|
+
region.collapsed = true;
|
|
549
|
+
this.updateCollapsedLines();
|
|
550
|
+
this.emit();
|
|
551
|
+
return true;
|
|
552
|
+
}
|
|
553
|
+
/**
|
|
554
|
+
* Expand fold at a line
|
|
555
|
+
*/
|
|
556
|
+
expand(line) {
|
|
557
|
+
const region = this.getRegionAtLine(line);
|
|
558
|
+
if (!region || !region.collapsed)
|
|
559
|
+
return false;
|
|
560
|
+
region.collapsed = false;
|
|
561
|
+
this.updateCollapsedLines();
|
|
562
|
+
this.emit();
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Collapse all folds
|
|
567
|
+
*/
|
|
568
|
+
collapseAll() {
|
|
569
|
+
for (const region of this.regions) {
|
|
570
|
+
region.collapsed = true;
|
|
571
|
+
}
|
|
572
|
+
this.updateCollapsedLines();
|
|
573
|
+
this.emit();
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Expand all folds
|
|
577
|
+
*/
|
|
578
|
+
expandAll() {
|
|
579
|
+
for (const region of this.regions) {
|
|
580
|
+
region.collapsed = false;
|
|
581
|
+
}
|
|
582
|
+
this.collapsedLines.clear();
|
|
583
|
+
this.emit();
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* Collapse folds at a specific nesting level
|
|
587
|
+
*/
|
|
588
|
+
collapseLevel(level) {
|
|
589
|
+
for (const region of this.regions) {
|
|
590
|
+
if (region.level === level) {
|
|
591
|
+
region.collapsed = true;
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
this.updateCollapsedLines();
|
|
595
|
+
this.emit();
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* Get visible lines (line numbers that should be rendered)
|
|
599
|
+
*/
|
|
600
|
+
getVisibleLines(totalLines) {
|
|
601
|
+
const visible = [];
|
|
602
|
+
for (let i = 0; i < totalLines; i++) {
|
|
603
|
+
if (!this.collapsedLines.has(i)) {
|
|
604
|
+
visible.push(i);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return visible;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Get the number of hidden lines after a fold start
|
|
611
|
+
*/
|
|
612
|
+
getHiddenLineCount(startLine) {
|
|
613
|
+
const region = this.getRegionAtLine(startLine);
|
|
614
|
+
if (!region || !region.collapsed)
|
|
615
|
+
return 0;
|
|
616
|
+
return region.endLine - region.startLine;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Update the set of collapsed (hidden) lines
|
|
620
|
+
*/
|
|
621
|
+
updateCollapsedLines() {
|
|
622
|
+
this.collapsedLines.clear();
|
|
623
|
+
for (const region of this.regions) {
|
|
624
|
+
if (region.collapsed) {
|
|
625
|
+
// Hide all lines except the first one
|
|
626
|
+
for (let i = region.startLine + 1; i <= region.endLine; i++) {
|
|
627
|
+
this.collapsedLines.add(i);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** Maximum listeners to prevent memory leaks */
|
|
633
|
+
static MAX_LISTENERS = 100;
|
|
634
|
+
/**
|
|
635
|
+
* Subscribe to fold changes
|
|
636
|
+
* In development mode, throws an error to catch memory leaks early.
|
|
637
|
+
* In production, warns and evicts the oldest listener.
|
|
638
|
+
*/
|
|
639
|
+
onChange(callback) {
|
|
640
|
+
if (this.listeners.size >= FoldManager.MAX_LISTENERS) {
|
|
641
|
+
const msg = `[FoldManager] Maximum listener count (${FoldManager.MAX_LISTENERS}) exceeded. Possible memory leak - ensure listeners are unsubscribed.`;
|
|
642
|
+
// Fail fast in development to catch bugs early
|
|
643
|
+
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'development') {
|
|
644
|
+
throw new Error(msg);
|
|
645
|
+
}
|
|
646
|
+
// In production, warn and auto-cleanup oldest listener
|
|
647
|
+
console.warn(msg);
|
|
648
|
+
const firstListener = this.listeners.values().next().value;
|
|
649
|
+
if (firstListener) {
|
|
650
|
+
this.listeners.delete(firstListener);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
this.listeners.add(callback);
|
|
654
|
+
return () => this.listeners.delete(callback);
|
|
655
|
+
}
|
|
656
|
+
emit() {
|
|
657
|
+
for (const listener of this.listeners) {
|
|
658
|
+
try {
|
|
659
|
+
listener();
|
|
660
|
+
}
|
|
661
|
+
catch (error) {
|
|
662
|
+
console.error('[FoldManager] Listener callback failed:', error);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Create a new fold manager
|
|
669
|
+
*/
|
|
670
|
+
export function createFoldManager() {
|
|
671
|
+
return new FoldManager();
|
|
672
|
+
}
|