Skip to content

Commit 02900cb

Browse files
committed
Update
1 parent 1305c72 commit 02900cb

File tree

2 files changed

+314
-20
lines changed

2 files changed

+314
-20
lines changed
Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
import STTextView
2+
import SwiftUI
3+
4+
private let insetBottom = 12 as Double
5+
private let insetTop = 12 as Double
6+
7+
/// This SwiftUI view can be used to view and edit rich text.
8+
struct _CodeBlock: View {
9+
@Binding private var selection: NSRange?
10+
@State private var contentHeight: Double = 500
11+
let fontSize: Double
12+
let commonPrecedingSpaceCount: Int
13+
let highlightedCode: AttributedString
14+
let colorScheme: ColorScheme
15+
16+
/// Create a text edit view with a certain text that uses a certain options.
17+
/// - Parameters:
18+
/// - text: The attributed string content
19+
/// - options: Editor options
20+
/// - plugins: Editor plugins
21+
public init(
22+
code: String,
23+
language: String,
24+
firstLinePrecedingSpaceCount: Int,
25+
colorScheme: ColorScheme,
26+
fontSize: Double,
27+
selection: Binding<NSRange?> = .constant(nil)
28+
) {
29+
_selection = selection
30+
self.fontSize = fontSize
31+
self.colorScheme = colorScheme
32+
33+
let padding = firstLinePrecedingSpaceCount > 0
34+
? String(repeating: " ", count: firstLinePrecedingSpaceCount)
35+
: ""
36+
let result = Self.highlight(
37+
code: padding + code,
38+
language: language,
39+
colorScheme: colorScheme,
40+
fontSize: fontSize
41+
)
42+
commonPrecedingSpaceCount = result.commonLeadingSpaceCount
43+
highlightedCode = result.code
44+
}
45+
46+
public var body: some View {
47+
_CodeBlockRepresentable(
48+
text: highlightedCode,
49+
selection: $selection,
50+
fontSize: fontSize,
51+
onHeightChange: { height in
52+
print("Q", height)
53+
contentHeight = height
54+
}
55+
)
56+
.frame(height: contentHeight, alignment: .topLeading)
57+
.background(.background)
58+
.colorScheme(colorScheme)
59+
.onAppear {
60+
print("")
61+
}
62+
}
63+
64+
static func highlight(
65+
code: String,
66+
language: String,
67+
colorScheme: ColorScheme,
68+
fontSize: Double
69+
) -> (code: AttributedString, commonLeadingSpaceCount: Int) {
70+
let (lines, commonLeadingSpaceCount) = highlighted(
71+
code: code,
72+
language: language,
73+
brightMode: colorScheme != .dark,
74+
droppingLeadingSpaces: UserDefaults.shared
75+
.value(for: \.hideCommonPrecedingSpacesInSuggestion),
76+
fontSize: fontSize,
77+
replaceSpacesWithMiddleDots: false
78+
)
79+
80+
let string = NSMutableAttributedString()
81+
for (index, line) in lines.enumerated() {
82+
string.append(line)
83+
if index < lines.count - 1 {
84+
string.append(NSAttributedString(string: "\n"))
85+
}
86+
}
87+
88+
return (code: .init(string), commonLeadingSpaceCount: commonLeadingSpaceCount)
89+
}
90+
}
91+
92+
private struct _CodeBlockRepresentable: NSViewRepresentable {
93+
@Environment(\.isEnabled) private var isEnabled
94+
@Environment(\.lineSpacing) private var lineSpacing
95+
96+
@Binding private var selection: NSRange?
97+
let text: AttributedString
98+
let fontSize: Double
99+
let onHeightChange: (Double) -> Void
100+
101+
init(
102+
text: AttributedString,
103+
selection: Binding<NSRange?>,
104+
fontSize: Double,
105+
onHeightChange: @escaping (Double) -> Void
106+
) {
107+
self.text = text
108+
_selection = selection
109+
self.fontSize = fontSize
110+
self.onHeightChange = onHeightChange
111+
}
112+
113+
func makeNSView(context: Context) -> NSScrollView {
114+
let scrollView = STTextViewFrameObservable.scrollableTextView()
115+
scrollView.contentInsets = .init(top: 0, left: 0, bottom: insetBottom, right: 0)
116+
scrollView.automaticallyAdjustsContentInsets = false
117+
let textView = scrollView.documentView as! STTextView
118+
textView.delegate = context.coordinator
119+
textView.highlightSelectedLine = false
120+
textView.widthTracksTextView = true
121+
textView.heightTracksTextView = true
122+
textView.isEditable = true
123+
124+
textView.setSelectedRange(NSRange())
125+
let lineNumberRuler = STLineNumberRulerView(textView: textView)
126+
lineNumberRuler.backgroundColor = .clear
127+
lineNumberRuler.separatorColor = .clear
128+
lineNumberRuler.rulerInsets = .init(leading: 10, trailing: 10)
129+
scrollView.verticalRulerView = lineNumberRuler
130+
let columnNumberRuler = ColumnRuler(textView: textView)
131+
scrollView.horizontalRulerView = columnNumberRuler
132+
scrollView.rulersVisible = true
133+
134+
context.coordinator.isUpdating = true
135+
textView.setAttributedString(NSAttributedString(text))
136+
context.coordinator.isUpdating = false
137+
138+
return scrollView
139+
}
140+
141+
func updateNSView(_ scrollView: NSScrollView, context: Context) {
142+
context.coordinator.parent = self
143+
144+
let textView = scrollView.documentView as! STTextViewFrameObservable
145+
146+
textView.onHeightChange = onHeightChange
147+
textView.showsInvisibleCharacters = true
148+
textView.textContainer.lineBreakMode = .byCharWrapping
149+
150+
if let columnNumberRuler = scrollView.horizontalRulerView as? ColumnRuler {
151+
columnNumberRuler.columnNumber = 5
152+
}
153+
154+
do {
155+
context.coordinator.isUpdating = true
156+
if context.coordinator.isDidChangeText == false {
157+
textView.setAttributedString(.init(text))
158+
}
159+
context.coordinator.isUpdating = false
160+
context.coordinator.isDidChangeText = false
161+
}
162+
163+
if textView.selectedRange() != selection, let selection {
164+
textView.setSelectedRange(selection)
165+
}
166+
167+
if textView.isSelectable != isEnabled {
168+
textView.isSelectable = isEnabled
169+
}
170+
171+
textView.isEditable = false
172+
173+
if !textView.widthTracksTextView {
174+
textView.widthTracksTextView = false
175+
}
176+
177+
if !textView.heightTracksTextView {
178+
textView.heightTracksTextView = true
179+
}
180+
181+
let font = NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)
182+
if textView.font != font {
183+
textView.font = font
184+
}
185+
}
186+
187+
func makeCoordinator() -> TextCoordinator {
188+
TextCoordinator(parent: self)
189+
}
190+
191+
private func styledAttributedString(_ typingAttributes: [NSAttributedString.Key: Any])
192+
-> AttributedString
193+
{
194+
let paragraph = (typingAttributes[.paragraphStyle] as! NSParagraphStyle)
195+
.mutableCopy() as! NSMutableParagraphStyle
196+
if paragraph.lineSpacing != lineSpacing {
197+
paragraph.lineSpacing = lineSpacing
198+
var typingAttributes = typingAttributes
199+
typingAttributes[.paragraphStyle] = paragraph
200+
201+
let attributeContainer = AttributeContainer(typingAttributes)
202+
var styledText = text
203+
styledText.mergeAttributes(attributeContainer, mergePolicy: .keepNew)
204+
return styledText
205+
}
206+
207+
return text
208+
}
209+
210+
class TextCoordinator: STTextViewDelegate {
211+
var parent: _CodeBlockRepresentable
212+
var isUpdating: Bool = false
213+
var isDidChangeText: Bool = false
214+
var enqueuedValue: AttributedString?
215+
216+
init(parent: _CodeBlockRepresentable) {
217+
self.parent = parent
218+
}
219+
220+
func textViewDidChangeText(_ notification: Notification) {
221+
guard let textView = notification.object as? STTextView else {
222+
return
223+
}
224+
225+
(textView as! STTextViewFrameObservable).recalculateSize()
226+
}
227+
228+
func textViewDidChangeSelection(_ notification: Notification) {
229+
guard let textView = notification.object as? STTextView else {
230+
return
231+
}
232+
233+
Task { @MainActor in
234+
self.parent.selection = textView.selectedRange()
235+
}
236+
}
237+
}
238+
}
239+
240+
private class STTextViewFrameObservable: STTextView {
241+
var onHeightChange: ((Double) -> Void)?
242+
func recalculateSize() {
243+
var maxY = 0 as Double
244+
textLayoutManager.enumerateTextLayoutFragments(in: textLayoutManager.documentRange, options: [.ensuresLayout]) { fragment in
245+
print(fragment.layoutFragmentFrame)
246+
maxY = max(maxY, fragment.layoutFragmentFrame.maxY)
247+
return true
248+
}
249+
onHeightChange?(maxY)
250+
}
251+
}
252+
253+
private final class ColumnRuler: NSRulerView {
254+
var columnNumber: Int = 0
255+
256+
private var textView: STTextView? {
257+
clientView as? STTextView
258+
}
259+
260+
public required init(textView: STTextView, scrollView: NSScrollView? = nil) {
261+
super.init(
262+
scrollView: scrollView ?? textView.enclosingScrollView,
263+
orientation: .verticalRuler
264+
)
265+
clientView = textView
266+
ruleThickness = insetBottom
267+
}
268+
269+
@available(*, unavailable)
270+
required init(coder: NSCoder) {
271+
fatalError("init(coder:) has not been implemented")
272+
}
273+
274+
override func draw(_: NSRect) {
275+
guard let context: CGContext = NSGraphicsContext.current?.cgContext else { return }
276+
NSColor.windowBackgroundColor.withAlphaComponent(0.6).setFill()
277+
context.fill(bounds)
278+
279+
let insetLeft = scrollView?.verticalRulerView?.bounds.width ?? 0
280+
var drawingBounds = bounds
281+
drawingBounds.origin.x += insetLeft + 4
282+
let fontSize = 10 as Double
283+
drawingBounds.origin.y = (insetTop - fontSize) / 2
284+
NSString(string: "\(columnNumber)").draw(in: drawingBounds, withAttributes: [
285+
.font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular),
286+
.foregroundColor: NSColor.tertiaryLabelColor,
287+
])
288+
}
289+
}

