Skip to content

Commit 3335aee

Browse files
objectiveousclaude
andcommitted
Address PR #364 review comments from Copilot
- Extract renderDelegate cache invalidation into refreshEstimatedLineHeightCache() helper - Normalize modifier flags to [shift, control, option, command] so Ctrl-N/P work with Caps Lock/Fn - Fix handleArrowKey doc comment to accurately describe supported modifier combinations - Centralize key code constants (tab, downArrow, upArrow) as static properties Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 5b8e8fb commit 3335aee

File tree

3 files changed

+46
-17
lines changed

3 files changed

+46
-17
lines changed

Sources/CodeEditSourceEditor/Controller/TextViewController+Lifecycle.swift

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -204,17 +204,25 @@ extension TextViewController {
204204
}
205205
}
206206

207+
// MARK: - Key Codes
208+
209+
static let tabKeyCode: UInt16 = 0x30
210+
static let downArrowKeyCode: UInt16 = 125
211+
static let upArrowKeyCode: UInt16 = 126
212+
213+
/// The set of modifier flags relevant to key binding matching.
214+
/// Masks out transient flags like Caps Lock, Fn, and numeric pad that would
215+
/// prevent exact-match comparisons from succeeding.
216+
private static let relevantModifiers: NSEvent.ModifierFlags = [.shift, .control, .option, .command]
217+
207218
func handleEvent(event: NSEvent) -> NSEvent? {
208219
let modifierFlags = event.modifierFlags.intersection(.deviceIndependentFlagsMask)
220+
.intersection(Self.relevantModifiers)
209221
switch event.type {
210222
case .keyDown:
211-
let tabKey: UInt16 = 0x30
212-
let downArrow: UInt16 = 125
213-
let upArrow: UInt16 = 126
214-
215-
if event.keyCode == downArrow || event.keyCode == upArrow {
223+
if event.keyCode == Self.downArrowKeyCode || event.keyCode == Self.upArrowKeyCode {
216224
return self.handleArrowKey(event: event, modifierFlags: modifierFlags)
217-
} else if event.keyCode == tabKey {
225+
} else if event.keyCode == Self.tabKeyCode {
218226
return self.handleTab(event: event, modifierFlags: modifierFlags.rawValue)
219227
} else {
220228
return self.handleCommand(event: event, modifierFlags: modifierFlags)
@@ -291,12 +299,14 @@ extension TextViewController {
291299
}
292300
}
293301

294-
/// Handles up/down arrow key events with all modifier combinations.
302+
/// Handles up/down arrow key events for plain, Shift, Option, Command,
303+
/// and their combinations among these modifiers.
295304
/// Dispatches the appropriate movement method on the text view and consumes the event.
296305
///
297-
/// - Returns: `nil` to consume the event after dispatching the movement action.
306+
/// - Returns: `nil` to consume the event after dispatching the movement action,
307+
/// or the original event for unsupported modifier combinations.
298308
private func handleArrowKey(event: NSEvent, modifierFlags: NSEvent.ModifierFlags) -> NSEvent? {
299-
let isDown = event.keyCode == 125
309+
let isDown = event.keyCode == Self.downArrowKeyCode
300310
let shift = modifierFlags.contains(.shift)
301311
let option = modifierFlags.contains(.option)
302312
let command = modifierFlags.contains(.command)

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,21 @@ public class TextViewController: NSViewController {
163163
/// Toggle the visibility of the gutter view in the editor.
164164
public var showGutter: Bool { configuration.peripherals.showGutter }
165165

166+
/// Optional provider for custom gutter decorations.
167+
public var gutterDecorationProvider: (any GutterDecorationProviding)? {
168+
didSet {
169+
gutterView.decorationProvider = gutterDecorationProvider
170+
gutterView.needsDisplay = true
171+
}
172+
}
173+
174+
/// Optional interaction delegate for gutter decorations.
175+
public var gutterDecorationInteractionDelegate: (any GutterDecorationInteractionDelegate)? {
176+
didSet {
177+
gutterView.decorationInteractionDelegate = gutterDecorationInteractionDelegate
178+
}
179+
}
180+
166181
/// Toggle the visibility of the minimap view in the editor.
167182
public var showMinimap: Bool { configuration.peripherals.showMinimap }
168183

@@ -290,6 +305,16 @@ public class TextViewController: NSViewController {
290305
self.gutterView.setNeedsDisplay(self.gutterView.frame)
291306
}
292307

308+
/// Force the layout manager to recalculate its cached estimated line height.
309+
///
310+
/// The estimated line height is cached and not invalidated when the font or line height multiplier changes,
311+
/// causing vertical cursor movement (`moveDown:`/`moveUp:`) to use stale values and fail to cross line
312+
/// boundaries. Re-assigning `renderDelegate` triggers the cache to refresh.
313+
func refreshEstimatedLineHeightCache() {
314+
let renderDelegate = textView.layoutManager.renderDelegate
315+
textView.layoutManager.renderDelegate = renderDelegate
316+
}
317+
293318
deinit {
294319
if let highlighter {
295320
textView.removeStorageDelegate(highlighter)

Sources/CodeEditSourceEditor/SourceEditorConfiguration/SourceEditorConfiguration+Appearance.swift

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,7 @@ 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
90+
controller.refreshEstimatedLineHeightCache()
9591
needsHighlighterInvalidation = true
9692
}
9793

@@ -108,9 +104,7 @@ extension SourceEditorConfiguration {
108104

109105
if oldConfig?.lineHeightMultiple != lineHeightMultiple {
110106
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
107+
controller.refreshEstimatedLineHeightCache()
114108
}
115109

116110
if oldConfig?.wrapLines != wrapLines {

0 commit comments

Comments
 (0)