Skip to content

fix: capture editedRange via NSTextStorageDelegate for incremental highlighting#655

Merged
batonogov merged 2 commits intomainfrom
fix/yaml-highlight-disappears-649
Mar 29, 2026
Merged

fix: capture editedRange via NSTextStorageDelegate for incremental highlighting#655
batonogov merged 2 commits intomainfrom
fix/yaml-highlight-disappears-649

Conversation

@batonogov
Copy link
Copy Markdown
Owner

Summary

  • Fixes fix: syntax highlighting disappears during YAML editing #649 — syntax highlighting disappears when editing YAML (and other files)
  • Root cause: NSTextStorage.editedRange resets to NSNotFound after processEditing(), but textDidChange fires after processEditing. So editedRange was always nil, forcing every keystroke to use full re-highlight instead of incremental highlightEditedAsync
  • For files >50KB (viewport-based highlighting), this caused visible highlight disappearance during editing
  • Fix: Coordinator now implements NSTextStorageDelegate and captures editedRange in textStorage(_:didProcessEditing:range:changeInLength:) before it resets. textDidChange consumes this pendingEditedRange for proper incremental highlighting

Test plan

  • pendingEditedRange_capturedByTextStorageDelegate — verifies delegate captures range on character edits
  • pendingEditedRange_consumedByTextDidChange — verifies textDidChange consumes and clears the range
  • pendingEditedRange_clearedOnProgrammaticTextChange — verifies range is cleared during programmatic text replacement
  • pendingEditedRange_notSetForAttributeOnlyEdits — verifies attribute-only edits (highlighting) don't set the range
  • All existing CodeEditorCoordinatorTests pass
  • All HighlightPersistenceTests pass
  • SwiftLint clean

…mental highlighting (#649)

NSTextStorage.editedRange resets to NSNotFound after processEditing(),
but textDidChange fires after processEditing. This meant editedRange was
always nil in textDidChange, causing every keystroke to trigger a full
re-highlight instead of incremental. For large files (>50KB) this caused
visible highlight disappearance during editing.

Fix: add NSTextStorageDelegate to Coordinator that captures editedRange
in textStorage(_:didProcessEditing:range:changeInLength:) before it
resets. textDidChange now consumes this pendingEditedRange for proper
incremental highlighting via highlightEditedAsync.
Copy link
Copy Markdown
Owner Author

@batonogov batonogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REQUEST CHANGES (self-review)

1. lineStartsCache тоже нуждается в pendingEditedRangependingChangeInLength)

PR правильно идентифицирует root cause: NSTextStorage.editedRange уже NSNotFound к моменту textDidChange. Но блок lineStartsCache (строки 1025-1036 в main) также читает storage.editedRange и storage.changeInLength напрямую:

if var cache = lineStartsCache,
   let storage = textView.textStorage,
   storage.editedRange.location != NSNotFound {  // ← всегда NSNotFound!
    cache.update(
        editedRange: storage.editedRange,
        changeInLength: storage.changeInLength,
        ...

После этого PR инкрементальное обновление lineStartsCache никогда не сработает — всегда будет full rebuild. Технически этот баг существовал и до PR, но раз мы чиним root cause, надо чинить всё.

Fix: Захватывать changeInLength вместе с editedRange в delegate:

var pendingEditedRange: NSRange?
var pendingChangeInLength: Int = 0

func textStorage(_ textStorage: NSTextStorage,
                 didProcessEditing editedMask: NSTextStorageEditActions,
                 range editedRange: NSRange, changeInLength delta: Int) {
    if editedMask.contains(.editedCharacters), !isProgrammaticTextChange {
        pendingEditedRange = editedRange
        pendingChangeInLength = delta
    }
}

И использовать pendingEditedRange / pendingChangeInLength в блоке lineStartsCache:

if var cache = lineStartsCache, let er = pendingEditedRange {
    cache.update(editedRange: er, changeInLength: pendingChangeInLength,
                 in: textView.string as NSString)
    lineStartsCache = cache
} else {
    lineStartsCache = LineStartsCache(text: textView.string)
}

2. Конфликт с #653 (undo fix)

PR #653 вставляет isUndoRedoInProgress и значительно модифицирует textDidChange. Оба PR трогают один участок CodeEditorView.swift. Мерж-конфликт гарантирован — кто мержится вторым, разрешает руками. Не блокер, но учитывай порядок.

3. Всё остальное — ОК

  • NSTextStorageDelegate реализован корректно: didProcessEditing — правильный callback
  • editedMask.contains(.editedCharacters) правильно отсекает attribute-only edits
  • isProgrammaticTextChange guard корректен
  • pendingEditedRange = nil сбрасывается в обоих путях — утечки нет
  • textStorage.delegate ставится после настройки текста
  • Тесты покрывают 4 кейса: capture, consume, programmatic clear, attribute-only skip

Вердикт: REQUEST CHANGES — исправить п.1 (lineStartsCache + changeInLength), остальное LGTM.

… update

storage.editedRange is already reset to NSNotFound by the time
textDidChange fires. Capture changeInLength alongside editedRange
in NSTextStorageDelegate and use both pending values for the
incremental lineStartsCache.update() call.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 29, 2026

✅ Code Coverage: 71.6%

Threshold: 70%

Coverage is above the minimum threshold.

Generated by CI — see job summary for detailed file-level breakdown.

Copy link
Copy Markdown
Owner Author

@batonogov batonogov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE. pendingChangeInLength захватывается в NSTextStorageDelegate вместе с pendingEditedRange. lineStartsCache корректно использует pending-значения вместо storage.editedRange. Сброс в 3 местах (programmatic updateContentIfNeeded, programmatic textDidChange, нормальный textDidChange). 4 теста покрывают все пути.

@batonogov batonogov merged commit bb452b2 into main Mar 29, 2026
14 checks passed
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.

fix: syntax highlighting disappears during YAML editing

1 participant