Tool/Sources/SharedUIComponents/SyntaxHighlighting.swift

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ public func highlighted(
4242
language: String,
4343
brightMode: Bool,
4444
droppingLeadingSpaces: Bool,
45-
fontSize: Double
45+
fontSize: Double,
46+
replaceSpacesWithMiddleDots: Bool = true
4647
) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) {
4748
let formatted = highlightedCodeBlock(
4849
code: code,
@@ -56,14 +57,16 @@ public func highlighted(
5657
return convertToCodeLines(
5758
formatted,
5859
middleDotColor: middleDotColor,
59-
droppingLeadingSpaces: droppingLeadingSpaces
60+
droppingLeadingSpaces: droppingLeadingSpaces,
61+
replaceSpacesWithMiddleDots: replaceSpacesWithMiddleDots
6062
)
6163
}
6264

6365
func convertToCodeLines(
6466
_ formattedCode: NSAttributedString,
6567
middleDotColor: NSColor,
66-
droppingLeadingSpaces: Bool
68+
droppingLeadingSpaces: Bool,
69+
replaceSpacesWithMiddleDots: Bool = true
6770
) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) {
6871
let input = formattedCode.string
6972
func isEmptyLine(_ line: String) -> Bool {
@@ -115,24 +118,26 @@ func convertToCodeLines(
115118
}
116119
}
117120

118-
// use regex to replace all spaces to a middle dot
119-
do {
120-
let regex = try NSRegularExpression(pattern: "[ ]*", options: [])
121-
let result = regex.matches(
122-
in: mutable.string,
123-
range: NSRange(location: 0, length: mutable.mutableString.length)
124-
)
125-
for r in result {
126-
let range = r.range
127-
mutable.replaceCharacters(
128-
in: range,
129-
with: String(repeating: "·", count: range.length)
121+
if replaceSpacesWithMiddleDots {
122+
// use regex to replace all spaces to a middle dot
123+
do {
124+
let regex = try NSRegularExpression(pattern: "[ ]*", options: [])
125+
let result = regex.matches(
126+
in: mutable.string,
127+
range: NSRange(location: 0, length: mutable.mutableString.length)
130128
)
131-
mutable.addAttributes([
132-
.foregroundColor: middleDotColor,
133-
], range: range)
134-
}
135-
} catch {}
129+
for r in result {
130+
let range = r.range
131+
mutable.replaceCharacters(
132+
in: range,
133+
with: String(repeating: "·", count: range.length)
134+
)
135+
mutable.addAttributes([
136+
.foregroundColor: middleDotColor,
137+
], range: range)
138+
}
139+
} catch {}
140+
}
136141
output.append(mutable)
137142
start += range.length + 1
138143
}

0 commit comments

Comments
 (0)