Skip to content

Commit a3d890a

Browse files
committed
Merge branch 'feature/hide-common-trailing-spaces-for-suggestion' into develop
2 parents 38118e0 + 43e084c commit a3d890a

8 files changed

Lines changed: 281 additions & 81 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/Preferences/Keys.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,15 @@ public struct UserDefaultPreferenceKeys {
116116

117117
public var useGlobalChat: UseGlobalChat { .init() }
118118

119+
public struct HideCommonPrecedingSpacesInSuggestion: UserDefaultPreferenceKey {
120+
public let defaultValue = false
121+
public let key = "HideCommonPrecedingSpacesInSuggestion"
122+
}
123+
124+
public var hideCommonPrecedingSpacesInSuggestion: HideCommonPrecedingSpacesInSuggestion {
125+
.init()
126+
}
127+
119128
public var disableLazyVStack: FeatureFlags.DisableLazyVStack { .init() }
120129
}
121130

Core/Sources/SuggestionWidget/SuggestionPanelContent/CodeBlockSuggestionPanel.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ struct CodeBlock: View {
55
@Environment(\.colorScheme) var colorScheme
66

77
var body: some View {
8-
VStack {
8+
VStack(spacing: 4) {
99
let code = suggestion.highlightedCode(colorScheme: colorScheme)
1010
ForEach(0..<code.endIndex, id: \.self) { index in
11-
HStack(alignment: .firstTextBaseline) {
11+
HStack(alignment: .firstTextBaseline, spacing: 4) {
1212
Text("\(index + suggestion.startLineIndex + 1)")
1313
.multilineTextAlignment(.trailing)
1414
.foregroundColor(.secondary)
@@ -18,12 +18,22 @@ struct CodeBlock: View {
1818
.frame(maxWidth: .infinity, alignment: .leading)
1919
.multilineTextAlignment(.leading)
2020
.lineSpacing(4)
21+
.overlay(alignment: .topLeading) {
22+
if index == 0, suggestion.commonPrecedingSpaceCount > 0 {
23+
Text("\(suggestion.commonPrecedingSpaceCount + 1)")
24+
.padding(.top, -12)
25+
.font(.footnote)
26+
.foregroundStyle(colorScheme == .dark ? .white : .black)
27+
.opacity(0.3)
28+
}
29+
}
2130
}
2231
}
2332
}
2433
.foregroundColor(.white)
2534
.font(.system(size: 12, design: .monospaced))
26-
.padding()
35+
.padding(.leading, 4)
36+
.padding([.trailing, .top, .bottom])
2737
}
2838
}
2939

@@ -171,7 +181,7 @@ struct CodeBlockSuggestionPanel_Dark_Objc_Preview: PreviewProvider {
171181

172182
struct CodeBlockSuggestionPanel_Bright_Objc_Preview: PreviewProvider {
173183
static var previews: some View {
174-
CodeBlockSuggestionPanel(suggestion:SuggestionProvider(
184+
CodeBlockSuggestionPanel(suggestion: SuggestionProvider(
175185
code: """
176186
- (void)addSubview:(UIView *)view {
177187
[self addSubview:view];

Core/Sources/SuggestionWidget/SuggestionProvider.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,34 @@ public final class SuggestionProvider: ObservableObject {
55
@Published public var code: String = "" {
66
didSet { highlightedCode = nil }
77
}
8+
89
@Published public var language: String = "" {
910
didSet { highlightedCode = nil }
1011
}
12+
1113
@Published public var startLineIndex: Int = 0
1214
@Published public var suggestionCount: Int = 0
1315
@Published public var currentSuggestionIndex: Int = 0
14-
16+
@Published public var commonPrecedingSpaceCount = 0
17+
1518
private var colorScheme: ColorScheme = .light
16-
private var highlightedCode: [NSAttributedString]? = nil
19+
private var highlightedCode: [NSAttributedString]?
20+
1721
func highlightedCode(colorScheme: ColorScheme) -> [NSAttributedString] {
1822
if colorScheme != self.colorScheme { highlightedCode = nil }
1923
self.colorScheme = colorScheme
2024
if let highlightedCode { return highlightedCode }
21-
let new = highlighted(
25+
let (new, spaceCount) = highlighted(
2226
code: code,
2327
language: language,
24-
brightMode: colorScheme != .dark
28+
brightMode: colorScheme != .dark,
29+
droppingLeadingSpaces: UserDefaults.shared
30+
.value(for: \.hideCommonPrecedingSpacesInSuggestion)
2531
)
2632
highlightedCode = new
33+
Task { @MainActor in
34+
commonPrecedingSpaceCount = spaceCount
35+
}
2736
return new
2837
}
2938

Lines changed: 80 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import AppKit
22
import Foundation
33
import Highlightr
4-
import Splash
54
import SwiftUI
65
import XPCShared
76

@@ -11,76 +10,39 @@ func highlightedCodeBlock(
1110
brightMode: Bool,
1211
fontSize: Double
1312
) -> NSAttributedString {
14-
switch language {
15-
case "swift":
16-
let plainTextColor = brightMode
17-
? .black
18-
: #colorLiteral(red: 0.6509803922, green: 0.6980392157, blue: 0.7529411765, alpha: 1)
19-
let highlighter =
20-
SyntaxHighlighter(
21-
format: AttributedStringOutputFormat(theme: .init(
22-
font: .init(size: 14),
23-
plainTextColor: plainTextColor,
24-
tokenColors: brightMode
25-
? [
26-
.keyword: #colorLiteral(red: 0.6078431373, green: 0.137254902, blue: 0.5764705882, alpha: 1),
27-
.string: #colorLiteral(red: 0.1371159852, green: 0.3430536985, blue: 0.362406373, alpha: 1),
28-
.type: #colorLiteral(red: 0.2456904352, green: 0.5002114773, blue: 0.5297455192, alpha: 1),
29-
.call: #colorLiteral(red: 0.1960784314, green: 0.4274509804, blue: 0.4549019608, alpha: 1),
30-
.number: #colorLiteral(red: 0.4385872483, green: 0.4995297194, blue: 0.5483990908, alpha: 1),
31-
.comment: #colorLiteral(red: 0.3647058824, green: 0.4235294118, blue: 0.4745098039, alpha: 1),
32-
.property: #colorLiteral(red: 0.1960784314, green: 0.4274509804, blue: 0.4549019608, alpha: 1),
33-
.dotAccess: #colorLiteral(red: 0.1960784314, green: 0.4274509804, blue: 0.4549019608, alpha: 1),
34-
.preprocessing: #colorLiteral(red: 0.3921568627, green: 0.2196078431, blue: 0.1254901961, alpha: 1),
35-
] : [
36-
.keyword: #colorLiteral(red: 0.8258609176, green: 0.5708742738, blue: 0.8922662139, alpha: 1),
37-
.string: #colorLiteral(red: 0.6253595352, green: 0.7963448763, blue: 0.5427476764, alpha: 1),
38-
.type: #colorLiteral(red: 0.9221783876, green: 0.7978314757, blue: 0.5575165749, alpha: 1),
39-
.call: #colorLiteral(red: 0.4466812611, green: 0.742190659, blue: 0.9515134692, alpha: 1),
40-
.number: #colorLiteral(red: 0.8620631099, green: 0.6468816996, blue: 0.4395158887, alpha: 1),
41-
.comment: #colorLiteral(red: 0.4233166873, green: 0.4612616301, blue: 0.5093258619, alpha: 1),
42-
.property: #colorLiteral(red: 0.906378448, green: 0.5044228435, blue: 0.5263597369, alpha: 1),
43-
.dotAccess: #colorLiteral(red: 0.906378448, green: 0.5044228435, blue: 0.5263597369, alpha: 1),
44-
.preprocessing: #colorLiteral(red: 0.3776347041, green: 0.8792117238, blue: 0.4709561467, alpha: 1),
45-
]
46-
))
47-
)
48-
let formatted = NSMutableAttributedString(attributedString: highlighter.highlight(code))
49-
formatted.addAttributes(
50-
[.font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular)],
51-
range: NSRange(location: 0, length: formatted.length)
13+
var language = language
14+
if language == "objective-c" {
15+
language = "objectivec"
16+
}
17+
func unhighlightedCode() -> NSAttributedString {
18+
return NSAttributedString(
19+
string: code,
20+
attributes: [
21+
.foregroundColor: brightMode ? NSColor.black : NSColor.white,
22+
.font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular),
23+
]
5224
)
53-
return formatted
54-
default:
55-
var language = language
56-
if language == "objective-c" {
57-
language = "objectivec"
58-
}
59-
func unhighlightedCode() -> NSAttributedString {
60-
return NSAttributedString(
61-
string: code,
62-
attributes: [
63-
.foregroundColor: brightMode ? NSColor.black : NSColor.white,
64-
.font: NSFont.monospacedSystemFont(ofSize: fontSize, weight: .regular),
65-
]
66-
)
67-
}
68-
guard let highlighter = Highlightr() else {
69-
return unhighlightedCode()
70-
}
71-
highlighter.setTheme(to: brightMode ? "xcode" : "atom-one-dark")
72-
highlighter.theme.setCodeFont(.monospacedSystemFont(ofSize: fontSize, weight: .regular))
73-
guard let formatted = highlighter.highlight(code, as: language) else {
74-
return unhighlightedCode()
75-
}
76-
if formatted.string == "undefined" {
77-
return unhighlightedCode()
78-
}
79-
return formatted
8025
}
26+
guard let highlighter = Highlightr() else {
27+
return unhighlightedCode()
28+
}
29+
highlighter.setTheme(to: brightMode ? "xcode" : "atom-one-dark")
30+
highlighter.theme.setCodeFont(.monospacedSystemFont(ofSize: fontSize, weight: .regular))
31+
guard let formatted = highlighter.highlight(code, as: language) else {
32+
return unhighlightedCode()
33+
}
34+
if formatted.string == "undefined" {
35+
return unhighlightedCode()
36+
}
37+
return formatted
8138
}
8239

83-
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) {
8446
let formatted = highlightedCodeBlock(
8547
code: code,
8648
language: language,
@@ -90,21 +52,67 @@ func highlighted(code: String, language: String, brightMode: Bool) -> [NSAttribu
9052
let middleDotColor = brightMode
9153
? NSColor.black.withAlphaComponent(0.1)
9254
: NSColor.white.withAlphaComponent(0.1)
93-
return convertToCodeLines(formatted, middleDotColor: middleDotColor)
55+
return convertToCodeLines(
56+
formatted,
57+
middleDotColor: middleDotColor,
58+
droppingLeadingSpaces: droppingLeadingSpaces
59+
)
9460
}
9561

96-
private func convertToCodeLines(
62+
func convertToCodeLines(
9763
_ formattedCode: NSAttributedString,
98-
middleDotColor: NSColor
99-
) -> [NSAttributedString] {
64+
middleDotColor: NSColor,
65+
droppingLeadingSpaces: Bool
66+
) -> (code: [NSAttributedString], commonLeadingSpaceCount: Int) {
10067
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+
10180
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+
}()
10296
var output = [NSAttributedString]()
10397
var start = 0
10498
for sub in separatedInput {
10599
let range = NSMakeRange(start, sub.utf16.count)
106100
let attributedString = formattedCode.attributedSubstring(from: range)
107101
let mutable = NSMutableAttributedString(attributedString: attributedString)
102+
103+
// remove leading spaces
104+
if commonLeadingSpaceCount > 0 {
105+
let leadingSpaces = String(repeating: " ", count: commonLeadingSpaceCount)
106+
if mutable.string.hasPrefix(leadingSpaces) {
107+
mutable.replaceCharacters(
108+
in: NSRange(location: 0, length: commonLeadingSpaceCount),
109+
with: ""
110+
)
111+
} else if isEmptyLine(mutable.string) {
112+
mutable.mutableString.setString("")
113+
}
114+
}
115+
108116
// use regex to replace all spaces to a middle dot
109117
do {
110118
let regex = try NSRegularExpression(pattern: "[ ]*", options: [])
@@ -126,5 +134,5 @@ private func convertToCodeLines(
126134
output.append(mutable)
127135
start += range.length + 1
128136
}
129-
return output
137+
return (output, commonLeadingSpaceCount)
130138
}

Core/Sources/SuggestionWidget/WidgetView.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ struct WidgetContextMenu: View {
9393
@AppStorage(\.useGlobalChat) var useGlobalChat
9494
@AppStorage(\.realtimeSuggestionToggle) var realtimeSuggestionToggle
9595
@AppStorage(\.acceptSuggestionWithAccessibilityAPI) var acceptSuggestionWithAccessibilityAPI
96+
@AppStorage(\.hideCommonPrecedingSpacesInSuggestion) var hideCommonPrecedingSpacesInSuggestion
9697

9798
var body: some View {
9899
Group {
@@ -123,6 +124,15 @@ struct WidgetContextMenu: View {
123124
}
124125
})
125126

127+
Button(action: {
128+
hideCommonPrecedingSpacesInSuggestion.toggle()
129+
}, label: {
130+
Text("Hide Common Preceding Spaces in Suggestion")
131+
if hideCommonPrecedingSpacesInSuggestion {
132+
Image(systemName: "checkmark")
133+
}
134+
})
135+
126136
Divider()
127137

128138
Button(action: {

0 commit comments

Comments
 (0)