From 487f573edcee958bdcb9012cbae1957dd177ec65 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Tue, 22 Apr 2025 11:45:20 -0500 Subject: [PATCH 01/11] Started work on the reformatting guide --- .../TextViewController+LoadView.swift | 4 + .../TextViewController+ReloadUI.swift | 5 + .../ReformattingGuideView.swift | 95 +++++++++++++++++++ 3 files changed, 104 insertions(+) create mode 100644 Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index f0b828399..fd51d6570 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -28,6 +28,10 @@ extension TextViewController { minimapView = MinimapView(textView: textView, theme: theme) scrollView.addFloatingSubview(minimapView, for: .vertical) + // Add reformatting guide view + let guideView = ReformattingGuideView(frame: NSRect(x: 0, y: 0, width: 100, height: 100)) + textView.addSubview(guideView) + let findViewController = FindViewController(target: self, childView: scrollView) addChild(findViewController) self.findViewController = findViewController diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift index e7c584588..ca78d9b3b 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift @@ -19,5 +19,10 @@ extension TextViewController { highlighter?.invalidate() minimapView.updateContentViewHeight() minimapView.updateDocumentVisibleViewPosition() + + // Update reformatting guide position + if let guideView = textView.subviews.first(where: { $0 is ReformattingGuideView }) as? ReformattingGuideView { + guideView.updatePosition(in: textView) + } } } diff --git a/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift new file mode 100644 index 000000000..fb17f1504 --- /dev/null +++ b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift @@ -0,0 +1,95 @@ +import AppKit +import CodeEditTextView + +class ReformattingGuideView: NSView { + private let column: Int = 80 + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + wantsLayer = true + print("ReformattingGuideView initialized with frame: \(frameRect)") + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func draw(_ dirtyRect: NSRect) { + super.draw(dirtyRect) + print("Drawing guide view with frame: \(frame)") + + // For debugging, make the guide more visible + NSColor.red.withAlphaComponent(0.3).setFill() + frame.fill() + + // Draw a border around the guide for better visibility + NSColor.blue.setStroke() + let borderPath = NSBezierPath(rect: frame) + borderPath.lineWidth = 2.0 + borderPath.stroke() + + // Get the current theme's background color to determine if we're in light or dark mode + let isLightMode = NSApp.effectiveAppearance.name == .aqua + + // Set the line color based on the theme + let lineColor = isLightMode ? + NSColor.black.withAlphaComponent(0.1) : + NSColor.white.withAlphaComponent(0.1) + + // Set the shaded area color (slightly more transparent) + let shadedColor = isLightMode ? + NSColor.black.withAlphaComponent(0.05) : + NSColor.white.withAlphaComponent(0.05) + + // Draw the vertical line (accounting for inverted Y coordinate system) + lineColor.setStroke() + let linePath = NSBezierPath() + linePath.move(to: NSPoint(x: frame.minX, y: frame.maxY)) // Start at top + linePath.line(to: NSPoint(x: frame.minX, y: frame.minY)) // Draw down to bottom + linePath.lineWidth = 1.0 + linePath.stroke() + + // Draw the shaded area to the right of the line + shadedColor.setFill() + let shadedRect = NSRect( + x: frame.minX, + y: frame.minY, + width: frame.width, + height: frame.height + ) + shadedRect.fill() + } + + func updatePosition(in textView: TextView) { + // Wait for the text view to have a valid width + guard textView.frame.width > 0 else { + print("Text view width is 0, skipping position update") + return + } + + // Calculate the x position based on the font's character width and column number + let charWidth = textView.font.boundingRectForFont.width + let xPosition = CGFloat(column) * charWidth + + print("Updating guide position:") + print("- Character width: \(charWidth)") + print("- Target column: \(column)") + print("- Calculated x position: \(xPosition)") + print("- Text view width: \(textView.frame.width)") + + // Ensure we don't create an invalid frame + let maxWidth = max(0, textView.frame.width - xPosition/2) + + // Update the frame to be a vertical line at column 80 with a shaded area to the right + let newFrame = NSRect( + x: 200, + y: 0, + width: maxWidth, + height: textView.frame.height + ) + print("Setting new frame: \(newFrame)") + + frame = newFrame + needsDisplay = true + } +} From 4b15a77c219752a346d43e5f3f676a76affe5686 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 24 Apr 2025 15:22:29 -0500 Subject: [PATCH 02/11] Implemented reformatting guide, styled it, and added configuration options to show or hide it and change it's position. --- .../Views/ContentView.swift | 24 ++++- .../Views/StatusBar.swift | 15 ++- .../CodeEditSourceEditor.swift | 34 ++++++- .../TextViewController+LoadView.swift | 26 +++++- .../TextViewController+ReloadUI.swift | 2 +- .../Controller/TextViewController.swift | 47 +++++++++- .../Minimap/MinimapView.swift | 27 ++++-- .../ReformattingGuideView.swift | 91 +++++++++++-------- 8 files changed, 206 insertions(+), 60 deletions(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift index 564d2c129..30cb0a15b 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift @@ -28,6 +28,16 @@ struct ContentView: View { @State private var treeSitterClient = TreeSitterClient() @AppStorage("showMinimap") private var showMinimap: Bool = true @State private var indentOption: IndentOption = .spaces(count: 4) + @AppStorage("reformatAtColumn") private var reformatAtColumn: Int = 80 { + didSet { + print("reformatAtColumn changed to: \(reformatAtColumn)") + } + } + @AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false { + didSet { + print("showReformattingGuide changed to: \(showReformattingGuide)") + } + } init(document: Binding, fileURL: URL?) { self._document = document @@ -52,7 +62,9 @@ struct ContentView: View { contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 28.0, right: 0), additionalTextInsets: NSEdgeInsets(top: 1, left: 0, bottom: 1, right: 0), useSystemCursor: useSystemCursor, - showMinimap: showMinimap + showMinimap: showMinimap, + reformatAtColumn: reformatAtColumn, + showReformattingGuide: showReformattingGuide ) .overlay(alignment: .bottom) { StatusBar( @@ -65,7 +77,9 @@ struct ContentView: View { language: $language, theme: $theme, showMinimap: $showMinimap, - indentOption: $indentOption + indentOption: $indentOption, + reformatAtColumn: $reformatAtColumn, + showReformattingGuide: $showReformattingGuide ) } .ignoresSafeArea() @@ -87,6 +101,12 @@ struct ContentView: View { theme = .light } } + .onChange(of: reformatAtColumn) { _, newValue in + print("ContentView: reformatAtColumn changed to \(newValue)") + } + .onChange(of: showReformattingGuide) { _, newValue in + print("ContentView: showReformattingGuide changed to \(newValue)") + } } } } diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift index e08aa04eb..779f5cd35 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift @@ -24,12 +24,25 @@ struct StatusBar: View { @Binding var theme: EditorTheme @Binding var showMinimap: Bool @Binding var indentOption: IndentOption + @Binding var reformatAtColumn: Int + @Binding var showReformattingGuide: Bool var body: some View { HStack { Menu { + IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty) + .buttonStyle(.borderless) Toggle("Wrap Lines", isOn: $wrapLines) Toggle("Show Minimap", isOn: $showMinimap) + Toggle("Show Reformatting Guide", isOn: $showReformattingGuide) + Picker("Reformat column at column", selection: $reformatAtColumn) { + ForEach([40, 60, 80, 100, 120, 140, 160, 180, 200], id: \.self) { column in + Text("\(column)").tag(column) + } + } + .onChange(of: reformatAtColumn) { _, newValue in + reformatAtColumn = max(1, min(200, newValue)) + } if #available(macOS 14, *) { Toggle("Use System Cursor", isOn: $useSystemCursor) } else { @@ -65,8 +78,6 @@ struct StatusBar: View { .frame(height: 12) LanguagePicker(language: $language) .buttonStyle(.borderless) - IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty) - .buttonStyle(.borderless) } .font(.subheadline) .fontWeight(.medium) diff --git a/Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift b/Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift index 22348d10b..12a0f5a10 100644 --- a/Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift +++ b/Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift @@ -50,6 +50,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { /// - useSystemCursor: If true, uses the system cursor on `>=macOS 14`. /// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager /// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information. + /// - showMinimap: Whether to show the minimap + /// - reformatAtColumn: The column to reformat at + /// - showReformattingGuide: Whether to show the reformatting guide public init( _ text: Binding, language: CodeLanguage, @@ -72,7 +75,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { useSystemCursor: Bool = true, undoManager: CEUndoManager? = nil, coordinators: [any TextViewCoordinator] = [], - showMinimap: Bool + showMinimap: Bool, + reformatAtColumn: Int, + showReformattingGuide: Bool ) { self.text = .binding(text) self.language = language @@ -100,6 +105,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { self.undoManager = undoManager self.coordinators = coordinators self.showMinimap = showMinimap + self.reformatAtColumn = reformatAtColumn + self.showReformattingGuide = showReformattingGuide } /// Initializes a Text Editor @@ -129,6 +136,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { /// See `BracketPairEmphasis` for more information. Defaults to `nil` /// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager /// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information. + /// - showMinimap: Whether to show the minimap + /// - reformatAtColumn: The column to reformat at + /// - showReformattingGuide: Whether to show the reformatting guide public init( _ text: NSTextStorage, language: CodeLanguage, @@ -151,7 +161,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { useSystemCursor: Bool = true, undoManager: CEUndoManager? = nil, coordinators: [any TextViewCoordinator] = [], - showMinimap: Bool + showMinimap: Bool, + reformatAtColumn: Int, + showReformattingGuide: Bool ) { self.text = .storage(text) self.language = language @@ -179,6 +191,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { self.undoManager = undoManager self.coordinators = coordinators self.showMinimap = showMinimap + self.reformatAtColumn = reformatAtColumn + self.showReformattingGuide = showReformattingGuide } package var text: TextAPI @@ -203,6 +217,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { private var undoManager: CEUndoManager? package var coordinators: [any TextViewCoordinator] package var showMinimap: Bool + private var reformatAtColumn: Int + private var showReformattingGuide: Bool public typealias NSViewControllerType = TextViewController @@ -229,7 +245,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { bracketPairEmphasis: bracketPairEmphasis, undoManager: undoManager, coordinators: coordinators, - showMinimap: showMinimap + showMinimap: showMinimap, + reformatAtColumn: reformatAtColumn, + showReformattingGuide: showReformattingGuide ) switch text { case .binding(let binding): @@ -286,6 +304,14 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { updateEditorProperties(controller) updateThemeAndLanguage(controller) updateHighlighting(controller, coordinator: coordinator) + + if controller.reformatAtColumn != reformatAtColumn { + controller.reformatAtColumn = reformatAtColumn + } + + if controller.showReformattingGuide != showReformattingGuide { + controller.showReformattingGuide = showReformattingGuide + } } private func updateTextProperties(_ controller: TextViewController) { @@ -369,6 +395,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable { controller.bracketPairEmphasis == bracketPairEmphasis && controller.useSystemCursor == useSystemCursor && controller.showMinimap == showMinimap && + controller.reformatAtColumn == reformatAtColumn && + controller.showReformattingGuide == showReformattingGuide && areHighlightProvidersEqual(controller: controller, coordinator: coordinator) } diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index fd51d6570..786503c09 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -12,9 +12,11 @@ extension TextViewController { override public func loadView() { super.loadView() + // Create scroll view scrollView = NSScrollView() scrollView.documentView = textView + // Create gutter view gutterView = GutterView( font: font.rulerFont, textColor: theme.text.color.withAlphaComponent(0.35), @@ -25,13 +27,21 @@ extension TextViewController { gutterView.updateWidthIfNeeded() scrollView.addFloatingSubview(gutterView, for: .horizontal) + // Create reformatting guide view + guideView = ReformattingGuideView( + column: self.reformatAtColumn, + isVisible: self.showReformattingGuide, + theme: theme + ) + guideView.wantsLayer = true + scrollView.addFloatingSubview(guideView, for: .vertical) + guideView.updatePosition(in: textView) + + // Create minimap view minimapView = MinimapView(textView: textView, theme: theme) scrollView.addFloatingSubview(minimapView, for: .vertical) - // Add reformatting guide view - let guideView = ReformattingGuideView(frame: NSRect(x: 0, y: 0, width: 100, height: 100)) - textView.addSubview(guideView) - + // Create find view let findViewController = FindViewController(target: self, childView: scrollView) addChild(findViewController) self.findViewController = findViewController @@ -43,10 +53,13 @@ extension TextViewController { textView.setUndoManager(_undoManager) } + // Style views styleTextView() styleScrollView() styleGutterView() styleMinimapView() + + // Set up setUpHighlighter() setUpTextFormation() @@ -101,7 +114,8 @@ extension TextViewController { object: scrollView.contentView, queue: .main ) { [weak self] notification in - guard let clipView = notification.object as? NSClipView else { return } + guard let clipView = notification.object as? NSClipView, + let textView = self?.textView else { return } self?.textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero) self?.gutterView.needsDisplay = true self?.minimapXConstraint?.constant = clipView.bounds.origin.x @@ -124,11 +138,13 @@ extension TextViewController { object: textView, queue: .main ) { [weak self] _ in + guard let textView = self?.textView else { return } self?.gutterView.frame.size.height = (self?.textView.frame.height ?? 0) + 10 self?.gutterView.frame.origin.y = (self?.textView.frame.origin.y ?? 0.0) - (self?.scrollView.contentInsets.top ?? 0) self?.gutterView.needsDisplay = true + self?.guideView?.updatePosition(in: textView) self?.scrollView.needsLayout = true } diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift index ca78d9b3b..d5ec302b8 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift @@ -19,7 +19,7 @@ extension TextViewController { highlighter?.invalidate() minimapView.updateContentViewHeight() minimapView.updateDocumentVisibleViewPosition() - + // Update reformatting guide position if let guideView = textView.subviews.first(where: { $0 is ReformattingGuideView }) as? ReformattingGuideView { guideView.updatePosition(in: textView) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index b7bc97262..6f3c0f1bc 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -69,6 +69,7 @@ public class TextViewController: NSViewController { gutterView.textColor = theme.text.color.withAlphaComponent(0.35) gutterView.selectedLineTextColor = theme.text.color minimapView.setTheme(theme) + guideView?.setTheme(theme) } } @@ -233,6 +234,37 @@ public class TextViewController: NSViewController { ) } + /// The column at which to show the reformatting guide + public var reformatAtColumn: Int = 80 { + didSet { + if let guideView = textView.subviews.first( + where: { $0 is ReformattingGuideView } + ) as? ReformattingGuideView { + guideView.setColumn(reformatAtColumn) + } + } + } + + /// Whether to show the reformatting guide + public var showReformattingGuide: Bool = false { + didSet { + if let guideView = self.guideView { + guideView.setVisible(showReformattingGuide) + guideView.updatePosition(in: textView) + guideView.needsDisplay = true + } + } + } + + /// The reformatting guide view + var guideView: ReformattingGuideView! { + didSet { + if let oldValue = oldValue { + oldValue.removeFromSuperview() + } + } + } + // MARK: Init init( @@ -257,7 +289,9 @@ public class TextViewController: NSViewController { bracketPairEmphasis: BracketPairEmphasis?, undoManager: CEUndoManager? = nil, coordinators: [TextViewCoordinator] = [], - showMinimap: Bool + showMinimap: Bool, + reformatAtColumn: Int = 80, + showReformattingGuide: Bool = false ) { self.language = language self.font = font @@ -278,6 +312,8 @@ public class TextViewController: NSViewController { self.bracketPairEmphasis = bracketPairEmphasis self._undoManager = undoManager self.showMinimap = showMinimap + self.reformatAtColumn = reformatAtColumn + self.showReformattingGuide = showReformattingGuide super.init(nibName: nil, bundle: nil) @@ -306,6 +342,15 @@ public class TextViewController: NSViewController { delegate: self ) + // Initialize guide view + self.guideView = ReformattingGuideView(column: reformatAtColumn, isVisible: showReformattingGuide, theme: theme) + if let guideView = self.guideView { + guideView.wantsLayer = true + guideView.layer?.zPosition = 1 + textView.addSubview(guideView) + guideView.updatePosition(in: textView) + } + coordinators.forEach { $0.prepareCoordinator(controller: self) } diff --git a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift index ce6b6f86e..ec2ff614d 100644 --- a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift +++ b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift @@ -77,6 +77,7 @@ public class MinimapView: FlippedNSView { public init(textView: TextView, theme: EditorTheme) { self.textView = textView self.lineRenderer = MinimapLineRenderer(textView: textView) + let isLightMode = theme.background.brightnessComponent > 0.5 self.scrollView = ForwardingScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false @@ -92,13 +93,17 @@ public class MinimapView: FlippedNSView { self.documentVisibleView = NSView() documentVisibleView.translatesAutoresizingMaskIntoConstraints = false documentVisibleView.wantsLayer = true - documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.05).cgColor + documentVisibleView.layer?.backgroundColor = isLightMode + ? NSColor.black.withAlphaComponent(0.065).cgColor + : NSColor.white.withAlphaComponent(0.065).cgColor self.separatorView = NSView() separatorView.translatesAutoresizingMaskIntoConstraints = false separatorView.wantsLayer = true - separatorView.layer?.backgroundColor = NSColor.separatorColor.cgColor - + separatorView.layer?.backgroundColor = isLightMode + ? NSColor.black.withAlphaComponent(0.1).cgColor + : NSColor.white.withAlphaComponent(0.1).cgColor + super.init(frame: .zero) setUpPanGesture() @@ -171,16 +176,16 @@ public class MinimapView: FlippedNSView { // Constrain to all sides scrollView.topAnchor.constraint(equalTo: topAnchor), scrollView.bottomAnchor.constraint(equalTo: bottomAnchor), - scrollView.leadingAnchor.constraint(equalTo: leadingAnchor), + scrollView.leadingAnchor.constraint(equalTo: separatorView.trailingAnchor), scrollView.trailingAnchor.constraint(equalTo: trailingAnchor), // Scrolling, but match width - contentView.leadingAnchor.constraint(equalTo: leadingAnchor), - contentView.trailingAnchor.constraint(equalTo: trailingAnchor), + contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), + contentView.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor), contentViewHeightConstraint, // Y position set manually - documentVisibleView.leadingAnchor.constraint(equalTo: leadingAnchor), + documentVisibleView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor), documentVisibleView.trailingAnchor.constraint(equalTo: trailingAnchor), // Separator on leading side @@ -310,7 +315,13 @@ public class MinimapView: FlippedNSView { /// /// - Parameter theme: The selected theme. public func setTheme(_ theme: EditorTheme) { - documentVisibleView.layer?.backgroundColor = theme.text.color.withAlphaComponent(0.05).cgColor + let isLightMode = theme.background.brightnessComponent > 0.5 + documentVisibleView.layer?.backgroundColor = isLightMode + ? NSColor.black.withAlphaComponent(0.065).cgColor + : NSColor.white.withAlphaComponent(0.065).cgColor + separatorView.layer?.backgroundColor = isLightMode + ? NSColor.black.withAlphaComponent(0.1).cgColor + : NSColor.white.withAlphaComponent(0.1).cgColor layer?.backgroundColor = theme.background.cgColor } } diff --git a/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift index fb17f1504..2322f282f 100644 --- a/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift +++ b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift @@ -2,12 +2,26 @@ import AppKit import CodeEditTextView class ReformattingGuideView: NSView { - private let column: Int = 80 + private var column: Int + private var _isVisible: Bool + private var theme: EditorTheme + + var isVisible: Bool { + get { _isVisible } + set { + _isVisible = newValue + isHidden = !newValue + needsDisplay = true + } + } - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) + init(column: Int = 80, isVisible: Bool = false, theme: EditorTheme) { + self.column = column + self._isVisible = isVisible + self.theme = theme + super.init(frame: .zero) wantsLayer = true - print("ReformattingGuideView initialized with frame: \(frameRect)") + isHidden = !isVisible } required init?(coder: NSCoder) { @@ -16,30 +30,22 @@ class ReformattingGuideView: NSView { override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) - print("Drawing guide view with frame: \(frame)") - - // For debugging, make the guide more visible - NSColor.red.withAlphaComponent(0.3).setFill() - frame.fill() - - // Draw a border around the guide for better visibility - NSColor.blue.setStroke() - let borderPath = NSBezierPath(rect: frame) - borderPath.lineWidth = 2.0 - borderPath.stroke() + guard isVisible else { + return + } - // Get the current theme's background color to determine if we're in light or dark mode - let isLightMode = NSApp.effectiveAppearance.name == .aqua + // Determine if we should use light or dark colors based on the theme's background color + let isLightMode = theme.background.brightnessComponent > 0.5 // Set the line color based on the theme let lineColor = isLightMode ? - NSColor.black.withAlphaComponent(0.1) : - NSColor.white.withAlphaComponent(0.1) + NSColor.black.withAlphaComponent(0.075) : + NSColor.white.withAlphaComponent(0.175) // Set the shaded area color (slightly more transparent) let shadedColor = isLightMode ? - NSColor.black.withAlphaComponent(0.05) : - NSColor.white.withAlphaComponent(0.05) + NSColor.black.withAlphaComponent(0.025) : + NSColor.white.withAlphaComponent(0.025) // Draw the vertical line (accounting for inverted Y coordinate system) lineColor.setStroke() @@ -61,35 +67,44 @@ class ReformattingGuideView: NSView { } func updatePosition(in textView: TextView) { - // Wait for the text view to have a valid width - guard textView.frame.width > 0 else { - print("Text view width is 0, skipping position update") + guard isVisible else { return } // Calculate the x position based on the font's character width and column number let charWidth = textView.font.boundingRectForFont.width - let xPosition = CGFloat(column) * charWidth + let xPosition = CGFloat(column) * charWidth / 2 // Divide by 2 to account for coordinate system - print("Updating guide position:") - print("- Character width: \(charWidth)") - print("- Target column: \(column)") - print("- Calculated x position: \(xPosition)") - print("- Text view width: \(textView.frame.width)") + // Get the scroll view's content size + guard let scrollView = textView.enclosingScrollView else { return } + let contentSize = scrollView.documentVisibleRect.size // Ensure we don't create an invalid frame - let maxWidth = max(0, textView.frame.width - xPosition/2) + let maxWidth = max(0, contentSize.width - xPosition) - // Update the frame to be a vertical line at column 80 with a shaded area to the right + // Update the frame to be a vertical line at the specified column with a shaded area to the right let newFrame = NSRect( - x: 200, - y: 0, - width: maxWidth, - height: textView.frame.height - ) - print("Setting new frame: \(newFrame)") + x: xPosition, + y: 0, // Start above the visible area + width: maxWidth + 1000, + height: contentSize.height // Use extended height + ).pixelAligned frame = newFrame needsDisplay = true } + + func setVisible(_ visible: Bool) { + isVisible = visible + } + + func setColumn(_ newColumn: Int) { + column = newColumn + needsDisplay = true + } + + func setTheme(_ newTheme: EditorTheme) { + theme = newTheme + needsDisplay = true + } } From bb31db4e51775434dd4863d125ecc6fda384e933 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 24 Apr 2025 15:33:38 -0500 Subject: [PATCH 03/11] Fixed an issue with updating reformatAtColumn. --- .../Controller/TextViewController.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index 6f3c0f1bc..d71199b52 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -237,10 +237,10 @@ public class TextViewController: NSViewController { /// The column at which to show the reformatting guide public var reformatAtColumn: Int = 80 { didSet { - if let guideView = textView.subviews.first( - where: { $0 is ReformattingGuideView } - ) as? ReformattingGuideView { + if let guideView = self.guideView { guideView.setColumn(reformatAtColumn) + guideView.updatePosition(in: textView) + guideView.needsDisplay = true } } } From 9c0e46582ce3d34eabca7c41a7332fe6466182a0 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 24 Apr 2025 15:35:25 -0500 Subject: [PATCH 04/11] Removed debugging code --- .../Views/ContentView.swift | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift index 30cb0a15b..8a42a5f1a 100644 --- a/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift +++ b/Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift @@ -28,16 +28,8 @@ struct ContentView: View { @State private var treeSitterClient = TreeSitterClient() @AppStorage("showMinimap") private var showMinimap: Bool = true @State private var indentOption: IndentOption = .spaces(count: 4) - @AppStorage("reformatAtColumn") private var reformatAtColumn: Int = 80 { - didSet { - print("reformatAtColumn changed to: \(reformatAtColumn)") - } - } - @AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false { - didSet { - print("showReformattingGuide changed to: \(showReformattingGuide)") - } - } + @AppStorage("reformatAtColumn") private var reformatAtColumn: Int = 80 + @AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false init(document: Binding, fileURL: URL?) { self._document = document @@ -101,12 +93,6 @@ struct ContentView: View { theme = .light } } - .onChange(of: reformatAtColumn) { _, newValue in - print("ContentView: reformatAtColumn changed to \(newValue)") - } - .onChange(of: showReformattingGuide) { _, newValue in - print("ContentView: showReformattingGuide changed to \(newValue)") - } } } } From 6680de6860a8482471a9e4900bcc26561b1ba515 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Thu, 24 Apr 2025 16:57:32 -0500 Subject: [PATCH 05/11] Fixed some SwiftLint errors --- .../Controller/TextViewController+LoadView.swift | 4 ++-- Sources/CodeEditSourceEditor/Minimap/MinimapView.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index 786503c09..f1365d6d7 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -58,7 +58,7 @@ extension TextViewController { styleScrollView() styleGutterView() styleMinimapView() - + // Set up setUpHighlighter() setUpTextFormation() @@ -116,7 +116,7 @@ extension TextViewController { ) { [weak self] notification in guard let clipView = notification.object as? NSClipView, let textView = self?.textView else { return } - self?.textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero) + textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero) self?.gutterView.needsDisplay = true self?.minimapXConstraint?.constant = clipView.bounds.origin.x } diff --git a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift index ec2ff614d..ef680f5ee 100644 --- a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift +++ b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift @@ -103,7 +103,7 @@ public class MinimapView: FlippedNSView { separatorView.layer?.backgroundColor = isLightMode ? NSColor.black.withAlphaComponent(0.1).cgColor : NSColor.white.withAlphaComponent(0.1).cgColor - + super.init(frame: .zero) setUpPanGesture() From 07433a69fe00ad0ad7f780bb7e5a1cc7f128aa2c Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 09:37:23 -0500 Subject: [PATCH 06/11] Removed unneccessary logic for guideview in init. --- .../Controller/TextViewController.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index d71199b52..1abf823a2 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -344,12 +344,6 @@ public class TextViewController: NSViewController { // Initialize guide view self.guideView = ReformattingGuideView(column: reformatAtColumn, isVisible: showReformattingGuide, theme: theme) - if let guideView = self.guideView { - guideView.wantsLayer = true - guideView.layer?.zPosition = 1 - textView.addSubview(guideView) - guideView.updatePosition(in: textView) - } coordinators.forEach { $0.prepareCoordinator(controller: self) From 5a0fa9c73a520aea79b055ff557512197f8b86a2 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 09:40:48 -0500 Subject: [PATCH 07/11] Changed function name --- .../Controller/TextViewController+LoadView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index f1365d6d7..f140bbc0d 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -68,7 +68,7 @@ extension TextViewController { } setUpConstraints() - setUpListeners() + setUpOberservers() textView.updateFrameIfNeeded() @@ -107,7 +107,7 @@ extension TextViewController { ]) } - func setUpListeners() { + func setUpOberservers() { // Layout on scroll change NotificationCenter.default.addObserver( forName: NSView.boundsDidChangeNotification, From ddef66cc7f0189031d74a422a0e79e480862be54 Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 10:43:58 -0500 Subject: [PATCH 08/11] Fixed swiftlint errors. --- .../TextViewController+LoadView.swift | 18 +++++++++++++++++- .../Controller/TextViewController.swift | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index f140bbc0d..07b881c2d 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -107,7 +107,7 @@ extension TextViewController { ]) } - func setUpOberservers() { + func setUpOnScrollChangeObserver() { // Layout on scroll change NotificationCenter.default.addObserver( forName: NSView.boundsDidChangeNotification, @@ -120,7 +120,9 @@ extension TextViewController { self?.gutterView.needsDisplay = true self?.minimapXConstraint?.constant = clipView.bounds.origin.x } + } + func setUpOnScrollViewFrameChangeObserver() { // Layout on frame change NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, @@ -132,7 +134,9 @@ extension TextViewController { self?.emphasisManager?.removeEmphases(for: EmphasisGroup.brackets) self?.updateTextInsets() } + } + func setUpTextViewFrameChangeObserver() { NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, object: textView, @@ -147,7 +151,9 @@ extension TextViewController { self?.guideView?.updatePosition(in: textView) self?.scrollView.needsLayout = true } + } + func setUpSelectionChangedObserver() { NotificationCenter.default.addObserver( forName: TextSelectionManager.selectionChangedNotification, object: textView.selectionManager, @@ -156,7 +162,9 @@ extension TextViewController { self?.updateCursorPosition() self?.emphasizeSelectionPairs() } + } + func setUpAppearanceChangedObserver() { NSApp.publisher(for: \.effectiveAppearance) .receive(on: RunLoop.main) .sink { [weak self] newValue in @@ -173,6 +181,14 @@ extension TextViewController { .store(in: &cancellables) } + func setUpOberservers() { + setUpOnScrollChangeObserver() + setUpOnScrollViewFrameChangeObserver() + setUpTextViewFrameChangeObserver() + setUpSelectionChangedObserver() + setUpAppearanceChangedObserver() + } + func setUpKeyBindings(eventMonitor: inout Any?) { eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event -> NSEvent? in guard let self = self else { return event } diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift index 1abf823a2..8d3b8b69f 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController.swift @@ -16,7 +16,7 @@ import TextFormation /// /// A view controller class for managing a source editor. Uses ``CodeEditTextView/TextView`` for input and rendering, /// tree-sitter for syntax highlighting, and TextFormation for live editing completions. -public class TextViewController: NSViewController { +public class TextViewController: NSViewController { // swiftlint:disable:this type_body_length // swiftlint:disable:next line_length public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification") From 5b26e5f3c05474ecfd1fe3e080e8e4f3ebc4be2c Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 13:47:00 -0500 Subject: [PATCH 09/11] Fixed tests --- Sources/CodeEditSourceEditor/Minimap/MinimapView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift index ef680f5ee..d515d94d1 100644 --- a/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift +++ b/Sources/CodeEditSourceEditor/Minimap/MinimapView.swift @@ -77,7 +77,7 @@ public class MinimapView: FlippedNSView { public init(textView: TextView, theme: EditorTheme) { self.textView = textView self.lineRenderer = MinimapLineRenderer(textView: textView) - let isLightMode = theme.background.brightnessComponent > 0.5 + let isLightMode = (theme.background.usingColorSpace(.deviceRGB)?.brightnessComponent ?? 0.0) > 0.5 self.scrollView = ForwardingScrollView() scrollView.translatesAutoresizingMaskIntoConstraints = false From 4a88f7b434b91359a1f6cc86dad6a27ea9f191ad Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 13:50:59 -0500 Subject: [PATCH 10/11] Added comment on draw function --- .../ReformattingGuide/ReformattingGuideView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift index 2322f282f..bb395ee28 100644 --- a/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift +++ b/Sources/CodeEditSourceEditor/ReformattingGuide/ReformattingGuideView.swift @@ -28,6 +28,7 @@ class ReformattingGuideView: NSView { fatalError("init(coder:) has not been implemented") } + // Draw the reformatting guide line and shaded area override func draw(_ dirtyRect: NSRect) { super.draw(dirtyRect) guard isVisible else { From a4aa1d1f403aee43fac8c1fc3e69353f5bda15bd Mon Sep 17 00:00:00 2001 From: Austin Condiff Date: Mon, 28 Apr 2025 13:56:48 -0500 Subject: [PATCH 11/11] Resolved PR issues --- .../Controller/TextViewController+LoadView.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift index 07b881c2d..1b960ed48 100644 --- a/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift +++ b/Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift @@ -12,11 +12,9 @@ extension TextViewController { override public func loadView() { super.loadView() - // Create scroll view scrollView = NSScrollView() scrollView.documentView = textView - // Create gutter view gutterView = GutterView( font: font.rulerFont, textColor: theme.text.color.withAlphaComponent(0.35), @@ -27,7 +25,6 @@ extension TextViewController { gutterView.updateWidthIfNeeded() scrollView.addFloatingSubview(gutterView, for: .horizontal) - // Create reformatting guide view guideView = ReformattingGuideView( column: self.reformatAtColumn, isVisible: self.showReformattingGuide, @@ -37,11 +34,9 @@ extension TextViewController { scrollView.addFloatingSubview(guideView, for: .vertical) guideView.updatePosition(in: textView) - // Create minimap view minimapView = MinimapView(textView: textView, theme: theme) scrollView.addFloatingSubview(minimapView, for: .vertical) - // Create find view let findViewController = FindViewController(target: self, childView: scrollView) addChild(findViewController) self.findViewController = findViewController @@ -53,13 +48,11 @@ extension TextViewController { textView.setUndoManager(_undoManager) } - // Style views styleTextView() styleScrollView() styleGutterView() styleMinimapView() - // Set up setUpHighlighter() setUpTextFormation() @@ -108,7 +101,6 @@ extension TextViewController { } func setUpOnScrollChangeObserver() { - // Layout on scroll change NotificationCenter.default.addObserver( forName: NSView.boundsDidChangeNotification, object: scrollView.contentView, @@ -123,7 +115,6 @@ extension TextViewController { } func setUpOnScrollViewFrameChangeObserver() { - // Layout on frame change NotificationCenter.default.addObserver( forName: NSView.frameDidChangeNotification, object: scrollView.contentView,