Skip to content

fix: preserve raw markdown content in markdown mode#12

Merged
saadiq merged 11 commits intomainfrom
saadiq/fix-markdown-whitespace
Oct 5, 2025
Merged

fix: preserve raw markdown content in markdown mode#12
saadiq merged 11 commits intomainfrom
saadiq/fix-markdown-whitespace

Conversation

@saadiq
Copy link
Copy Markdown
Owner

@saadiq saadiq commented Oct 5, 2025

Summary

Fixes markdown editor to display files exactly as they appear on disk, preserving all whitespace including multiple blank lines when in markdown mode.

Problem

The markdown editor was normalizing content through the markdown-it → HTML → turndown pipeline even when viewing in markdown mode. This caused:

  • Files to display normalized content instead of raw content
  • Extra whitespace to be collapsed/altered
  • False "dirty" states when file content didn't match what was shown

Solution

  • MarkdownEditor: Removed all normalization logic - now displays/edits markdown exactly as typed
  • EditorComponent: Centralized normalization control to only normalize when:
    • Switching from markdown → WYSIWYG mode
    • Initially loading a file when already in WYSIWYG mode
  • Disabled automatic onContentLoaded callback from useEditor hook

Result

✅ Markdown mode displays files exactly as they are on disk
✅ WYSIWYG mode still receives normalized content for proper rendering
✅ No false dirty states - content only changes when explicitly switching to WYSIWYG
✅ Round-trip fidelity maintained where needed

Testing

  • Verified files with multiple blank lines display correctly in markdown mode
  • Confirmed switching to WYSIWYG mode normalizes content as expected
  • Checked that dirty state tracking works correctly

…tion

Moved normalization control from MarkdownEditor to EditorComponent to display files exactly as they appear on disk when in markdown mode. Previously, content was being normalized through markdown-it → HTML → turndown pipeline even in markdown mode, causing unwanted whitespace changes and false dirty states. Normalization now only occurs when switching to WYSIWYG mode or initially loading in WYSIWYG mode, ensuring markdown mode preserves all original formatting including multiple blank lines.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Code Review: Fix Markdown Whitespace Preservation

Summary

This PR successfully addresses the issue of markdown content normalization by centralizing normalization control in EditorComponent and removing it from MarkdownEditor. The approach is sound and aligns with the stated goals.


✅ Strengths

  1. Clear Separation of Concerns: The change correctly identifies that MarkdownEditor should display raw content while EditorComponent handles normalization only when needed for WYSIWYG mode.

  2. Preserved User Intent: Files now display exactly as they appear on disk in markdown mode, which is the expected behavior for a markdown editor.

  3. Proper State Management: The solution correctly updates originalContent via onContentLoaded to prevent false dirty states.

  4. Well-Tested Logic: The isInitialLoad detection and view mode transition handling appears robust.


🔍 Issues & Concerns

1. Missing Dependency in useEffect (High Priority)

Location: EditorComponent.tsx:94

The useEffect has an incorrect dependency array. It includes onUpdate and onContentLoaded, but these are likely recreated on every render from the parent component, which could cause infinite loops or unnecessary re-renders.

// Current (line 94)
}, [viewMode, editor, content, onUpdate, onContentLoaded]);

Recommendation: Use useCallback refs or remove these from dependencies if they don't change:

const onUpdateRef = useRef(onUpdate);
const onContentLoadedRef = useRef(onContentLoaded);

useEffect(() => {
  onUpdateRef.current = onUpdate;
  onContentLoadedRef.current = onContentLoaded;
}, [onUpdate, onContentLoaded]);

// Then use onUpdateRef.current and onContentLoadedRef.current in the effect
// and remove them from the dependency array

Note: useEditor hook already does this correctly (see useEditor.ts:59-62). The same pattern should be applied here.

2. Logic Complexity in useEffect (Medium Priority)

Location: EditorComponent.tsx:63-94

The normalization logic has become quite complex with three different conditions:

  • isViewModeTransition
  • isInitialLoad
  • viewMode === 'wysiwyg' && hasContentChanged

Concerns:

  • The isInitialLoad check uses !prevContentRef.current which could be falsy for empty strings, not just undefined/null
  • The condition isInitialLoad && viewMode === 'wysiwyg' is redundant since isInitialLoad already checks prevViewModeRef.current === viewMode

Recommendation: Simplify and add comments:

// Normalize content when:
// 1. Switching from markdown to WYSIWYG (view mode transition)
// 2. Initial load with content while already in WYSIWYG mode
// 3. Content changes while in WYSIWYG mode
const shouldNormalize = 
  isViewModeTransition || 
  (viewMode === 'wysiwyg' && (isInitialLoad || hasContentChanged));

3. Error Handling Inconsistency (Low Priority)

Location: EditorComponent.tsx:81-84

If normalization fails, the error is logged but the content still attempts to convert using contentToConvert = normalizedMarkdown. However, normalizedMarkdown might be in an invalid state.

Recommendation: Fall back to original content on error:

} catch (error) {
  console.error('Failed to normalize markdown content during view mode switch:', error);
  contentToConvert = content; // Use original content on error
}

4. Normalization on Initial WYSIWYG Load Modifies File (Medium Priority)

Location: EditorComponent.tsx:71-80

When initially loading a file in WYSIWYG mode, the code normalizes the content and calls onContentLoaded(normalizedMarkdown), which updates the originalContent in useFileContent. This means:

  • The file content is immediately modified even if the user hasn't made changes
  • If the file has intentional whitespace/formatting, it's lost on first load

Question: Is this intended behavior? Should initial loads in WYSIWYG mode modify the baseline content?

Recommendation: Consider only normalizing on view mode transitions, not initial loads. Or add a user preference for "preserve original formatting until first edit".


🧪 Test Coverage

Critical Issue: No test coverage exists for these components.

Files to test:

  • EditorComponent.tsx - View mode switching, normalization timing
  • MarkdownEditor.tsx - Raw content display/editing
  • Integration test for the full flow

Suggested test cases:

  1. ✅ Markdown mode displays content exactly as loaded
  2. ✅ Switching to WYSIWYG normalizes content
  3. ✅ Switching back to markdown preserves WYSIWYG changes
  4. ✅ No false dirty states when viewing in markdown mode
  5. ✅ Multiple blank lines preserved in markdown mode
  6. ✅ Initial load in WYSIWYG mode behavior
  7. ✅ Content changes in WYSIWYG mode trigger updates
  8. ✅ Error handling when normalization fails

🔒 Security Considerations

No security issues identified. The changes don't introduce new attack vectors:

  • URL sanitization still occurs in markdown.ts
  • No new user input paths
  • No XSS risks

