Skip to content

Commit 3dccebd

Browse files
Added Reformatting Guide (#314)
### Description This will add a reformatting guide that is disabled by default. When enabled users will see a line drawn at a configurable column to guide them as to when and how they should reformat their code. ### Related Issues - #270 ### Checklist - [x] I read and understood the [contributing guide](https://github.com/CodeEditApp/CodeEdit/blob/main/CONTRIBUTING.md) as well as the [code of conduct](https://github.com/CodeEditApp/CodeEdit/blob/main/CODE_OF_CONDUCT.md) - [x] The issues this PR addresses are related to each other - [x] My changes generate no new warnings - [x] My code builds and runs on my machine - [x] My changes are all related to the related issue above - [x] I documented my code ### Screenshots <img width="1443" alt="image" src="https://github.com/user-attachments/assets/6b2bb8de-98cf-4628-bced-3c2330494676" /> <img width="1443" alt="image" src="https://github.com/user-attachments/assets/3c9ef6bb-b5a5-434e-bc0b-bf039169c271" /> <img width="1443" alt="image" src="https://github.com/user-attachments/assets/f03e8597-e383-4d8e-b3f1-328e26d818aa" />
1 parent 334506c commit 3dccebd

File tree

8 files changed

+260
-22
lines changed

8 files changed

+260
-22
lines changed

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/ContentView.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ struct ContentView: View {
2828
@State private var treeSitterClient = TreeSitterClient()
2929
@AppStorage("showMinimap") private var showMinimap: Bool = true
3030
@State private var indentOption: IndentOption = .spaces(count: 4)
31+
@AppStorage("reformatAtColumn") private var reformatAtColumn: Int = 80
32+
@AppStorage("showReformattingGuide") private var showReformattingGuide: Bool = false
3133

3234
init(document: Binding<CodeEditSourceEditorExampleDocument>, fileURL: URL?) {
3335
self._document = document
@@ -52,7 +54,9 @@ struct ContentView: View {
5254
contentInsets: NSEdgeInsets(top: proxy.safeAreaInsets.top, left: 0, bottom: 28.0, right: 0),
5355
additionalTextInsets: NSEdgeInsets(top: 1, left: 0, bottom: 1, right: 0),
5456
useSystemCursor: useSystemCursor,
55-
showMinimap: showMinimap
57+
showMinimap: showMinimap,
58+
reformatAtColumn: reformatAtColumn,
59+
showReformattingGuide: showReformattingGuide
5660
)
5761
.overlay(alignment: .bottom) {
5862
StatusBar(
@@ -65,7 +69,9 @@ struct ContentView: View {
6569
language: $language,
6670
theme: $theme,
6771
showMinimap: $showMinimap,
68-
indentOption: $indentOption
72+
indentOption: $indentOption,
73+
reformatAtColumn: $reformatAtColumn,
74+
showReformattingGuide: $showReformattingGuide
6975
)
7076
}
7177
.ignoresSafeArea()

Example/CodeEditSourceEditorExample/CodeEditSourceEditorExample/Views/StatusBar.swift

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,25 @@ struct StatusBar: View {
2424
@Binding var theme: EditorTheme
2525
@Binding var showMinimap: Bool
2626
@Binding var indentOption: IndentOption
27+
@Binding var reformatAtColumn: Int
28+
@Binding var showReformattingGuide: Bool
2729

2830
var body: some View {
2931
HStack {
3032
Menu {
33+
IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty)
34+
.buttonStyle(.borderless)
3135
Toggle("Wrap Lines", isOn: $wrapLines)
3236
Toggle("Show Minimap", isOn: $showMinimap)
37+
Toggle("Show Reformatting Guide", isOn: $showReformattingGuide)
38+
Picker("Reformat column at column", selection: $reformatAtColumn) {
39+
ForEach([40, 60, 80, 100, 120, 140, 160, 180, 200], id: \.self) { column in
40+
Text("\(column)").tag(column)
41+
}
42+
}
43+
.onChange(of: reformatAtColumn) { _, newValue in
44+
reformatAtColumn = max(1, min(200, newValue))
45+
}
3346
if #available(macOS 14, *) {
3447
Toggle("Use System Cursor", isOn: $useSystemCursor)
3548
} else {
@@ -65,8 +78,6 @@ struct StatusBar: View {
6578
.frame(height: 12)
6679
LanguagePicker(language: $language)
6780
.buttonStyle(.borderless)
68-
IndentPicker(indentOption: $indentOption, enabled: document.text.isEmpty)
69-
.buttonStyle(.borderless)
7081
}
7182
.font(.subheadline)
7283
.fontWeight(.medium)

Sources/CodeEditSourceEditor/CodeEditSourceEditor/CodeEditSourceEditor.swift

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
5050
/// - useSystemCursor: If true, uses the system cursor on `>=macOS 14`.
5151
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
5252
/// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
53+
/// - showMinimap: Whether to show the minimap
54+
/// - reformatAtColumn: The column to reformat at
55+
/// - showReformattingGuide: Whether to show the reformatting guide
5356
public init(
5457
_ text: Binding<String>,
5558
language: CodeLanguage,
@@ -72,7 +75,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
7275
useSystemCursor: Bool = true,
7376
undoManager: CEUndoManager? = nil,
7477
coordinators: [any TextViewCoordinator] = [],
75-
showMinimap: Bool
78+
showMinimap: Bool,
79+
reformatAtColumn: Int,
80+
showReformattingGuide: Bool
7681
) {
7782
self.text = .binding(text)
7883
self.language = language
@@ -100,6 +105,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
100105
self.undoManager = undoManager
101106
self.coordinators = coordinators
102107
self.showMinimap = showMinimap
108+
self.reformatAtColumn = reformatAtColumn
109+
self.showReformattingGuide = showReformattingGuide
103110
}
104111

105112
/// Initializes a Text Editor
@@ -129,6 +136,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
129136
/// See `BracketPairEmphasis` for more information. Defaults to `nil`
130137
/// - undoManager: The undo manager for the text view. Defaults to `nil`, which will create a new CEUndoManager
131138
/// - coordinators: Any text coordinators for the view to use. See ``TextViewCoordinator`` for more information.
139+
/// - showMinimap: Whether to show the minimap
140+
/// - reformatAtColumn: The column to reformat at
141+
/// - showReformattingGuide: Whether to show the reformatting guide
132142
public init(
133143
_ text: NSTextStorage,
134144
language: CodeLanguage,
@@ -151,7 +161,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
151161
useSystemCursor: Bool = true,
152162
undoManager: CEUndoManager? = nil,
153163
coordinators: [any TextViewCoordinator] = [],
154-
showMinimap: Bool
164+
showMinimap: Bool,
165+
reformatAtColumn: Int,
166+
showReformattingGuide: Bool
155167
) {
156168
self.text = .storage(text)
157169
self.language = language
@@ -179,6 +191,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
179191
self.undoManager = undoManager
180192
self.coordinators = coordinators
181193
self.showMinimap = showMinimap
194+
self.reformatAtColumn = reformatAtColumn
195+
self.showReformattingGuide = showReformattingGuide
182196
}
183197

184198
package var text: TextAPI
@@ -203,6 +217,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
203217
private var undoManager: CEUndoManager?
204218
package var coordinators: [any TextViewCoordinator]
205219
package var showMinimap: Bool
220+
private var reformatAtColumn: Int
221+
private var showReformattingGuide: Bool
206222

207223
public typealias NSViewControllerType = TextViewController
208224

@@ -229,7 +245,9 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
229245
bracketPairEmphasis: bracketPairEmphasis,
230246
undoManager: undoManager,
231247
coordinators: coordinators,
232-
showMinimap: showMinimap
248+
showMinimap: showMinimap,
249+
reformatAtColumn: reformatAtColumn,
250+
showReformattingGuide: showReformattingGuide
233251
)
234252
switch text {
235253
case .binding(let binding):
@@ -286,6 +304,14 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
286304
updateEditorProperties(controller)
287305
updateThemeAndLanguage(controller)
288306
updateHighlighting(controller, coordinator: coordinator)
307+
308+
if controller.reformatAtColumn != reformatAtColumn {
309+
controller.reformatAtColumn = reformatAtColumn
310+
}
311+
312+
if controller.showReformattingGuide != showReformattingGuide {
313+
controller.showReformattingGuide = showReformattingGuide
314+
}
289315
}
290316

291317
private func updateTextProperties(_ controller: TextViewController) {
@@ -369,6 +395,8 @@ public struct CodeEditSourceEditor: NSViewControllerRepresentable {
369395
controller.bracketPairEmphasis == bracketPairEmphasis &&
370396
controller.useSystemCursor == useSystemCursor &&
371397
controller.showMinimap == showMinimap &&
398+
controller.reformatAtColumn == reformatAtColumn &&
399+
controller.showReformattingGuide == showReformattingGuide &&
372400
areHighlightProvidersEqual(controller: controller, coordinator: coordinator)
373401
}
374402

Sources/CodeEditSourceEditor/Controller/TextViewController+LoadView.swift

Lines changed: 33 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,15 @@ extension TextViewController {
2525
gutterView.updateWidthIfNeeded()
2626
scrollView.addFloatingSubview(gutterView, for: .horizontal)
2727

28+
guideView = ReformattingGuideView(
29+
column: self.reformatAtColumn,
30+
isVisible: self.showReformattingGuide,
31+
theme: theme
32+
)
33+
guideView.wantsLayer = true
34+
scrollView.addFloatingSubview(guideView, for: .vertical)
35+
guideView.updatePosition(in: textView)
36+
2837
minimapView = MinimapView(textView: textView, theme: theme)
2938
scrollView.addFloatingSubview(minimapView, for: .vertical)
3039

@@ -43,6 +52,7 @@ extension TextViewController {
4352
styleScrollView()
4453
styleGutterView()
4554
styleMinimapView()
55+
4656
setUpHighlighter()
4757
setUpTextFormation()
4858

@@ -51,7 +61,7 @@ extension TextViewController {
5161
}
5262

5363
setUpConstraints()
54-
setUpListeners()
64+
setUpOberservers()
5565

5666
textView.updateFrameIfNeeded()
5767

@@ -90,20 +100,21 @@ extension TextViewController {
90100
])
91101
}
92102

93-
func setUpListeners() {
94-
// Layout on scroll change
103+
func setUpOnScrollChangeObserver() {
95104
NotificationCenter.default.addObserver(
96105
forName: NSView.boundsDidChangeNotification,
97106
object: scrollView.contentView,
98107
queue: .main
99108
) { [weak self] notification in
100-
guard let clipView = notification.object as? NSClipView else { return }
101-
self?.textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero)
109+
guard let clipView = notification.object as? NSClipView,
110+
let textView = self?.textView else { return }
111+
textView.updatedViewport(self?.scrollView.documentVisibleRect ?? .zero)
102112
self?.gutterView.needsDisplay = true
103113
self?.minimapXConstraint?.constant = clipView.bounds.origin.x
104114
}
115+
}
105116

106-
// Layout on frame change
117+
func setUpOnScrollViewFrameChangeObserver() {
107118
NotificationCenter.default.addObserver(
108119
forName: NSView.frameDidChangeNotification,
109120
object: scrollView.contentView,
@@ -114,20 +125,26 @@ extension TextViewController {
114125
self?.emphasisManager?.removeEmphases(for: EmphasisGroup.brackets)
115126
self?.updateTextInsets()
116127
}
128+
}
117129

130+
func setUpTextViewFrameChangeObserver() {
118131
NotificationCenter.default.addObserver(
119132
forName: NSView.frameDidChangeNotification,
120133
object: textView,
121134
queue: .main
122135
) { [weak self] _ in
136+
guard let textView = self?.textView else { return }
123137
self?.gutterView.frame.size.height = (self?.textView.frame.height ?? 0) + 10
124138
self?.gutterView.frame.origin.y = (self?.textView.frame.origin.y ?? 0.0)
125139
- (self?.scrollView.contentInsets.top ?? 0)
126140

127141
self?.gutterView.needsDisplay = true
142+
self?.guideView?.updatePosition(in: textView)
128143
self?.scrollView.needsLayout = true
129144
}
145+
}
130146

147+
func setUpSelectionChangedObserver() {
131148
NotificationCenter.default.addObserver(
132149
forName: TextSelectionManager.selectionChangedNotification,
133150
object: textView.selectionManager,
@@ -136,7 +153,9 @@ extension TextViewController {
136153
self?.updateCursorPosition()
137154
self?.emphasizeSelectionPairs()
138155
}
156+
}
139157

158+
func setUpAppearanceChangedObserver() {
140159
NSApp.publisher(for: \.effectiveAppearance)
141160
.receive(on: RunLoop.main)
142161
.sink { [weak self] newValue in
@@ -153,6 +172,14 @@ extension TextViewController {
153172
.store(in: &cancellables)
154173
}
155174

175+
func setUpOberservers() {
176+
setUpOnScrollChangeObserver()
177+
setUpOnScrollViewFrameChangeObserver()
178+
setUpTextViewFrameChangeObserver()
179+
setUpSelectionChangedObserver()
180+
setUpAppearanceChangedObserver()
181+
}
182+
156183
func setUpKeyBindings(eventMonitor: inout Any?) {
157184
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] event -> NSEvent? in
158185
guard let self = self else { return event }

Sources/CodeEditSourceEditor/Controller/TextViewController+ReloadUI.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,10 @@ extension TextViewController {
1919
highlighter?.invalidate()
2020
minimapView.updateContentViewHeight()
2121
minimapView.updateDocumentVisibleViewPosition()
22+
23+
// Update reformatting guide position
24+
if let guideView = textView.subviews.first(where: { $0 is ReformattingGuideView }) as? ReformattingGuideView {
25+
guideView.updatePosition(in: textView)
26+
}
2227
}
2328
}

Sources/CodeEditSourceEditor/Controller/TextViewController.swift

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import TextFormation
1616
///
1717
/// A view controller class for managing a source editor. Uses ``CodeEditTextView/TextView`` for input and rendering,
1818
/// tree-sitter for syntax highlighting, and TextFormation for live editing completions.
19-
public class TextViewController: NSViewController {
19+
public class TextViewController: NSViewController { // swiftlint:disable:this type_body_length
2020
// swiftlint:disable:next line_length
2121
public static let cursorPositionUpdatedNotification: Notification.Name = .init("TextViewController.cursorPositionNotification")
2222

@@ -69,6 +69,7 @@ public class TextViewController: NSViewController {
6969
gutterView.textColor = theme.text.color.withAlphaComponent(0.35)
7070
gutterView.selectedLineTextColor = theme.text.color
7171
minimapView.setTheme(theme)
72+
guideView?.setTheme(theme)
7273
}
7374
}
7475

@@ -233,6 +234,37 @@ public class TextViewController: NSViewController {
233234
)
234235
}
235236

237+
/// The column at which to show the reformatting guide
238+
public var reformatAtColumn: Int = 80 {
239+
didSet {
240+
if let guideView = self.guideView {
241+
guideView.setColumn(reformatAtColumn)
242+
guideView.updatePosition(in: textView)
243+
guideView.needsDisplay = true
244+
}
245+
}
246+
}
247+
248+
/// Whether to show the reformatting guide
249+
public var showReformattingGuide: Bool = false {
250+
didSet {
251+
if let guideView = self.guideView {
252+
guideView.setVisible(showReformattingGuide)
253+
guideView.updatePosition(in: textView)
254+
guideView.needsDisplay = true
255+
}
256+
}
257+
}
258+
259+
/// The reformatting guide view
260+
var guideView: ReformattingGuideView! {
261+
didSet {
262+
if let oldValue = oldValue {
263+
oldValue.removeFromSuperview()
264+
}
265+
}
266+
}
267+
236268
// MARK: Init
237269

238270
init(
@@ -257,7 +289,9 @@ public class TextViewController: NSViewController {
257289
bracketPairEmphasis: BracketPairEmphasis?,
258290
undoManager: CEUndoManager? = nil,
259291
coordinators: [TextViewCoordinator] = [],
260-
showMinimap: Bool
292+
showMinimap: Bool,
293+
reformatAtColumn: Int = 80,
294+
showReformattingGuide: Bool = false
261295
) {
262296
self.language = language
263297
self.font = font
@@ -278,6 +312,8 @@ public class TextViewController: NSViewController {
278312
self.bracketPairEmphasis = bracketPairEmphasis
279313
self._undoManager = undoManager
280314
self.showMinimap = showMinimap
315+
self.reformatAtColumn = reformatAtColumn
316+
self.showReformattingGuide = showReformattingGuide
281317

282318
super.init(nibName: nil, bundle: nil)
283319

@@ -306,6 +342,9 @@ public class TextViewController: NSViewController {
306342
delegate: self
307343
)
308344

345+
// Initialize guide view
346+
self.guideView = ReformattingGuideView(column: reformatAtColumn, isVisible: showReformattingGuide, theme: theme)
347+
309348
coordinators.forEach {
310349
$0.prepareCoordinator(controller: self)
311350
}

0 commit comments

Comments
 (0)