Skip to content

Commit b503b3e

Browse files
committed
fix(mac): stabilize workflows and diagnostics
1 parent 2491e39 commit b503b3e

34 files changed

Lines changed: 1015 additions & 535 deletions

shell/mac/Info.plist

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
<false/>
3434
<key>SUEnableAutomaticChecks</key>
3535
<true/>
36+
<key>SUEnableSystemProfiling</key>
37+
<false/>
3638
<key>SUFeedURL</key>
3739
<string>https://raw.githubusercontent.com/hewigovens/jayjay/main/docs/appcast.xml</string>
3840
<key>SUPublicEDKey</key>

shell/mac/Packages/JayJayDiffUI/Sources/JayJayDiffUI/DiffPlaceholders.swift

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,21 @@
11
import JayJayCore
22

3+
/// Nil-handling facade over jayjay-core's placeholder predicates (`crate::placeholder`),
4+
/// so the editable-vs-placeholder rule cannot drift from core.
35
public enum DiffPlaceholder {
4-
private static let nonEditablePrefixes = [
5-
"<binary file",
6-
"<directory>",
7-
"<git lfs ",
8-
"<git submodule",
9-
"<conflict",
10-
"<access denied"
11-
]
12-
136
public static func isEditableText(_ text: String?) -> Bool {
147
guard let text else { return true }
15-
return !nonEditablePrefixes.contains(where: text.hasPrefix)
8+
return isEditableDiffText(text: text)
169
}
1710

1811
public static func isGitLfs(_ text: String?) -> Bool {
19-
hasPrefix(text, "<git lfs ")
12+
guard let text else { return false }
13+
return isGitLfsPlaceholder(text: text)
2014
}
2115

2216
public static func isGitSubmodule(_ text: String?) -> Bool {
23-
hasPrefix(text, "<git submodule")
24-
}
25-
26-
private static func hasPrefix(_ text: String?, _ prefix: String) -> Bool {
27-
text?.hasPrefix(prefix) == true
17+
guard let text else { return false }
18+
return isGitSubmodulePlaceholder(text: text)
2819
}
2920
}
3021

shell/mac/Packages/JayJayDiffUI/Tests/JayJayDiffUITests/DiffConflictRenderingTests.swift

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,10 @@ final class DiffConflictRenderingTests: XCTestCase {
3838
assertSameColor(theme.lineText(header), theme.conflictHeaderText)
3939
}
4040

