Skip to content

Added Reformatting Guide #314

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 28, 2025
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +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
@AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false

init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
self._document = document
Expand All @@ -52,7 +54,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(
Expand All @@ -65,7 +69,9 @@ struct ContentView: View {
language: $language,
theme: $theme,
showMinimap: $showMinimap,
indentOption: $indentOption
indentOption: $indentOption,
reformatAtColumn: $reformatAtColumn,
showReformattingGuide: $showReformattingGuide
)
}
.ignoresSafeArea()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>,
language: CodeLanguage,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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):
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ extension TextViewController {
gutterView.updateWidthIfNeeded()
scrollView.addFloatingSubview(gutterView, for: .horizontal)

guideView = ReformattingGuideView(
column: self.reformatAtColumn,
isVisible: self.showReformattingGuide,
theme: theme
)
guideView.wantsLayer = true
scrollView.addFloatingSubview(guideView, for: .vertical)
guideView.updatePosition(in: textView)

minimapView = MinimapView(textView: textView, theme: theme)
scrollView.addFloatingSubview(minimapView, for: .vertical)

Expand All @@ -43,6 +52,7 @@ extension TextViewController {
styleScrollView()
styleGutterView()
styleMinimapView()

setUpHighlighter()
setUpTextFormation()

Expand All @@ -51,7 +61,7 @@ extension TextViewController {
}

setUpConstraints()
setUpListeners()
setUpOberservers()

textView.updateFrameIfNeeded()

Expand Down Expand Up @@ -90,20 +100,21 @@ extension TextViewController {
])
}

func setUpListeners() {
// Layout on scroll change
func setUpOnScrollChangeObserver() {
NotificationCenter.default.addObserver(
forName: NSView.boundsDidChangeNotification,
object: scrollView.contentView,
queue: .main
) { [weak self] notification in
guard let clipView = notification.object as? NSClipView else { return }
self?.textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero)
guard let clipView = notification.object as? NSClipView,
let textView = self?.textView else { return }
textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero)
self?.gutterView.needsDisplay = true
self?.minimapXConstraint?.constant = clipView.bounds.origin.x
}
}

// Layout on frame change
func setUpOnScrollViewFrameChangeObserver() {
NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
object: scrollView.contentView,
Expand All @@ -114,20 +125,26 @@ extension TextViewController {
self?.emphasisManager?.removeEmphases(for: EmphasisGroup.brackets)
self?.updateTextInsets()
}
}

func setUpTextViewFrameChangeObserver() {
NotificationCenter.default.addObserver(
forName: NSView.frameDidChangeNotification,
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
}
}

func setUpSelectionChangedObserver() {
NotificationCenter.default.addObserver(
forName: TextSelectionManager.selectionChangedNotification,
object: textView.selectionManager,
Expand All @@ -136,7 +153,9 @@ extension TextViewController {
self?.updateCursorPosition()
self?.emphasizeSelectionPairs()
}
}

func setUpAppearanceChangedObserver() {
NSApp.publisher(for: \.effectiveAppearance)
.receive(on: RunLoop.main)
.sink { [weak self] newValue in
Expand All @@ -153,6 +172,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 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
43 changes: 41 additions & 2 deletions Sources/CodeEditSourceEditor/Controller/TextViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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 = self.guideView {
guideView.setColumn(reformatAtColumn)
guideView.updatePosition(in: textView)
guideView.needsDisplay = true
}
}
}

/// 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(
Expand All @@ -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
Expand All @@ -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)

Expand Down Expand Up @@ -306,6 +342,9 @@ public class TextViewController: NSViewController {
delegate: self
)

// Initialize guide view
self.guideView = ReformattingGuideView(column: reformatAtColumn, isVisible: showReformattingGuide, theme: theme)

coordinators.forEach {
$0.prepareCoordinator(controller: self)
}
Expand Down
Loading