12 KiB
TASK-011 Phase 4: Fix Document State Corruption (Final 5%)
Status: 🟡 Ready to Start
Priority: P1 - High (Editor 95% working, final polish needed)
Started: 2026-01-11
Depends on: TASK-011-PHASE-3 (Completed)
Context
Phase 3 successfully fixed the critical cursor positioning and feedback loop issues! The editor is now 95% functional with excellent features:
✅ What's Working Perfectly (Phase 3 Fixes):
- ✅ Syntax highlighting with VSCode Dark+ theme
- ✅ Autocompletion with Noodl-specific completions
- ✅ Linting and inline error display
- ✅ Cursor positioning (FIXED - no more jumps!)
- ✅ Click positioning (accurate)
- ✅ Arrow navigation (smooth)
- ✅ Basic typing (no lag)
- ✅ Format button (Prettier integration)
- ✅ History tracking and restore
- ✅ Resize functionality
- ✅ Keyboard shortcuts (Cmd+S, Cmd+/, etc.)
- ✅ Line numbers, active line highlighting
- ✅ Search/replace
- ✅ Undo/redo
🔴 Remaining Issues (5%)
Issue 1: Empty Braces + Enter Key Corruption
Problem:
When typing {} and pressing Enter between braces, document state becomes corrupted:
- Type
{→ closing}appears automatically ✅ - Press Enter between braces
- BUG: Closing brace moves to line 2 (should be line 3)
- BUG: Left gutter highlights lines 2+ as if "inside braces"
- Try to type text → each character appears on new line (SEVERE)
- Fold/unfold the braces → temporarily fixes, but re-breaks on unfold
Expected Behavior:
{
█ // Cursor here with proper indentation
}
Actual Behavior:
{
}█ // Cursor here, no indentation
// Then each typed character creates a new line
Issue 2: JSON Object Literal Validation
Problem:
Typing {"foo": "bar"} shows error: Unexpected token ':'
Needs Investigation:
- This might be correct for Expression validation (objects need parens in expressions)
- Need to verify:
- Does
({"foo": "bar"})work without error? - Is this only in Expression nodes (correct) or also in Script nodes (wrong)?
- Should we detect object literals and suggest wrapping in parens?
- Does
Root Cause Analysis
Issue 1 Root Cause: Race Condition in State Synchronization
The Problem:
const handleChange = useCallback(
(newValue: string) => {
isInternalChangeRef.current = true;
// ... update validation, call onChange ...
setTimeout(() => {
isInternalChangeRef.current = false; // ❌ NOT RELIABLE
}, 0);
},
[onChange, validationType]
);
useEffect(() => {
if (isInternalChangeRef.current) return; // Skip internal changes
// Sync external value changes
editorViewRef.current.dispatch({
changes: {
/* full document replacement */
}
});
}, [value, validationType]);
What Goes Wrong:
closeBrackets()auto-adds}→ triggershandleChange- Sets
isInternalChangeRef.current = true - Calls parent
onChangewith"{}" - Schedules reset with
setTimeout(..., 0) - BEFORE setTimeout fires: React re-renders (validation state change)
- Value sync
useEffectseesisInternalChangeRefstill true → skips (good!) - AFTER setTimeout fires: Flag resets to false
- Another React render happens (from fold, or validation, or something)
- Value sync
useEffectruns with flag = false - Full document replacement → CORRUPTION
Additional Factors:
indentOnInput()extension might be interferingcloseBrackets()+ custom Enter handler conflictfoldGutter()operations trigger unexpected re-renders- Enter key handler may not be firing due to keymap order
Solution Strategy
Strategy 1: Eliminate Race Condition (Recommended)
Replace setTimeout with more reliable synchronization:
// Use a counter instead of boolean
const changeGenerationRef = useRef(0);
const handleChange = useCallback(
(newValue: string) => {
const generation = ++changeGenerationRef.current;
// Propagate to parent
if (onChange) onChange(newValue);
// NO setTimeout - just track generation
},
[onChange]
);
useEffect(() => {
// Check if this is from our last internal change
const lastGeneration = lastExternalGenerationRef.current;
if (changeGenerationRef.current > lastGeneration) {
// We've had internal changes since last external update
return;
}
// Safe to sync
lastExternalGenerationRef.current = changeGenerationRef.current;
// ... sync value
}, [value]);
Strategy 2: Fix Extension Conflicts
Test extensions in isolation:
// Start with MINIMAL extensions
const extensions: Extension[] = [
javascript(),
createOpenNoodlTheme(),
lineNumbers(),
history(),
EditorView.lineWrapping,
customKeybindings(options),
EditorView.updateListener.of(onChange)
];
// Add back one at a time:
// 1. Test without closeBrackets() - does Enter work?
// 2. Test without indentOnInput() - does Enter work?
// 3. Test without foldGutter() - does Enter work?
Strategy 3: Custom Enter Handler (Already Attempted)
Current implementation not firing - needs to be FIRST in keymap order:
// Move customKeybindings BEFORE other keymaps in extensions array
const extensions: Extension[] = [
javascript(),
createOpenNoodlTheme(),
// ⚠️ KEYBINDINGS MUST BE EARLY
customKeybindings(options), // Has custom Enter handler
// Then other extensions that might handle keys
bracketMatching(),
closeBrackets()
// ...
];
Implementation Plan
Phase 1: Isolate the Problem (30 minutes)
Goal: Determine which extension causes the corruption
-
Strip down to minimal extensions:
const extensions: Extension[] = [ javascript(), createOpenNoodlTheme(), lineNumbers(), history(), EditorView.lineWrapping, customKeybindings(options), onChange ? EditorView.updateListener.of(...) : [] ]; -
Test basic typing:
- Type
{} - Press Enter
- Does it work? If YES → one of the removed extensions is the culprit
- Type
-
Add extensions back one by one:
- Add
closeBrackets()→ test - Add
indentOnInput()→ test - Add
foldGutter()→ test - Add
bracketMatching()→ test
- Add
-
Identify culprit extension(s)
Phase 2: Fix Synchronization Race (1 hour)
Goal: Eliminate the setTimeout-based race condition
- Implement generation counter approach
- Test that value sync doesn't corrupt during typing
- Verify fold/unfold doesn't trigger corruption
Phase 3: Fix Enter Key Handler (30 minutes)
Goal: Custom Enter handler fires reliably
- Move keybindings earlier in extension order
- Add logging to confirm handler fires
- Test brace expansion works correctly
Phase 4: Fix JSON Validation (15 minutes)
Goal: Clarify if this is bug or correct behavior
- Test in Expression node:
({"foo": "bar"})- should work - Test in Script node:
{"foo": "bar"}- should work - If Expression requires parens: Add helpful error message or auto-suggestion
Phase 5: Comprehensive Testing (30 minutes)
Run all original test cases:
- ✅ Basic typing:
hello world - ✅ Empty braces:
{}→ Enter → type inside - ✅ Navigation: Arrow keys move correctly
- ✅ Clicking: Cursor appears at click position
- ✅ JSON: Object literals validate correctly
- ✅ Multi-line: Complex code structures
- ✅ Fold/unfold: No corruption
- ✅ Format: Code reformats correctly
- ✅ History: Restore previous versions
- ✅ Resize: Editor resizes smoothly
Success Criteria
Must Have:
- Type
{}, press Enter, type text → text appears on single line with proper indentation - No "character per line" corruption
- Fold/unfold braces doesn't cause issues
- All Phase 3 fixes remain working (cursor, navigation, etc.)
Should Have:
- JSON object literals handled correctly (or clear error message)
- Custom Enter handler provides nice brace expansion
- No console errors
- Smooth, responsive typing experience
Nice to Have:
- Auto-indent works intelligently
- Bracket auto-closing works without conflicts
- Code folding available for complex functions
Time Budget
Estimated Time: 2-3 hours
Maximum Time: 4 hours before considering alternate approaches
If exceeds 4 hours:
- Consider disabling problematic extensions permanently
- Consider simpler Enter key handling
- Consider removing fold functionality if unsolvable
Fallback Options
Option A: Disable Problematic Extensions
If we can't fix the conflicts, disable:
closeBrackets()- user can type closing braces manuallyfoldGutter()- less critical featureindentOnInput()- user can use Tab key
Pros: Editor is 100% stable and functional
Cons: Slightly less convenient
Option B: Simplified Enter Handler
Instead of smart brace handling, just handle Enter normally:
// Let default Enter behavior work
// Add one level of indentation when inside braces
// Don't try to auto-expand braces
Option C: Keep Current State
The editor is 95% functional. We could:
- Document the brace issue as known limitation
- Suggest users type closing brace manually first
- Focus on other high-priority tasks
Testing Checklist
After implementing fix:
Core Functionality
- Basic typing works smoothly
- Cursor stays in correct position
- Click positioning is accurate
- Arrow key navigation works
- Syntax highlighting displays correctly
Brace Handling (The Fix!)
- Type
{}→ closes automatically - Press Enter between braces → creates 3 lines
- Cursor positioned on middle line with indentation
- Type text → appears on that line (NOT new lines)
- Closing brace is on its own line
- No corruption after fold/unfold
Validation
- Invalid code shows error
- Valid code shows green checkmark
- Error messages are helpful
- Object literals handled correctly
Advanced Features
- Format button works
- History restore works
- Cmd+S saves
- Cmd+/ toggles comments
- Resize grip works
- Search/replace works
Edge Cases
- Empty editor → start typing works
- Select all → replace works
- Undo/redo doesn't corrupt
- Multiple nested braces work
- Long lines wrap correctly
Notes
What Phase 3 Accomplished
Phase 3 fixed the critical issue - the cursor feedback loop that made the editor unusable. The fixes were:
- Removed
setLocalValue()during typing - eliminated re-render storms - Added
isInternalChangeRefflag - prevents value sync loops - Made CodeMirror single source of truth - cleaner architecture
- Preserved cursor during external updates - smooth when needed
These changes brought the editor from "completely broken" to "95% excellent".
What Phase 4 Needs to Do
Phase 4 is about polishing the last 5% - fixing edge cases with auto-bracket expansion and Enter key handling. This is much simpler than Phase 3's fundamental architectural fix.
Key Insight
The issue is NOT with our Phase 3 fixes - those work great for normal typing. The issue is conflicts between CodeMirror extensions when handling special keys (Enter) and operations (fold/unfold).
References
- Phase 3 Task:
TASK-011-PHASE-3-CURSOR-FIXES.md- Background on cursor fixes - CodeMirror Docs: https://codemirror.net/docs/
- Extension Conflicts: https://codemirror.net/examples/config/
- Keymap Priority: https://codemirror.net/docs/ref/#view.keymap
Created: 2026-01-11
Last Updated: 2026-01-11