Skip to content

Commit 5b8e8fb

Browse files
objectiveousclaude
andcommitted
Fix down arrow key not working after font size change
The cached _estimateLineHeight in TextLayoutManager was never invalidated when the font changed. The vertical cursor movement calculation uses this estimate to compute the target y-coordinate, and after enough font size increases the stale (too small) value prevented moveDown: from crossing into the next line. Up arrow was unaffected because subtracting from the line top always lands in the previous line. Fix: re-assign renderDelegate after font/lineHeight changes to trigger its didSet which clears the cached estimate. Also handle arrow keys explicitly in the event monitor and add Ctrl+N/P (moveDown/moveUp) to handleCommand for robustness. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1fa4d3c commit 5b8e8fb

File tree

2 files changed

+66
-1
lines changed

2 files changed

+66
-1
lines changed

Sources/CodeEditSourceEditor/Controller/TextViewController+Lifecycle.swift

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,12 @@ extension TextViewController {
209209
switch event.type {
210210
case .keyDown:
211211
let tabKey: UInt16 = 0x30
212+
let downArrow: UInt16 = 125
213+
let upArrow: UInt16 = 126
212214

213-
if event.keyCode == tabKey {
215+
if event.keyCode == downArrow || event.keyCode == upArrow {
216+
return self.handleArrowKey(event: event, modifierFlags: modifierFlags)
217+
} else if event.keyCode == tabKey {
214218
return self.handleTab(event: event, modifierFlags: modifierFlags.rawValue)
215219
} else {
216220
return self.handleCommand(event: event, modifierFlags: modifierFlags)
@@ -276,11 +280,64 @@ extension TextViewController {
276280
}
277281
jumpToDefinitionModel.performJump(at: cursor.range)
278282
return nil
283+
case (controlKey, "n"):
284+
self.textView.moveDown(nil)
285+
return nil
286+
case (controlKey, "p"):
287+
self.textView.moveUp(nil)
288+
return nil
279289
case (_, _):
280290
return event
281291
}
282292
}
283293

294+
/// Handles up/down arrow key events with all modifier combinations.
295+
/// Dispatches the appropriate movement method on the text view and consumes the event.
296+
///
297+
/// - Returns: `nil` to consume the event after dispatching the movement action.
298+
private func handleArrowKey(event: NSEvent, modifierFlags: NSEvent.ModifierFlags) -> NSEvent? {
299+
let isDown = event.keyCode == 125
300+
let shift = modifierFlags.contains(.shift)
301+
let option = modifierFlags.contains(.option)
302+
let command = modifierFlags.contains(.command)
303+
304+
switch (isDown, shift, option, command) {
305+
// Plain arrow
306+
case (true, false, false, false):
307+
self.textView.moveDown(nil)
308+
case (false, false, false, false):
309+
self.textView.moveUp(nil)
310+
// Shift+Arrow (extend selection)
311+
case (true, true, false, false):
312+
self.textView.moveDownAndModifySelection(nil)
313+
case (false, true, false, false):
314+
self.textView.moveUpAndModifySelection(nil)
315+
// Option+Arrow (paragraph)
316+
case (true, false, true, false):
317+
self.textView.moveToEndOfParagraph(nil)
318+
case (false, false, true, false):
319+
self.textView.moveToBeginningOfParagraph(nil)
320+
// Cmd+Arrow (document)
321+
case (true, false, false, true):
322+
self.textView.moveToEndOfDocument(nil)
323+
case (false, false, false, true):
324+
self.textView.moveToBeginningOfDocument(nil)
325+
// Shift+Option+Arrow (extend selection to paragraph)
326+
case (true, true, true, false):
327+
self.textView.moveToEndOfParagraphAndModifySelection(nil)
328+
case (false, true, true, false):
329+
self.textView.moveToBeginningOfParagraphAndModifySelection(nil)
330+
// Shift+Cmd+Arrow (extend selection to document)
331+
case (true, true, false, true):
332+
self.textView.moveToEndOfDocumentAndModifySelection(nil)
333+
case (false, true, false, true):
334+
self.textView.moveToBeginningOfDocumentAndModifySelection(nil)
335+
default:
336+
return event
337+
}
338+
return nil
339+
}
340+
284341
/// Handles the tab key event.
285342
/// If the Shift key is pressed, it handles unindenting. If no modifier key is pressed, it checks if multiple lines
286343
/// are highlighted and handles indenting accordingly.

Sources/CodeEditSourceEditor/SourceEditorConfiguration/SourceEditorConfiguration+Appearance.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ extension SourceEditorConfiguration {
8787
controller.textView.font = font
8888
controller.textView.typingAttributes = controller.attributesFor(nil)
8989
controller.gutterView.font = font.rulerFont
90+
// Force the layout manager to recalculate its cached estimated line height.
91+
// The estimate is cached and not invalidated by font changes, causing vertical
92+
// cursor movement (moveDown:) to use stale values and fail to cross line boundaries.
93+
let renderDelegate = controller.textView.layoutManager.renderDelegate
94+
controller.textView.layoutManager.renderDelegate = renderDelegate
9095
needsHighlighterInvalidation = true
9196
}
9297

@@ -103,6 +108,9 @@ extension SourceEditorConfiguration {
103108

104109
if oldConfig?.lineHeightMultiple != lineHeightMultiple {
105110
controller.textView.layoutManager.lineHeightMultiplier = lineHeightMultiple
111+
// Also invalidate the cached estimated line height (same issue as font change above).
112+
let renderDelegate = controller.textView.layoutManager.renderDelegate
113+
controller.textView.layoutManager.renderDelegate = renderDelegate
106114
}
107115

108116
if oldConfig?.wrapLines != wrapLines {

0 commit comments

Comments
 (0)