41-
func test_diffDisplayItemsExposeConflictBlock() {
41+
/// Conflict-block structure (title/range/section labels) is asserted in Rust
42+
/// (jj-diff builds_first_class_conflict_blocks); here we cover the production
43+
/// display pipeline the gutter/SBS views actually call through FFI.
44+
func test_conflictDisplayLinesCollapseToProductionRendering() {
4245
let lines = [
4346
line("<<<<<<< conflict 1 of 1", kind: .start, newLineNo: 1),
4447
line("base", kind: .removed, style: .removed, newLineNo: 2),
@@ -47,19 +50,6 @@ final class DiffConflictRenderingTests: XCTestCase {
4750
line(">>>>>>> conflict 1 of 1 ends", kind: .end, newLineNo: 5)
4851
]
4952

50-
let items = diffDisplayItems(lines: lines)
51-
52-
XCTAssertEqual(items.count, 1)
53-
guard case let .conflictBlock(block) = items.first else {
54-
XCTFail("Expected a conflict block display item")
55-
return
56-
}
57-
58-
XCTAssertEqual(block.title, "Conflict 1 of 1")
59-
XCTAssertEqual(block.lineStart, 0)
60-
XCTAssertEqual(block.lineEnd, 5)
61-
XCTAssertEqual(block.sections.map(\.label), ["Conflict 1 of 1", "Side #1", "End Conflict 1 of 1"])
62-
6353
let displayLines = diffDisplayLines(lines: lines)
6454
XCTAssertEqual(displayLines.count, 3)
6555
XCTAssertEqual(displayLines[0].rawText, "Conflict 1 of 1 · ◆ Side #1")

shell/mac/Sources/JayJay/App/Config/AppSettings.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import Foundation
22

33
@Observable
44
final class AppSettings {
5+
/// UserDefaults key for the anonymous-stats opt-in; the Sparkle feed delegate reads it too.
6+
static let sendsAnonymousStatsKey = "jayjay.sendsAnonymousStats"
7+
58
private enum StorageKeys {
69
static let fontFamily = "jayjay.fontFamily"
710
static let fontSize = "jayjay.fontSize"
@@ -140,6 +143,13 @@ final class AppSettings {
140143
didSet { defaults.set(sponsorNextPromptCount, forKey: StorageKeys.sponsorNextPromptCount) }
141144
}
142145

146+
// MARK: - Privacy
147+
148+
/// Route Sparkle update checks through the telemetry worker when opted in.
149+
var sendsAnonymousStats: Bool {
150+
didSet { defaults.set(sendsAnonymousStats, forKey: Self.sendsAnonymousStatsKey) }
151+
}
152+
143153
// MARK: - Init
144154

145155
private let defaults: UserDefaults
@@ -168,6 +178,7 @@ final class AppSettings {
168178
sponsorActionCount = defaults.integer(forKey: StorageKeys.sponsorActionCount)
169179
sponsorDismissed = defaults.bool(forKey: StorageKeys.sponsorDismissed)
170180
sponsorNextPromptCount = max(defaults.integer(forKey: StorageKeys.sponsorNextPromptCount), 5)
181+
sendsAnonymousStats = defaults.object(forKey: Self.sendsAnonymousStatsKey) as? Bool ?? false
171182
}
172183

173184
// MARK: - Repo helpers

shell/mac/Sources/JayJay/App/JayJayApp.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ struct JayJayApp: App {
129129

130130
Window("About JayJay", id: AppWindows.about) {
131131
AboutView()
132+
.environment(settings)
132133
.environment(\.jayjayFontSize, settings.fontSize)
133134
.environment(\.jayjayFontFamily, settings.fontFamily)
134135
.preferredColorScheme(settings.appearanceMode.colorScheme)

shell/mac/Sources/JayJay/App/SparkleUpdater.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import SwiftUI
33

44
final class SparkleUpdater: ObservableObject {
55
private let controller: SPUStandardUpdaterController
6+
private let feedDelegate = UpdaterFeedDelegate()
67

78
init() {
8-
controller = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil)
9+
controller = SPUStandardUpdaterController(
10+
startingUpdater: true,
11+
updaterDelegate: feedDelegate,
12+
userDriverDelegate: nil
13+
)
914
}
1015

1116
func checkForUpdates() {
@@ -21,3 +26,15 @@ final class SparkleUpdater: ObservableObject {
2126
set { controller.updater.automaticallyChecksForUpdates = newValue }
2227
}
2328
}
29+
30+
/// Routes the appcast through the telemetry worker only when the user opts in;
31+
/// when off, checks hit the direct appcast so no request reaches the worker.
32+
private final class UpdaterFeedDelegate: NSObject, SPUUpdaterDelegate {
33+
private static let direct = "https://raw.githubusercontent.com/hewigovens/jayjay/main/docs/appcast.xml"
34+
private static let stats = "https://jayjay.hewigovens.workers.dev/appcast.xml"
35+
36+
func feedURLString(for _: SPUUpdater) -> String? {
37+
let enabled = UserDefaults.standard.object(forKey: AppSettings.sendsAnonymousStatsKey) as? Bool ?? false
38+
return enabled ? Self.stats : Self.direct
39+
}
40+
}

shell/mac/Sources/JayJay/App/Window/RepositoryCommands.swift

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,27 @@ struct RepositoryCommands: Commands {
8080
}
8181
}
8282

83-
/// Open the origin remote's web page. Core normalizes scp/ssh/git/https
84-
/// remotes to an https URL, so we never hand ssh:// to the system (Terminal).
83+
/// Open the origin remote's web page. Core normalizes ssh/scp remotes to https so we
84+
/// never hand ssh:// to the system; the blocking open+resolve runs off the main thread.
8585
static func openRemoteRepository(at path: String) {
86-
guard let repo = try? JayJayRepo.open(path: path),
87-
let webURL = repo.remoteWebUrl().flatMap(URL.init(string:))
88-
else { return }
89-
NSWorkspace.shared.open(webURL)
86+
Task.detached {
87+
guard let repo = try? JayJayRepo.open(path: path),
88+
let url = repo.remoteWebUrl().flatMap(URL.init(string:))
89+
else { return }
90+
await MainActor.run {
91+
_ = NSWorkspace.shared.open(url)
92+
}
93+
}
94+
}
95+
96+
/// Variant for call sites holding a live repo, avoiding a redundant `JayJayRepo.open`.
97+
@MainActor
98+
static func openRemoteRepository(repo: JayJayRepo) {
99+
Task.detached { [repo] in
100+
guard let url = repo.remoteWebUrl().flatMap(URL.init(string:)) else { return }
101+
await MainActor.run {
102+
_ = NSWorkspace.shared.open(url)
103+
}
104+
}
90105
}
91106
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import JayJayCore
2+
import JayJayDiffUI
3+
import SwiftUI
4+
5+
extension DiffSection: DiffGutterEditActions {
6+
var currentSelectedLineRange: ClosedRange<Int>? {
7+
selectedLineRange
8+
}
9+
10+
var canOpenDiffEdit: Bool {
11+
onOpenDiffEdit != nil
12+
}
13+
14+
var canAbandonSelectedLines: Bool {
15+
isWorkingCopy
16+
}
17+
18+
func didSelectLines(_ lineRange: ClosedRange<Int>) {
19+
selectedLineRange = lineRange
20+
}
21+
22+
func openDiffEdit() {
23+
onOpenDiffEdit?()
24+
}
25+
26+
func abandonSelectedLines(in lineRange: ClosedRange<Int>) {
27+
abandonSelectedLines(lineRange: lineRange)
28+
}
29+
30+
private func abandonSelectedLines(lineRange: ClosedRange<Int>) {
31+
guard let actions,
32+
let repo,
33+
let rev,
34+
let fileDiff
35+
else { return }
36+
37+
let oldContent = loadedOldContent ?? hunk.oldContent
38+
let newContent = loadedNewContent ?? hunk.newContent
39+
let selectedKeys: Set<String> = Set(
40+
fileDiff.lines.enumerated().compactMap { index, line in
41+
let displayLine = index + 1
42+
guard lineRange.contains(displayLine),
43+
line.style == .added || line.style == .removed
44+
else { return nil }
45+
return diffEditLineKey(line)
46+
}
47+
)
48+
guard !selectedKeys.isEmpty else { return }
49+
50+
let path = hunk.path
51+
let oldPath = hunk.oldPath
52+
let hunkType = hunk.hunkType
53+
let ignoreWhitespace = settings.ignoreWhitespace
54+
Task.detached { [repo] in
55+
let fullDiff = repo.computeNativeDiffFull(
56+
path: path,
57+
oldContent: oldContent ?? "",
58+
newContent: newContent ?? "",
59+
ignoreWhitespace: ignoreWhitespace
60+
)
61+
let fullLineIndices = fullDiff.lines.enumerated().compactMap { index, line in
62+
selectedKeys.contains(diffEditLineKey(line)) ? index + 1 : nil
63+
}
64+
let ranges = diffEditCollapsedRanges(fullLineIndices)
65+
guard !ranges.isEmpty else { return }
66+
67+
await MainActor.run {
68+
actions.applyDiffSelection(
69+
rev: rev,
70+
destination: .removeFromSource,
71+
selections: [
72+
DiffEditFileSelection(
73+
path: path,
74+
oldPath: oldPath,
75+
oldContent: oldContent,
76+
newContent: newContent,
77+
hunkType: hunkType,
78+
lineRanges: ranges
79+
)
80+
],
81+
message: "",
82+
ignoreWhitespace: ignoreWhitespace
83+
)
84+
}
85+
}
86+
}
87+
}
88+
89+
private func diffEditLineKey(_ line: DiffLine) -> String {
90+
let style = switch line.style {
91+
case .added: "added"
92+
case .removed: "removed"
93+
case .context: "context"
94+
case .separator: "separator"
95+
case .unchanged: "unchanged"
96+
}
97+
return "\(style)|\(line.oldLineNo.map(String.init) ?? "-")|\(line.newLineNo.map(String.init) ?? "-")"
98+
}
99+
100+
private func diffEditCollapsedRanges(_ indices: [Int]) -> [DiffEditRange] {
101+
guard let first = indices.first else { return [] }
102+
103+
var ranges: [DiffEditRange] = []
104+
var start = first
105+
var previous = first
106+
107+
for index in indices.dropFirst() {
108+
if index == previous + 1 {
109+
previous = index
110+
continue
111+
}
112+
ranges.append(DiffEditRange(startLine: UInt32(start), endLine: UInt32(previous)))
113+
start = index
114+
previous = index
115+
}
116+
117+
ranges.append(DiffEditRange(startLine: UInt32(start), endLine: UInt32(previous)))
118+
return ranges
119+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import JayJayCore
2+
import JayJayDiffUI
3+
import SwiftUI
4+
5+
extension DiffSection: DiffGutterReviewActions {
6+
var reviewCheckboxesEnabled: Bool {
7+
// Working-copy only; off in interdiff/compare mode.
8+
isWorkingCopy && reviewStore != nil && rev != nil
9+
&& !hunk.reviewIdentity.isEmpty && compareFromRev == nil
10+
}
11+
12+
func isHunkReviewed(groupIndex: UInt32) -> Bool {
13+
guard let reviewStore, let rev else { return false }
14+
return reviewStore.isHunkReviewed(
15+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity, hunkIndex: groupIndex
16+
)
17+
}
18+
19+
func toggleHunkReviewed(groupIndex: UInt32) {
20+
guard let reviewStore, let rev else { return }
21+
// Demoting a file-marked file: materialize the survivors so we don't drop them all.
22+
if reviewStore.isReviewed(changeId: rev, path: hunk.path, identity: hunk.reviewIdentity),
23+
let total = totalChangeGroupCount, total > 0
24+
{
25+
let kept = (0 ..< UInt32(total)).filter { $0 != groupIndex }
26+
reviewStore.setReviewedHunks(
27+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity, hunkIndices: kept
28+
)
29+
} else {
30+
reviewStore.toggleHunkReviewed(
31+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity, hunkIndex: groupIndex
32+
)
33+
promoteFileMarkIfAllReviewed()
34+
}
35+
// ChangeDetailView caches reviewedPaths in @State; nudge it to recompute.
36+
onReviewStateChanged?()
37+
}
38+
39+
private func promoteFileMarkIfAllReviewed() {
40+
guard let reviewStore, let rev,
41+
let total = totalChangeGroupCount, total > 0
42+
else { return }
43+
let allReviewed = (0 ..< UInt32(total)).allSatisfy {
44+
reviewStore.isHunkReviewed(
45+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity, hunkIndex: $0
46+
)
47+
}
48+
let alreadyMarked = reviewStore.isReviewed(
49+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity
50+
)
51+
if allReviewed, !alreadyMarked {
52+
reviewStore.markReviewed(
53+
changeId: rev, path: hunk.path, identity: hunk.reviewIdentity
54+
)
55+
}
56+
}
57+
58+
/// Contiguous runs of added/removed lines. Nil until the diff has loaded.
59+
var totalChangeGroupCount: Int? {
60+
guard let lines = fileDiff?.lines else { return nil }
61+
var count = 0
62+
var inGroup = false
63+
for line in lines {
64+
let isChanged = line.style == .added || line.style == .removed
65+
if isChanged, !inGroup {
66+
count += 1
67+
inGroup = true
68+
} else if !isChanged, inGroup {
69+
inGroup = false
70+
}
71+
}
72+
return count
73+
}
74+
}

0 commit comments

Comments
 (0)