Skip to content

Commit b366666

Browse files
committed
Support hiding leading spaces
1 parent 22419ac commit b366666

6 files changed

Lines changed: 221 additions & 11 deletions

File tree

Core/Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ let package = Package(
3838
.package(url: "https://github.com/JohnSundell/Splash", from: "0.1.0"),
3939
.package(url: "https://github.com/nmdias/FeedKit", from: "9.1.2"),
4040
.package(url: "https://github.com/intitni/swift-markdown-ui", branch: "main"),
41-
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
41+
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.0.0"),
4242
],
4343
targets: [
4444
.target(name: "CGEventObserver"),
@@ -123,9 +123,11 @@ let package = Package(
123123
"Environment",
124124
"Highlightr",
125125
"Splash",
126+
.product(name: "AsyncAlgorithms", package: "swift-async-algorithms"),
126127
.product(name: "MarkdownUI", package: "swift-markdown-ui"),
127128
]
128129
),
130+
.testTarget(name: "SuggestionWidgetTests", dependencies: ["SuggestionWidget"]),
129131
.target(
130132
name: "UpdateChecker",
131133
dependencies: [

Core/Sources/Service/GUI/WidgetDataSource.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ final class WidgetDataSource {
1818
if useGlobalChat {
1919
chat = globalChat ?? ChatService(chatGPTService: ChatGPTService())
2020
globalChat = chat
21+
2122
} else {
2223
chat = chats[url] ?? ChatService(chatGPTService: ChatGPTService())
2324
chats[url] = chat

Core/Sources/SuggestionWidget/SuggestionProvider.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,26 @@ public final class SuggestionProvider: ObservableObject {
1111
@Published public var startLineIndex: Int = 0
1212
@Published public var suggestionCount: Int = 0
1313
@Published public var currentSuggestionIndex: Int = 0
14+
@Published public var commonPrecedingSpaceCount = 0
1415

1516
private var colorScheme: ColorScheme = .light
1617
private var highlightedCode: [NSAttributedString]? = nil
18+
1719
func highlightedCode(colorScheme: ColorScheme) -> [NSAttributedString] {
1820
if colorScheme != self.colorScheme { highlightedCode = nil }
1921
self.colorScheme = colorScheme
2022
if let highlightedCode { return highlightedCode }
21-
let new = highlighted(
23+
let (new, spaceCount) = highlighted(
2224
code: code,
2325
language: language,
24-
brightMode: colorScheme != .dark
26+
brightMode: colorScheme != .dark,
27+
droppingLeadingSpaces: true
2528
)
2629
highlightedCode = new
30+
commonPrecedingSpaceCount = spaceCount
2731
return new
2832
}
29-
33+
3034
public var onSelectPreviousSuggestionTapped: () -> Void
3135
public var onSelectNextSuggestionTapped: () -> Void
3236
public var onRejectSuggestionTapped: () -> Void

Core/Sources/SuggestionWidget/SyntaxHighlighting.swift

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@ func highlightedCodeBlock(
3737
return formatted
3838
}
3939

40-
func highlighted(code: String, language: String, brightMode: Bool) -> [NSAttributedString] {
40+
func highlighted(
41+
code: String,
42+
language: String,
43+
brightMode: Bool,
44+
droppingLeadingSpaces: Bool
45+
) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) {
4146
let formatted = highlightedCodeBlock(
4247
code: code,
4348
language: language,
@@ -47,24 +52,70 @@ func highlighted(code: String, language: String, brightMode: Bool) -> [NSAttribu
4752
let middleDotColor = brightMode
4853
? NSColor.black.withAlphaComponent(0.1)
4954
: NSColor.white.withAlphaComponent(0.1)
50-
return convertToCodeLines(formatted, middleDotColor: middleDotColor)
55+
return convertToCodeLines(
56+
formatted,
57+
middleDotColor: middleDotColor,
58+
droppingLeadingSpaces: droppingLeadingSpaces
59+
)
5160
}
5261

53-
private func convertToCodeLines(
62+
func convertToCodeLines(
5463
_ formattedCode: NSAttributedString,
55-
middleDotColor: NSColor
56-
) -> [NSAttributedString] {
64+
middleDotColor: NSColor,
65+
droppingLeadingSpaces: Bool
66+
) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) {
5767
let input = formattedCode.string
68+
func isEmptyLine(_ line: String) -> Bool {
69+
guard let regex = try? NSRegularExpression(pattern: #"^\s*\n?$"#) else { return false }
70+
if regex.firstMatch(
71+
in: line,
72+
options: [],
73+
range: NSMakeRange(0, line.utf16.count)
74+
) != nil {
75+
return true
76+
}
77+
return false
78+
}
79+
5880
let separatedInput = input.components(separatedBy: "\n")
81+
let commonLeadingSpaceCount = {
82+
if !droppingLeadingSpaces { return 0 }
83+
let splitted = separatedInput
84+
var result = 0
85+
outerLoop: for i in [4, 8, 12, 16, 20] {
86+
for line in splitted {
87+
if isEmptyLine(line) { continue }
88+
if i >= line.count { break outerLoop }
89+
let targetIndex = line.index(line.startIndex, offsetBy: i - 1)
90+
if line[targetIndex] != " " { break outerLoop }
91+
}
92+
result = i
93+
}
94+
return result
95+
}()
5996
var output = [NSAttributedString]()
6097
var start = 0
6198
for sub in separatedInput {
6299
let range = NSMakeRange(start, sub.utf16.count)
63100
let attributedString = formattedCode.attributedSubstring(from: range)
64101
let mutable = NSMutableAttributedString(attributedString: attributedString)
102+
103+
// remove leading spaces
104+
if commonLeadingSpaceCount > 0 {
105+
let leadingSpaces = String(repeating: " ", count: commonLeadingSpaceCount)
106+
if isEmptyLine(mutable.string) {
107+
mutable.mutableString.setString("")
108+
} else if mutable.string.hasPrefix(leadingSpaces) {
109+
mutable.replaceCharacters(
110+
in: NSRange(location: 0, length: commonLeadingSpaceCount),
111+
with: ""
112+
)
113+
}
114+
}
115+
65116
// use regex to replace all spaces to a middle dot
66117
do {
67-
let regex = try NSRegularExpression(pattern: "[ ]*", options: [])
118+
let regex = try NSRegularExpression(pattern: #"\s*"#, options: [])
68119
let result = regex.matches(
69120
in: mutable.string,
70121
range: NSRange(location: 0, length: mutable.mutableString.length)
@@ -83,5 +134,5 @@ private func convertToCodeLines(
83134
output.append(mutable)
84135
start += range.length + 1
85136
}
86-
return output
137+
return (output, commonLeadingSpaceCount)
87138
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import XCTest
2+
3+
@testable import SuggestionWidget
4+
5+
final class ConvertToCodeLinesTests: XCTestCase {
6+
func test_do_not_remove_common_leading_spaces() async throws {
7+
let code = """
8+
struct Cat {
9+
}
10+
"""
11+
let (result, spaceCount) = highlighted(
12+
code: code,
13+
language: "md",
14+
brightMode: true,
15+
droppingLeadingSpaces: false
16+
)
17+
18+
XCTAssertEqual(spaceCount, 0)
19+
print(code.replacingOccurrences(of: " ", with: "·"))
20+
XCTAssertEqual(result.map(\.string), [
21+
"····struct·Cat·{",
22+
"····}",
23+
])
24+
}
25+
26+
func test_wont_remove_common_leading_spaces_2_spaces() async throws {
27+
let code = """
28+
struct Cat {
29+
}
30+
"""
31+
let (result, spaceCount) = highlighted(
32+
code: code,
33+
language: "md",
34+
brightMode: true,
35+
droppingLeadingSpaces: true
36+
)
37+
38+
XCTAssertEqual(spaceCount, 0)
39+
XCTAssertEqual(result.map(\.string), [
40+
"··struct·Cat·{",
41+
"····}",
42+
])
43+
}
44+
45+
func test_remove_common_leading_spaces_4_spaces() async throws {
46+
let code = """
47+
struct Cat {
48+
}
49+
"""
50+
let (result, spaceCount) = highlighted(
51+
code: code,
52+
language: "md",
53+
brightMode: true,
54+
droppingLeadingSpaces: true
55+
)
56+
57+
XCTAssertEqual(spaceCount, 4)
58+
XCTAssertEqual(result.map(\.string), [
59+
"struct·Cat·{",
60+
"}",
61+
])
62+
}
63+
64+
func test_remove_common_leading_spaces_8_spaces() async throws {
65+
let code = """
66+
struct Cat {
67+
}
68+
"""
69+
let (result, spaceCount) = highlighted(
70+
code: code,
71+
language: "md",
72+
brightMode: true,
73+
droppingLeadingSpaces: true
74+
)
75+
76+
XCTAssertEqual(spaceCount, 8)
77+
XCTAssertEqual(result.map(\.string), [
78+
"struct·Cat·{",
79+
"}",
80+
])
81+
}
82+
83+
func test_remove_common_leading_spaces_one_line_is_empty() async throws {
84+
let code = """
85+
struct Cat {
86+
87+
}
88+
"""
89+
let (result, spaceCount) = highlighted(
90+
code: code,
91+
language: "md",
92+
brightMode: true,
93+
droppingLeadingSpaces: true
94+
)
95+
96+
XCTAssertEqual(spaceCount, 4)
97+
XCTAssertEqual(result.map(\.string), [
98+
"struct·Cat·{",
99+
"",
100+
"}",
101+
])
102+
}
103+
104+
func test_remove_common_leading_spaces_one_line_has_no_leading_spaces() async throws {
105+
let code = """
106+
struct Cat {
107+
//
108+
}
109+
"""
110+
let (result, spaceCount) = highlighted(
111+
code: code,
112+
language: "md",
113+
brightMode: true,
114+
droppingLeadingSpaces: true
115+
)
116+
117+
XCTAssertEqual(spaceCount, 0)
118+
XCTAssertEqual(result.map(\.string), [
119+
"····struct·Cat·{",
120+
"//",
121+
"····}",
122+
])
123+
}
124+
125+
func test_remove_common_leading_spaces_one_line_has_fewer_leading_spaces() async throws {
126+
let code = """
127+
struct Cat {
128+
//
129+
}
130+
"""
131+
let (result, spaceCount) = highlighted(
132+
code: code,
133+
language: "md",
134+
brightMode: true,
135+
droppingLeadingSpaces: true
136+
)
137+
138+
XCTAssertEqual(spaceCount, 4)
139+
XCTAssertEqual(result.map(\.string), [
140+
"····struct·Cat·{",
141+
"//",
142+
"····}",
143+
])
144+
}
145+
}

TestPlan.xctestplan

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,13 @@
5656
"identifier" : "OpenAIServiceTests",
5757
"name" : "OpenAIServiceTests"
5858
}
59+
},
60+
{
61+
"target" : {
62+
"containerPath" : "container:Core",
63+
"identifier" : "SuggestionWidgetTests",
64+
"name" : "SuggestionWidgetTests"
65+
}
5966
}
6067
],
6168
"version" : 1

0 commit comments

Comments
 (0)