Skip to content
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 @@ -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),
Expand All @@ -25,9 +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)

// Create find view
let findViewController = FindViewController(target: self, childView: scrollView)
addChild(findViewController)
self.findViewController = findViewController
Expand All @@ -39,10 +53,13 @@ extension TextViewController {
textView.setUndoManager(_undoManager)
}

// Style views
styleTextView()
styleScrollView()
styleGutterView()
styleMinimapView()

// Set up
setUpHighlighter()
setUpTextFormation()

Expand Down Expand Up @@ -97,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
Expand All @@ -120,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
}

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)
}
}
}
47 changes: 46 additions & 1 deletion Sources/CodeEditSourceEditor/Controller/TextViewController.swift
Original file line number Diff line number Diff line change
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,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)
}
Expand Down
27 changes: 19 additions & 8 deletions Sources/CodeEditSourceEditor/Minimap/MinimapView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
Loading
Loading