⚡ Performance Considerations

  1. Normalization Cost: Every switch to WYSIWYG mode now runs markdown → HTML → markdown conversion. For large documents (>5000 lines per CLAUDE.md), this could cause lag.

    Recommendation: Consider caching the normalized version or showing a loading indicator for large files.

  2. useEffect Dependency Issue: If onUpdate/onContentLoaded change frequently, this could cause excessive re-renders and normalization cycles.


📝 Documentation

The CLAUDE.md should be updated to reflect this change:

Current documentation (line ~90):

  • Markdown normalization - In markdown mode, content is normalized through markdown-it/turndown pipeline on blur to ensure consistency with WYSIWYG mode

Should be updated to:

  • Markdown normalization - Content is normalized through markdown-it/turndown pipeline only when switching from markdown to WYSIWYG mode, or when initially loading in WYSIWYG mode. Raw markdown mode displays content exactly as it appears on disk.

🎯 Recommendations Summary

Must Fix:

  1. Fix useEffect dependency array (use refs for callbacks)
  2. Add test coverage for view mode switching

Should Fix:
3. Simplify the normalization condition logic
4. Clarify/reconsider normalization on initial WYSIWYG load behavior
5. Update CLAUDE.md documentation

Nice to Have:
6. Improve error handling in normalization try/catch
7. Add performance optimization for large files


✨ Conclusion

This is a well-thought-out fix that correctly identifies and solves the root cause. The main concerns are around the useEffect dependency array (which could cause bugs) and missing test coverage. Once those are addressed, this should be ready to merge.

Approval Status: ⚠️ Approve with changes - Fix the dependency array issue before merging.

Remove normalization on initial WYSIWYG load to preserve original file formatting. Previously, opening a file while in WYSIWYG mode would immediately normalize the content, destructively modifying intentional whitespace without user action. Now normalization only occurs when user explicitly switches from markdown to WYSIWYG mode.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

PR Review: Fix Markdown Whitespace Preservation

Summary

This PR addresses a legitimate issue where markdown files weren't displaying their raw content in markdown mode due to unwanted normalization. The solution appropriately centralizes normalization control in EditorComponent and removes it from MarkdownEditor.


✅ Strengths

1. Clear Problem-Solution Architecture

  • Successfully moves normalization logic from MarkdownEditor to EditorComponent where it belongs
  • Properly preserves raw markdown in markdown mode while still normalizing when switching to WYSIWYG
  • The approach of disabling onContentLoaded in useEditor and handling normalization explicitly is sound

2. Code Quality

  • Clean removal of the normalization pipeline from MarkdownEditor.tsx
  • Good use of refs to track view mode transitions
  • Proper error handling in the normalization block (lines 84-86)

3. Performance

  • Removed unnecessary blur-time normalization that could impact typing performance
  • Normalization only occurs when explicitly switching modes (good UX)

⚠️ Potential Issues & Concerns

1. Critical: Logic Flaw in Initial Load Detection

const isInitialLoad = prevViewModeRef.current === viewMode && !prevContentRef.current && content;

Location: EditorComponent.tsx:66

Issue: This condition is problematic. isInitialLoad will be true whenever:

  • View mode hasn't changed AND
  • Previous content was empty/falsy AND
  • Current content exists

This means switching between files (empty → new file) would trigger this, potentially causing unwanted behavior. The condition seems intended to detect first load, but it's actually checking for "any transition from no content to having content in the same view mode."

Impact: Could cause unnecessary content conversions or re-renders when switching files.

Suggestion: Consider whether this case is even needed. If the goal is to handle initial page load in WYSIWYG mode, you might want:

const isInitialLoad = viewMode === 'wysiwyg' && !prevContentRef.current && content && !prevViewModeRef.current;

Or simply remove it if the isViewModeTransition already covers the necessary cases.

2. Code Smell: Conditional Normalization Dependencies

if (isViewModeTransition && onUpdate && onContentLoaded) {

Location: EditorComponent.tsx:72

Issue: Normalization only happens if both onUpdate AND onContentLoaded are provided. However, onUpdate isn't actually used in the normalization block - only onContentLoaded is called. This creates a confusing dependency.

Impact: If a parent component only provides onContentLoaded (valid per the interface), normalization won't occur, leading to subtle bugs.

Suggestion:

if (isViewModeTransition && onContentLoaded) {
  // normalization logic
}

3. Security: Redundant Parser Creation

const md = createMarkdownParser();
const turndown = createTurndownService();

Location: EditorComponent.tsx:74-75

Issue: These are created fresh on every view mode transition. While not a security issue, it's inefficient. The useEditor hook already creates stable refs for these (lines 56 in useEditor.ts).

Suggestion: Consider using useRef to cache these parsers:

const mdRef = useRef(createMarkdownParser());
const turndownRef = useRef(createTurndownService());

4. Race Condition Risk
The normalization happens in a useEffect, and immediately calls onContentLoaded(normalizedMarkdown), which will trigger the parent to update content, which triggers this same effect again. While the hash check in useEditor prevents infinite loops, the flow is complex and fragile.

Question: Have you tested rapid view mode toggling (markdown → WYSIWYG → markdown → WYSIWYG)? There might be edge cases where the refs don't update in time.


🧪 Test Coverage Concerns

Missing Test Coverage:

  • No tests for EditorComponent.tsx or MarkdownEditor.tsx
  • The complex view mode switching logic has no automated verification
  • The normalization behavior change has no regression protection

High-Risk Scenarios to Test:

  1. ✅ Markdown mode displays raw content (mentioned in PR description)
  2. ✅ WYSIWYG mode normalizes content (mentioned in PR description)
  3. ❓ Switching from markdown → WYSIWYG → markdown preserves content
  4. ❓ Loading a file while already in WYSIWYG mode
  5. ❓ Switching files while in markdown mode
  6. ❓ Rapid view mode toggling
  7. ❓ Content with multiple consecutive blank lines survives round-trip

Recommendation: Add integration tests for the view mode switching logic, especially around normalization and dirty state tracking.


🔒 Security Assessment

No security concerns identified

  • URL sanitization remains intact in markdown.ts
  • No new user input handling
  • No new external data sources

📝 Code Style & Conventions

Minor: The comment on line 47-48 could be more concise:

// Don't use onContentLoaded - we'll handle normalization ourselves based on view mode

Could be:

// Disable automatic normalization - handled manually based on view mode

💡 Suggestions for Future Improvement

  1. Extract normalization logic into a separate utility function:

    function normalizeMarkdown(content: string): string {
      const md = createMarkdownParser();
      const turndown = createTurndownService();
      return turndown.turndown(md.render(content));
    }
  2. Add TypeScript strict null checks - Several places assume callbacks exist without checking (though typed as optional)

  3. Consider a view mode context - If more components need view mode, lifting to context would prevent prop drilling


🎯 Verdict

Conditional Approval - The core solution is sound, but there are implementation concerns:

Must Fix:

Should Fix:

Nice to Have:

  • Extract normalization to utility function
  • Add integration tests for edge cases

The PR solves a real UX problem and the architectural approach is correct. With the above fixes, this would be a solid improvement. Great work identifying and addressing the normalization issue! 🚀


Review conducted per CLAUDE.md conventions. Questions or concerns? Happy to discuss any of these points.

Remove all normalization on view mode switches and initial loads. Viewing in WYSIWYG mode is now completely non-destructive - files are converted to TipTap JSON for display only. Normalization only occurs when user actually edits in WYSIWYG mode (via onUpdate callback), ensuring intentional whitespace is preserved until user explicitly modifies content.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Code Review: Fix Markdown Whitespace Preservation

Summary

This PR successfully addresses the issue of markdown content normalization in markdown mode. The changes are well-focused and properly centralize normalization control.


✅ Strengths

1. Correct Problem Identification

  • The PR correctly identifies that the normalization pipeline (markdown-it → HTML → turndown) was being applied unnecessarily in markdown mode
  • The solution properly separates concerns: raw editing in markdown mode vs. normalized rendering in WYSIWYG mode

2. Minimal, Focused Changes

  • Only 8 additions / 27 deletions across 2 files
  • Removes unnecessary complexity from MarkdownEditor (no more normalization logic)
  • Centralizes normalization control in EditorComponent

3. Good Documentation

  • Clear comments explaining the behavior: "We do NOT normalize here - viewing in WYSIWYG is non-destructive"
  • PR description thoroughly explains the problem, solution, and expected results

⚠️ Potential Issues

1. Breaking Change in Content Flow ⚠️ HIGH PRIORITY

The change onContentLoaded: undefined in EditorComponent.tsx:48 disables a critical callback that was being used by App.tsx:

// App.tsx:340-348
const handleContentLoaded = useCallback((convertedContent: string) => {
  if (!fileContent.isDirty) {
    fileContent.updateOriginalContent(convertedContent);
    fileContent.updateContent(convertedContent);
  }
}, [fileContent]);

// App.tsx:613
<EditorComponent
  onContentLoaded={handleContentLoaded}
  ...
/>

Impact:

  • handleContentLoaded will never be called now, breaking the normalization flow for initial file loads
  • originalContent won't be updated with normalized content, potentially causing false dirty states
  • This callback was specifically designed to handle the case where files need normalization on initial load

Recommendation:
The PR description states "Disabled automatic onContentLoaded callback from useEditor hook" but this breaks the existing flow. You need to either:

  1. Option A (Recommended): Keep onContentLoaded but make the useEditor hook only call it when in WYSIWYG mode:

    // In useEditor.ts, only call onContentLoaded when needed
    if (onContentLoadedRef.current && shouldNormalize) {
      onContentLoadedRef.current(convertedMarkdown);
    }
  2. Option B: Remove the handleContentLoaded callback from App.tsx entirely and handle normalization differently. But this requires understanding the full cache validation flow.

  3. Option C: Pass view mode to useEditor and conditionally normalize based on that.

2. Inconsistent Initial Load Behavior ⚠️ MEDIUM PRIORITY

The new logic in EditorComponent.tsx:63-68 attempts to handle initial loads:

const isInitialLoad = prevViewModeRef.current === viewMode && !prevContentRef.current && content;

if (editor && content && (isViewModeTransition || isInitialLoad || ...)) {
  const json = markdownToJSON(content);
  editor.commands.setContent(json);
}

Issues:

  • isInitialLoad checks !prevContentRef.current but prevContentRef is initialized to content on line 42, so it's never empty on initial render
  • This means initial loads when already in WYSIWYG mode may not work correctly
  • The comment says "viewing in WYSIWYG is non-destructive" but the normalization still happens in the useEditor hook via the markdown → JSON → HTML → markdown pipeline

3. View Mode State Inconsistency ℹ️ LOW PRIORITY

When a user:

  1. Opens a file in markdown mode (sees raw content with multiple blank lines)
  2. Switches to WYSIWYG mode (content gets normalized, blank lines collapsed)
  3. Switches back to markdown mode

Current behavior: They'll see the normalized content, not the original raw content.

Question: Is this the intended behavior? The PR description suggests markdown mode should "display files exactly as they are on disk" but after switching to WYSIWYG, the content has been modified in memory.

Recommendation: Consider whether switching back to markdown mode should reload from disk (or cache of original raw content) vs. showing the normalized version.


🧪 Testing Concerns

Missing Test Coverage:

  • No tests added for the new behavior
  • Critical edge cases that should be tested:
    • Opening file with multiple blank lines in markdown mode → verify exact preservation
    • Opening file with multiple blank lines in WYSIWYG mode → verify normalization occurs
    • Switching modes back and forth → verify content consistency
    • Dirty state tracking with raw vs. normalized content
    • Cache validation with non-normalized content

Recommendation: Add tests to src/renderer/tests/ to cover these scenarios.


🔒 Security Considerations

✅ No security concerns identified. The URL sanitization in the markdown utils remains intact.


📊 Performance Considerations

Positive Impact: Removing the normalization from MarkdownEditor.handleBlur eliminates unnecessary markdown-it/turndown processing on every blur event in markdown mode.


🎯 Recommendations

Before Merging:

  1. Fix the onContentLoaded callback issue - This is a breaking change that needs to be addressed
  2. Fix the isInitialLoad logic - The condition will never be true as currently written
  3. Add tests for the new behavior, especially around:
    • Raw content preservation in markdown mode
    • Normalization in WYSIWYG mode
    • View mode switching
  4. Clarify intended behavior - Should markdown mode always show disk content, or can it show normalized content after WYSIWYG edits?
  5. Test the full user flow - Ensure cache validation, dirty state tracking, and file watching still work correctly with non-normalized content

Code Quality:

  • ✅ TypeScript types are correct
  • ✅ Code style follows project conventions
  • ✅ Comments are clear and helpful
  • ⚠️ Logic issues need addressing (see above)

📝 Summary

The intent of this PR is excellent - separating raw markdown editing from WYSIWYG normalization is the right approach. However, there are implementation issues that need to be resolved before merging:

  1. The disabled onContentLoaded callback breaks existing functionality
  2. The isInitialLoad logic has a bug that prevents it from working
  3. Test coverage is needed to ensure correctness

Recommendation: Request changes to fix issues #1 and #2, then add tests before merging.

@saadiq
Copy link
Copy Markdown
Owner Author

saadiq commented Oct 5, 2025

@claude address the high and medium priority recs.

For the View Mode State Inconsistency question, you are completely correct. if there hasn't been an edit in wysiwyg mode, the markdown view should reflect the unnormalized state of the file on the filesystem. No edits should have been made.

@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Claude finished @saadiq's task —— View job


✅ All High and Medium Priority Recommendations Addressed

Changes Summary

All issues fixed in commit 919766d:

  1. HIGH: onContentLoaded callback restored

    • Re-enabled with conditional WYSIWYG-only logic
    • Preserves normalization flow in App.tsx without affecting markdown mode
  2. HIGH: isInitialLoad logic removed

    • Replaced buggy condition with clear view mode transition detection
    • Simplified logic and removed edge cases
  3. MEDIUM: Removed unnecessary onUpdate dependency

    • onContentLoaded now works independently
    • No longer requires both callbacks to be present
  4. MEDIUM: View mode state preservation

    • hasWysiwygEditsRef: Tracks if user made edits in WYSIWYG mode
    • rawContentRef: Stores unnormalized content from file
    • Switching WYSIWYG → markdown without edits: restores raw unnormalized content
    • Switching WYSIWYG → markdown with edits: keeps normalized content
  5. useEffect dependency issues fixed

    • Used stable refs for callbacks with useCallback wrappers
    • Prevents infinite loops and unnecessary re-renders

How It Works Now

Load file in markdown mode
  ↓
Shows raw content exactly as on disk
  ↓
Switch to WYSIWYG
  ↓
Normalizes and renders (onContentLoaded called)
  ↓
┌─────────────────────────────┐
│ Make NO edits in WYSIWYG?   │
├─────────────────────────────┤
│ YES → Switch to markdown    │
│       → Raw content restored│
│                             │
│ NO  → Switch to markdown    │
│       → Normalized kept     │
└─────────────────────────────┘

Testing Recommendation

The user should test:

  • Loading a file with multiple blank lines in markdown mode (should show exact raw content)
  • Switching to WYSIWYG without editing, then back to markdown (raw content restored)
  • Making edits in WYSIWYG, then switching to markdown (normalized content kept)

Job Run • Branch: saadiq/fix-markdown-whitespace

- Re-enable onContentLoaded with conditional WYSIWYG-only logic
- Track WYSIWYG edits to preserve unnormalized content when viewing
- Remove buggy isInitialLoad logic in favor of clear view transitions
- Use stable callback refs to prevent useEffect dependency issues
- Restore raw content when switching to markdown if no edits made

This ensures markdown mode always shows raw file content unless
explicit edits were made in WYSIWYG mode.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-authored-by: Saadiq Rodgers-King <saadiq@users.noreply.github.com>
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Code Review

Thank you for this PR! The changes effectively address the markdown normalization issue. Here's my detailed feedback:

Strengths

  1. Clear Problem/Solution: The PR description clearly articulates the issue and the fix
  2. Logical Approach: Centralizing normalization control in EditorComponent is the right architectural choice
  3. Simplification: Removing normalization from MarkdownEditor makes it a proper controlled component
  4. Smart Edit Tracking: Using hasWysiwygEditsRef to determine whether to restore raw content is clever

🐛 Potential Issues

1. Race Condition with rawContentRef Updates (High Priority)

Location: EditorComponent.tsx:98-101

if (hasContentChanged) {
  hasWysiwygEditsRef.current = false;
  rawContentRef.current = content;
}

Problem: When switching from WYSIWYG → markdown (line 110-116), you restore rawContentRef.current, but this ref was updated to the current normalized content when hasContentChanged was true. This means:

  • User opens file with extra whitespace → rawContentRef = raw content ✓
  • User switches to WYSIWYG → content gets normalized → parent calls onContentLoaded with normalized content → parent re-renders with normalized content as content prop → hasContentChanged = true → rawContentRef = normalized content ✗
  • User switches back to markdown without edits → restores rawContentRef (now normalized) instead of original raw content ✗

Suggested Fix: Only update rawContentRef when content changes from an external source (file load), not from internal normalization:

// Add ref to track if we triggered the content change ourselves
const selfTriggeredChangeRef = useRef(false);

// In wrappedOnContentLoaded:
const wrappedOnContentLoaded = useCallback((convertedContent: string) => {
  if (viewModeRef.current === 'wysiwyg') {
    selfTriggeredChangeRef.current = true;
    onContentLoadedRef.current?.(convertedContent);
  }
}, []);

// In the sync effect:
if (hasContentChanged) {
  hasWysiwygEditsRef.current = false;
  // Only update rawContentRef if this is NOT a self-triggered change
  if (!selfTriggeredChangeRef.current) {
    rawContentRef.current = content;
  }
  selfTriggeredChangeRef.current = false;
}

2. Initial Load in WYSIWYG Mode (Medium Priority)

Location: EditorComponent.tsx:103-122

When a file is loaded while already in WYSIWYG mode, the rawContentRef is set to the new content (line 100), but this happens in the same effect that syncs to the editor (line 118-121). The sequence is:

  1. rawContentRef.current = content (unnormalized)
  2. Content gets converted and set in editor
  3. onContentLoaded is called with normalized content
  4. Parent updates content prop with normalized content
  5. On next render, rawContentRef gets overwritten with normalized content

This creates the same issue as #1 above.

3. Missing Dependency in useEffect (Low Priority)

Location: EditorComponent.tsx:93-126

The effect at line 93 uses onUpdateRef.current in the callback (line 113) but doesn't include it as a dependency. While it works because you're using a ref, it's better to avoid using callback refs inside effects when possible, or add a comment explaining why it's safe.

🎯 Code Quality Suggestions

1. Extract Complex Logic (Medium Priority)

The sync effect (lines 93-126) is doing a lot. Consider extracting the view mode transition logic into separate functions:

const handleMarkdownToWysiwyg = useCallback(() => {
  if (!editor || !content) return;
  const json = markdownToJSON(content);
  editor.commands.setContent(json);
}, [editor, content]);

const handleWysiwygToMarkdown = useCallback(() => {
  if (!hasWysiwygEditsRef.current && onUpdateRef.current) {
    onUpdateRef.current(rawContentRef.current);
  }
}, []);

2. Add TypeScript Types (Low Priority)

Consider creating a type for the ref object to make the intent clearer:

interface EditorState {
  hasWysiwygEdits: boolean;
  rawContent: string;
  prevContent: string;
  prevViewMode: ViewMode;
}

🧪 Test Coverage Concerns (High Priority)

No test files found for EditorComponent or MarkdownEditor. Given the complexity of view mode switching and content synchronization, this PR would greatly benefit from tests covering:

  1. Markdown mode preserves raw content - Load file with multiple blank lines, verify they're preserved
  2. WYSIWYG mode normalizes content - Switch to WYSIWYG, verify content is normalized
  3. Switching back without edits restores raw - Open file, switch WYSIWYG → markdown without editing, verify raw content restored
  4. Switching back with edits keeps normalized - Edit in WYSIWYG, switch to markdown, verify normalized content kept
  5. External content updates reset state - Load new file, verify hasWysiwygEdits resets

Recommendation: Add integration tests before merging to prevent regressions.

🔒 Security Considerations

No security issues identified. The URL sanitization logic (urlSanitizer.ts) is handled elsewhere and not affected by these changes.

Performance Considerations

  • The additional refs and callback wrappers have negligible performance impact
  • Content hashing in useEditor prevents unnecessary re-renders (good)
  • No performance concerns

📋 Summary

Critical: Fix the rawContentRef update logic to prevent it from being overwritten with normalized content during view mode transitions.

Recommended:

  • Add comprehensive test coverage for view mode switching behavior
  • Extract complex sync logic into smaller, testable functions
  • Add code comments explaining the view mode transition state machine

The core idea of this PR is sound, but the implementation has a subtle bug that will likely manifest when users switch between view modes. With the suggested fixes and test coverage, this will be a solid improvement.


🤖 Generated with Claude Code

…prehensive tests

Address code review feedback with the following improvements:

- Fix race condition with rawContentRef by introducing selfTriggeredChangeRef flag
  to distinguish between external content changes (file loads) and self-triggered
  normalization updates, preventing raw content from being overwritten

- Refactor complex view mode sync logic into separate handler functions for better
  maintainability:
  - handleMarkdownToWysiwyg: Manages markdown → WYSIWYG transitions
  - handleWysiwygToMarkdown: Manages WYSIWYG → markdown transitions
  - handleContentChangeInWysiwyg: Handles content updates in WYSIWYG mode

- Add comprehensive test suite (473 lines) covering:
  - Raw markdown content preservation in markdown mode
  - Content normalization in WYSIWYG mode
  - View mode transitions and content restoration
  - Race condition prevention with selfTriggeredChangeRef
  - Content updates and edit tracking
  - Edge cases (empty content, undefined callbacks, localStorage)
  - Keyboard shortcuts in different view modes

This refactoring improves code organization, prevents subtle timing bugs, and
provides test coverage for the markdown whitespace preservation feature.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Code Review: Fix Markdown Whitespace Preservation

Summary

This PR successfully addresses the markdown normalization issue by centralizing content control and preserving raw markdown in markdown mode. The implementation is well-thought-out with comprehensive test coverage.

✅ Strengths

Architecture & Design

  • Excellent separation of concerns: Raw content preservation logic is centralized in EditorComponent, keeping MarkdownEditor simple and focused
  • Smart content flow control: The wrappedOnContentLoaded callback conditionally triggers normalization only in WYSIWYG mode
  • Race condition prevention: selfTriggeredChangeRef prevents the raw content from being overwritten during self-triggered normalization updates
  • Clean state management: Multiple refs (hasWysiwygEditsRef, rawContentRef, selfTriggeredChangeRef) work together elegantly to track edit state

Code Quality

  • Comprehensive test coverage: 473 lines of well-structured tests covering edge cases, race conditions, and view mode transitions
  • Clear documentation: Excellent inline comments explaining the purpose of refs and state tracking
  • Type safety: Proper TypeScript usage throughout with no type compromises

UX Improvements

  • Solves the core problem: Files now display exactly as they appear on disk in markdown mode
  • No false dirty states: Content only changes when explicitly switching to WYSIWYG mode
  • Preserves user intent: Restores raw content when switching back to markdown without edits

🔍 Areas for Consideration

1. Potential Memory Concerns with rawContentRef

Location: EditorComponent.tsx:48

The rawContentRef stores the entire raw markdown content. For very large files (>5MB), this doubles memory usage since both the editor and the ref hold the content.

Suggestion: Consider documenting this trade-off or adding a size threshold check:

// Only store raw content for files under 5MB to avoid memory issues
if (content.length < 5 * 1024 * 1024) {
  rawContentRef.current = content;
}

2. useCallback Dependency Arrays

Location: EditorComponent.tsx:98-102, 105-111, 114-118

The handlers handleMarkdownToWysiwyg, handleWysiwygToMarkdown, and handleContentChangeInWysiwyg have content in their dependency arrays. Since content changes frequently, these callbacks recreate on every content update, which may be unnecessary.

Current behavior:

const handleMarkdownToWysiwyg = useCallback(() => {
  if (!editor || !content) return;
  const json = markdownToJSON(content);
  editor.commands.setContent(json);
}, [editor, content]); // Recreates on every content change

Suggestion: Consider using refs for content to stabilize callbacks:

const contentRef = useRef(content);
useEffect(() => { contentRef.current = content; }, [content]);

const handleMarkdownToWysiwyg = useCallback(() => {
  if (!editor || !contentRef.current) return;
  const json = markdownToJSON(contentRef.current);
  editor.commands.setContent(json);
}, [editor]); // Only recreates when editor changes

This is a minor optimization and the current implementation is functionally correct.

3. Edge Case: Multiple Rapid View Switches

Location: EditorComponent.tsx:121-156

If a user rapidly switches between views while content is loading/normalizing, there's a potential for the selfTriggeredChangeRef flag to get out of sync.

Scenario:

  1. User loads file in WYSIWYG (normalization starts)
  2. User quickly switches to markdown before normalization completes
  3. Normalization callback fires after switch

Current protection: The viewModeRef.current === 'wysiwyg' check in wrappedOnContentLoaded prevents this, which is good.

Suggestion: Add a comment explaining this edge case protection:

// Only call parent's onContentLoaded when in WYSIWYG mode
// This protects against race conditions if user switches views during normalization
if (viewModeRef.current === 'wysiwyg') {

4. Test Coverage: Integration Testing

Location: EditorComponent.test.tsx

The tests are excellent but heavily mocked. Consider adding a few integration tests that verify the actual markdown normalization behavior with real content.

Suggestion: Add tests like:

it('should preserve triple blank lines in markdown mode', async () => {
  const content = 'Line 1\n\n\n\nLine 2';
  render(<EditorComponent content={content} />);
  const textarea = screen.getByRole('textbox');
  expect(textarea.value).toBe(content); // Exact match
});

5. MarkdownEditor Simplification

Location: MarkdownEditor.tsx:20-22

The useEffect syncing external content to local state may cause unnecessary re-renders.

Current:

useEffect(() => {
  setLocalContent(content);
}, [content]);

Potential issue: If content prop changes frequently (e.g., during auto-save), this triggers state updates even if the user is typing.

Suggestion: Add a check to only update if different from local edits:

const isExternalChange = useRef(true);

useEffect(() => {
  if (isExternalChange.current && content !== localContent) {
    setLocalContent(content);
  }
  isExternalChange.current = true;
}, [content]);

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
  isExternalChange.current = false;
  const newValue = e.target.value;
  setLocalContent(newValue);
  onUpdate(newValue);
};

This prevents the cursor from jumping during typing if external updates come in.

🔒 Security

✅ No security concerns. The changes don't touch URL sanitization or introduce new user input handling.

⚡ Performance

✅ Performance impact is minimal:

  • Storing raw content in a ref is O(1) memory overhead
  • Callback recreations are negligible for normal file sizes
  • No additional render cycles introduced

🧪 Testing

✅ Excellent test coverage with 473 lines covering:

  • Raw content preservation
  • WYSIWYG normalization
  • View mode transitions
  • Race condition prevention
  • Edge cases (empty content, undefined callbacks)

Minor suggestion: Consider testing with actual large content (>1000 lines) to verify performance claims in CLAUDE.md.

📝 Code Style & Conventions

✅ Follows project conventions from CLAUDE.md:

  • Proper use of refs for race condition prevention
  • Clear separation between markdown and WYSIWYG modes
  • Consistent with existing patterns in the codebase

🎯 Recommendation

APPROVE with minor suggestions

This is a well-designed solution to a tricky problem. The core logic is sound, the implementation is clean, and the test coverage is comprehensive. The suggestions above are minor optimizations and edge case considerations that don't block merging.

The PR successfully achieves its goals:

  • ✅ Raw markdown content preserved in markdown mode
  • ✅ WYSIWYG mode still normalizes correctly
  • ✅ No false dirty states
  • ✅ Good user experience

Great work! 🚀

Add isExternalChangeRef to distinguish between user typing and external content updates. Only sync external changes to local state if they differ from current local content and aren't from user input. This prevents cursor position jumps that could occur if external updates (e.g., auto-save callbacks) trigger re-renders while user is typing.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

PR Review: Fix Preserve Raw Markdown Content in Markdown Mode

Summary

This PR successfully addresses the issue of markdown content being normalized when viewing files in markdown mode. The implementation centralizes normalization control to EditorComponent and removes normalization from MarkdownEditor.

Code Quality & Best Practices

✅ Strengths

  1. Clear separation of concerns: The changes properly separate raw markdown display from WYSIWYG normalization
  2. Well-documented code: Excellent inline comments explaining the purpose of each ref and callback
  3. Comprehensive test coverage: 473 lines of tests covering edge cases, race conditions, and view mode transitions
  4. Race condition prevention: The selfTriggeredChangeRef mechanism is clever and prevents a tricky bug where normalized content would overwrite raw content
  5. Performance consideration: Uses callback refs (onUpdateRef, onContentLoadedRef) to avoid unnecessary effect dependencies

🔍 Potential Issues & Concerns

1. Complex State Management with Multiple Refs (Medium Priority)

The component now manages 5+ refs tracking various states:

  • hasWysiwygEditsRef
  • rawContentRef
  • selfTriggeredChangeRef
  • prevViewModeRef
  • prevContentRef

Issue: This many refs can make the component harder to reason about and maintain. The interaction between selfTriggeredChangeRef and hasContentChanged is particularly subtle.

Suggestion: Consider adding a diagram or expanded comment block at the top of the component explaining the state flow and ref interactions:

/**
 * Content Flow & State Management:
 * 
 * 1. File Load:
 *    - rawContentRef stores original content
 *    - hasWysiwygEditsRef reset to false
 * 
 * 2. Markdown Mode:
 *    - Display rawContentRef as-is
 *    - No normalization
 * 
 * 3. Switch to WYSIWYG:
 *    - Content normalized via markdownToJSON
 *    - onContentLoaded triggers
 *    - selfTriggeredChangeRef prevents overwriting rawContentRef
 * 
 * 4. Edit in WYSIWYG:
 *    - hasWysiwygEditsRef = true
 *    - Content updates flow normally
 * 
 * 5. Switch back to Markdown:
 *    - IF hasWysiwygEditsRef: keep normalized content
 *    - ELSE: restore rawContentRef
 */

2. Potential Bug in MarkdownEditor (High Priority)

In MarkdownEditor.tsx lines 25-31:

useEffect(() => {
  if (isExternalChangeRef.current && content !== localContent) {
    setLocalContent(content);
  }
  // Reset flag after each content prop change
  isExternalChangeRef.current = true;
}, [content, localContent]);

Issue: localContent is in the dependency array, which means this effect runs every time the user types (since handleChange updates localContent). However, the condition isExternalChangeRef.current will be false when the user is typing, so it won't update. But adding localContent as a dependency creates unnecessary effect executions.

Suggestion: Remove localContent from the dependency array since the check content !== localContent will still work correctly:

useEffect(() => {
  if (isExternalChangeRef.current && content !== localContent) {
    setLocalContent(content);
  }
  isExternalChangeRef.current = true;
}, [content]); // Remove localContent from deps

3. Race Condition Edge Case (Low Priority)

In EditorComponent.tsx lines 127-137, there's a potential edge case:

if (hasContentChanged) {
  hasWysiwygEditsRef.current = false;
  
  if (!selfTriggeredChangeRef.current) {
    rawContentRef.current = content;
  }
  
  selfTriggeredChangeRef.current = false;
}

Issue: If two rapid content changes occur (external file change + normalization callback), the second change might incorrectly update rawContentRef with normalized content.

Suggestion: Consider using a more robust tracking mechanism, perhaps with a timestamp or content hash to identify self-triggered changes more reliably.

4. Missing Null Check (Low Priority)

In handleWysiwygToMarkdown (lines 105-111), there's no check if rawContentRef.current exists before calling onUpdateRef.current:

const handleWysiwygToMarkdown = useCallback(() => {
  if (!hasWysiwygEditsRef.current && onUpdateRef.current) {
    onUpdateRef.current(rawContentRef.current);
  }
}, []);

Suggestion: Add a null/undefined check:

if (!hasWysiwygEditsRef.current && onUpdateRef.current && rawContentRef.current !== undefined) {
  onUpdateRef.current(rawContentRef.current);
}

Security Concerns

No security issues identified. The changes:

  • Don't introduce new XSS vectors (existing urlSanitizer still applies)
  • Don't expose sensitive data
  • Don't modify file system operations

Performance Considerations

✅ Good Performance Practices

  1. Memoized callbacks: useCallback prevents unnecessary re-renders
  2. Ref-based tracking: Avoids state updates and re-renders for tracking flags
  3. Conditional normalization: Only normalizes when switching to WYSIWYG

⚠️ Minor Concerns

  1. Multiple useEffect dependencies: The main sync effect (lines 121-156) has 4 dependencies that could trigger re-renders. Consider splitting into multiple effects if performance issues arise.

Test Coverage

✅ Excellent Test Suite

The test file is comprehensive with:

  • Raw content preservation tests
  • View mode transition tests
  • Race condition prevention tests
  • Edge case handling (empty content, undefined callbacks)
  • Keyboard shortcut isolation by view mode

📝 Suggestions for Additional Tests

  1. Test rapid view mode switching: Switch markdown → WYSIWYG → markdown quickly to verify no race conditions
  2. Test with large files: Verify performance with 1000+ line markdown files
  3. Test content with special characters: Ensure Unicode, emojis, etc. are preserved correctly
  4. Integration test: Test actual file load → view → edit → save workflow

Architecture & Design

✅ Follows Project Conventions

  • Uses TypeScript properly with type safety
  • Follows React best practices (hooks, controlled components)
  • Maintains existing IPC architecture (no changes to main process)
  • Consistent with CLAUDE.md guidelines

💡 Design Improvement Suggestions

  1. Consider extracting view mode logic: The view mode switching logic could be extracted to a custom hook like useViewModeSync to simplify EditorComponent
  2. Document the state machine: The view mode transitions form a state machine. Consider adding a state diagram to the PR description or code comments

Breaking Changes

No breaking changes: The API of EditorComponent and MarkdownEditor remains unchanged.

Recommendations

Must Fix (Before Merge)

  1. ✅ Fix the MarkdownEditor useEffect dependency array issue (remove localContent)

Should Fix (Before or Shortly After Merge)

  1. Add comprehensive documentation comment explaining the ref interactions
  2. Add null check in handleWysiwygToMarkdown

Nice to Have (Future Improvement)

  1. Consider extracting view mode sync logic to custom hook
  2. Add integration tests for full file workflow
  3. Monitor for race condition edge cases in production

Overall Assessment

Score: 8.5/10

This is a solid PR that solves a real user pain point. The implementation is thoughtful with good race condition prevention. The test coverage is excellent. The main concern is code complexity - 5+ refs make the component harder to maintain. The MarkdownEditor dependency array issue should be fixed before merge.

The PR successfully achieves its goal: markdown mode now preserves raw content while WYSIWYG mode handles normalization intelligently.

Approval Status

Approved with minor changes requested

Great work on this fix! The approach is sound and the testing is thorough. Just address the dependency array issue and consider adding more documentation about the ref interactions.

saadiq added 2 commits October 5, 2025 11:20
Add detailed JSDoc block explaining the complex state management in EditorComponent, including:
- Purpose and lifecycle of each ref (rawContentRef, hasWysiwygEditsRef, selfTriggeredChangeRef, etc.)
- 7 detailed scenarios showing state transitions
- Race conditions and how they're handled
- Key design decisions

This addresses code review feedback about the component's complexity and makes the interaction between multiple refs easier to understand and maintain.
Remove localContent from dependency array to prevent unnecessary effect executions on every keystroke. The comparison 'content !== localContent' uses closure to capture the current localContent value, which is sufficient for detecting external updates.

This fixes a performance issue where the effect would run on every user keystroke even though the condition would fail (isExternalChangeRef.current = false during typing).
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Code Review: Preserve Raw Markdown Content in Markdown Mode

I've reviewed this PR and have feedback on code quality, potential issues, and improvements. Overall, this is a well-thought-out solution to a complex problem with excellent documentation.

Strengths

  1. Excellent Documentation: The 100-line JSDoc block in EditorComponent (lines 18-113) is outstanding
  2. Comprehensive Test Coverage: The 473-line test suite covers all critical scenarios
  3. Good Separation of Concerns: Moving normalization control from MarkdownEditor to EditorComponent is the right choice
  4. Race Condition Handling: The selfTriggeredChangeRef pattern cleverly solves the echo update problem

Critical Issues

1. Dangerous ESLint Disable in MarkdownEditor (lines 35-36)

The comment claims localContent is intentionally NOT in the dependency array and that the closure captures the value, but this is incorrect. The closure captures localContent at the time the effect is created, not the current value during execution. This creates a race condition where content updates may not be detected correctly.

Fix: Remove the eslint-disable and add localContent to the dependency array.

2. Memory Leak Risk with Callback Refs (lines 149-156)

The React-recommended pattern is to update refs synchronously without useEffect.

3. Inconsistent Error Handling

The code doesn't handle errors in markdownToJSON conversions. Consider adding try-catch with toast notifications.

Performance Considerations

4. Excessive Effect Dependencies (line 252)

The three handler functions are recreated whenever editor or content changes. Consider inlining handlers inside the effect.

Test Coverage Gaps

5. Missing Test Scenarios

  • Concurrent mode switches
  • Large documents (10000+ lines)
  • Error scenarios
  • Special characters (Unicode, emojis, RTL)

Recommended Priority

Must Fix Before Merge:

  1. Fix the ESLint disable in MarkdownEditor - this is a bug
  2. Fix callback ref pattern
  3. Add error handling

Should Fix:
4. Add missing test cases
5. Inline handlers to reduce effect dependencies

Overall Assessment

This PR demonstrates strong engineering with excellent documentation, comprehensive tests, and thoughtful handling of complex state. However, the ESLint disable in MarkdownEditor is a critical bug that must be fixed before merge.

Recommendation: Request changes for Issue 1, then approve with the other suggestions as follow-up improvements.

saadiq added 3 commits October 5, 2025 11:34
Replace closure-based comparison with ref pattern to avoid stale closures. The previous implementation used localContent via closure, which could capture stale values. Now using localContentRef.current ensures we always compare against the current value.

This fixes the actual bug that the ESLint disable was masking.
…prehensive tests

Three key improvements:

1. Fix callback ref pattern: Remove unnecessary useEffect and update refs on every render (standard React pattern). This is more performant and simpler.

2. Add error handling: Wrap all markdownToJSON calls in try-catch blocks with fallback to plain text. This prevents crashes from malformed markdown.

3. Inline handlers: Remove useCallback wrappers and inline logic directly into main effect. This eliminates circular dependencies and reduces complexity.

Dependencies reduced from 6 to 3: [viewMode, editor, content]

Tests added:
- Error handling and recovery
- Callback ref updates and stale callback prevention
New test file for MarkdownEditor covering:
- Cursor jump prevention during typing
- External vs internal update handling
- isExternalChangeRef flag behavior
- Props and attributes
- Content synchronization
- Edge cases (empty, long, special characters)

Extended EditorComponent tests with:
- Error handling for markdown parsing failures
- Fallback behavior when conversion fails
- Error recovery and continued functionality
- Callback ref updates and stale callback prevention

Total test coverage expanded significantly for both critical components.
@claude
Copy link
Copy Markdown

claude Bot commented Oct 5, 2025

Pull Request Review: Fix Markdown Content Preservation

Summary

This PR successfully addresses the markdown normalization issue by centralizing content control and preventing unwanted whitespace changes. The implementation is solid with excellent test coverage.


✅ Strengths

1. Clear Problem Definition

The PR description clearly articulates the issue (unwanted normalization) and the solution (centralized control in EditorComponent).

2. Comprehensive Documentation

The 100+ line JSDoc comment in EditorComponent.tsx:18-112 is exceptional. It documents:

  • Each ref's purpose and lifecycle
  • 7 detailed state flow scenarios
  • Race condition handling
  • Key design decisions

This is exemplary documentation for complex state management.

3. Excellent Test Coverage

  • EditorComponent.test.tsx: 664 lines covering view transitions, race conditions, error handling
  • MarkdownEditor.test.tsx: 219 lines covering cursor jumps, external updates, edge cases
  • Tests are well-organized with descriptive names
  • Edge cases are thoroughly covered

4. Progressive Refinement

The commit history shows iterative improvement based on review feedback:

  • Initial fix → race condition handling → error handling → comprehensive tests
  • Shows good collaboration and responsiveness to feedback

🔍 Code Quality Observations

Architecture

Good separation of concerns: MarkdownEditor handles display, EditorComponent manages normalization logic
Non-destructive viewing: WYSIWYG mode doesn't modify content until user edits
User intent preservation: hasWysiwygEditsRef tracks whether to restore raw content

Implementation Details

EditorComponent.tsx (src/renderer/components/Editor/EditorComponent.tsx)

  1. Ref Pattern (lines 150-153)

    const onUpdateRef = useRef(onUpdate);
    const onContentLoadedRef = useRef(onContentLoaded);
    onUpdateRef.current = onUpdate;
    onContentLoadedRef.current = onContentLoaded;

    ✅ Standard React pattern for stable callbacks - well done

  2. Self-Triggered Change Prevention (lines 156-161)

    const wrappedOnContentLoaded = useCallback((convertedContent: string) => {
      if (viewModeRef.current === 'wysiwyg') {
        selfTriggeredChangeRef.current = true;
        onContentLoadedRef.current?.(convertedContent);
      }
    }, []);

    ✅ Clever use of flag to prevent echo updates

  3. Error Handling (lines 213-219, 233-239)

    try {
      const json = markdownToJSON(content);
      editor.commands.setContent(json);
    } catch (error) {
      console.error('Failed to convert markdown to JSON:', error);
      editor.commands.setContent(content);
    }

    ✅ Proper fallback behavior prevents crashes from malformed markdown

MarkdownEditor.tsx (src/renderer/components/Editor/MarkdownEditor.tsx)

  1. External Change Detection (lines 21-25)

    const isExternalChangeRef = useRef(true);
    const localContentRef = useRef(localContent);
    localContentRef.current = localContent;

    ✅ Prevents cursor jumps during typing - critical UX fix

  2. Simplified Logic (lines 28-35)
    Removed normalization entirely - markdown displays exactly as typed
    ✅ Simpler is better for this component's role


🐛 Potential Issues

1. Minor: Dependency Array Question (MarkdownEditor.tsx:29-35)

useEffect(() => {
  if (isExternalChangeRef.current && content !== localContentRef.current) {
    setLocalContent(content);
  }
  isExternalChangeRef.current = true;
}, [content]);

Question: Should localContentRef.current be in the dependency array?

The current implementation uses a ref to avoid stale closures, which is correct. However, the ESLint rule was disabled in an earlier version (commit 289b715e6bc27b0747f2029cb89033b950a2d146), then fixed by using the ref pattern.

Verdict: ✅ Current implementation is correct - no action needed.

2. Edge Case: Rapid File Switching

Scenario: User rapidly switches between files while in WYSIWYG mode

Current behavior:

  • Each file load sets selfTriggeredChangeRef = false
  • rawContentRef gets updated correctly ✅
  • hasWysiwygEditsRef resets on content change ✅

Verdict: ✅ Properly handled by existing refs

3. Edge Case: External File Modification

Scenario: File changes on disk while open in editor (via file watcher)

Current behavior (EditorComponent.tsx:197-203):

if (hasContentChanged) {
  hasWysiwygEditsRef.current = false;
  if (!selfTriggeredChangeRef.current) {
    rawContentRef.current = content;
  }
  selfTriggeredChangeRef.current = false;
}

Potential issue: If user has made WYSIWYG edits and the file changes externally, their edits are discarded without warning.

Recommendation: ⚠️ Consider adding a warning dialog if hasWysiwygEditsRef.current === true and external content change detected. This prevents data loss from race conditions between user edits and file watcher updates.

Impact: Low priority - rare edge case, but worth considering for future enhancement.


🔒 Security

No security concerns - changes are purely client-side state management
URL sanitization already exists in urlSanitizer.ts (per CLAUDE.md)
No new attack surfaces introduced


⚡ Performance

Good performance characteristics:

  • Removed unnecessary normalization on every keystroke
  • Ref-based updates avoid unnecessary re-renders
  • Error handling includes fallback to prevent UI freezing

Potential optimization (future):

  • For very large files (>5000 lines per CLAUDE.md), consider debouncing the markdownToJSON calls in the effect
  • Current implementation is fine for typical use cases

🧪 Test Coverage

Excellent Coverage ✅

  • EditorComponent: 664 lines of tests
  • MarkdownEditor: 219 lines of tests
  • Total: 883 lines of test code

Test Quality

✅ Tests are well-structured with clear describe blocks
✅ Edge cases covered (empty content, rapid switching, errors)
✅ Race conditions explicitly tested
✅ Mock setup is comprehensive

Suggestions for Additional Tests (optional)

  1. Integration test: End-to-end flow of opening file → editing in WYSIWYG → switching to markdown → saving
  2. File watcher race condition: Simulate external file change while user is typing in WYSIWYG
  3. Performance test: Large file (1000+ lines) switching between modes

Priority: Low - current coverage is already excellent


📋 Best Practices Alignment

Checking against CLAUDE.md guidelines:

TypeScript: Properly typed throughout
Path aliases: Uses @renderer/* correctly
Error handling: try/catch with fallbacks
Documentation: Excellent JSDoc comments
Testing: Uses bun test runner correctly
Code patterns: Follows established patterns for hooks and state management


🎯 Recommendations

Must Address

None - this PR is ready to merge as-is.

Should Consider (Future Enhancements)

  1. External file change conflict detection: Warn users if external changes conflict with unsaved WYSIWYG edits
  2. Performance monitoring: Add telemetry for large file conversion times
  3. User preference: Consider adding a "preserve raw formatting" toggle in settings

Nice to Have

  1. Add integration tests for complete user workflows
  2. Consider extracting ref management logic into a custom hook for reusability

📊 Final Verdict

Rating: ⭐⭐⭐⭐⭐ (5/5)

Recommendation: ✅ Approve and merge

This PR demonstrates:

  • Clear problem understanding
  • Well-architected solution
  • Excellent documentation
  • Comprehensive test coverage
  • Responsive iteration based on feedback

The implementation is production-ready. The only suggestion (external file change warning) is a nice-to-have for future work, not a blocker.

Great work on this fix! 🎉


📝 Minor Nits (optional cleanup)

  1. Line length: Some comments exceed 100 characters (e.g., EditorComponent.tsx:47-48). Consider wrapping for readability.
  2. Consistent terminology: "Raw content" vs "unnormalized content" - both are used. Pick one for consistency.

These are purely stylistic and don't affect functionality.

@saadiq saadiq merged commit fa27052 into main Oct 5, 2025
1 check passed
@saadiq saadiq deleted the saadiq/fix-markdown-whitespace branch October 5, 2025 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant