Commit e9c405b56e
e9c405b56e633d52ee668a1c4fa82d048d9888eb
parent: b4a6bd651f
Unsigned
cmc <hello@cleberg.net> · 2026-09-30 03:16 UTC
Link references in review comments; prepare org bodies (!146)
- Review thread comments render through `FormattedBodyView` (Markdown, as the web renders them; the forge sends no `body_format` for them), so `#N`, `!N` and `owner/name#N` link in-app relative to the MR's repo. `DiffView` gets `.referenceNavigation()` for the threads it shows.
- `linkReferences(in:repo:)` moves from `MarkdownView` to `BodyReferences.swift`; new `BodyReferenceOrgTests` run it over OrgSwift's attributed output (verbatim, code and existing links skipped).
Org bodies: OrgView has no hook, so krz/org-swift!9 adds `textTransform`. The wiring is on branch `org-body-refs` (one commit on top of this one), which bumps the org-swift requirement to 0.7.0 and needs that tag before it builds.
574 unit tests, 0 failures; Release build compiles.
Layout: unified · split
gitbay/Views/MRs/DiffView.swift
+1
| @@ -43,6 +43,7 @@ struct DiffView: View { |
| 43 | .navigationBarTitleDisplayMode(.inline) |
43 | .navigationBarTitleDisplayMode(.inline) |
| 44 | .task { await model.load() } |
44 | .task { await model.load() } |
| 45 | .refreshable { await model.load() } |
45 | .refreshable { await model.load() } |
| |
46 | .referenceNavigation() |
| 46 | } |
47 | } |
| 47 | } |
48 | } |
| 48 | |
49 | |
gitbay/Views/MRs/MRView.swift
+2 −1
| @@ -689,7 +689,8 @@ struct ReviewThreadView: View { |
| 689 | .font(.gbSans(.caption2)) |
689 | .font(.gbSans(.caption2)) |
| 690 | .foregroundStyle(.tertiary) |
690 | .foregroundStyle(.tertiary) |
| 691 | } |
691 | } |
| 692 | Text(comment.body) |
692 | // Review comments carry no format; the web renders them as Markdown. |
| |
693 | FormattedBodyView(text: comment.body, repo: model.repoPath) |
| 693 | .font(.gbSans(.subheadline)) |
694 | .font(.gbSans(.subheadline)) |
| 694 | } |
695 | } |
| 695 | } |
696 | } |
gitbay/Views/Repos/MarkdownView.swift
−16
| @@ -326,22 +326,6 @@ struct MarkdownView: View { |
| 326 | return attributed |
326 | return attributed |
| 327 | } |
327 | } |
| 328 | |
328 | |
| 329 | /// Links `#N`, `!N` and `owner/name#N` outside code spans and |
| |
| 330 | /// existing links. Fenced code never reaches here. |
| |
| 331 | private static func linkReferences(in attributed: inout AttributedString, repo: String) { |
| |
| 332 | for reference in bodyReferences(in: String(attributed.characters), repo: repo) { |
| |
| 333 | let lower = attributed.characters.index(attributed.startIndex, |
| |
| 334 | offsetBy: reference.range.lowerBound) |
| |
| 335 | let upper = attributed.characters.index(lower, offsetBy: reference.range.count) |
| |
| 336 | let skip = attributed[lower..<upper].runs.contains { |
| |
| 337 | $0.link != nil || $0.inlinePresentationIntent?.contains(.code) == true |
| |
| 338 | } |
| |
| 339 | if !skip, let url = referenceURL(reference.destination) { |
| |
| 340 | attributed[lower..<upper].link = url |
| |
| 341 | } |
| |
| 342 | } |
| |
| 343 | } |
| |
| 344 | |
| |
| 345 | private func headingFont(_ level: Int) -> Font { |
329 | private func headingFont(_ level: Int) -> Font { |
| 346 | switch level { |
330 | switch level { |
| 347 | case 1: .title2.bold() |
331 | case 1: .title2.bold() |
gitbay/Views/Shared/BodyReferences.swift
+17
| @@ -42,6 +42,23 @@ nonisolated func bodyReferences(in text: String, repo: String) -> [BodyReference |
| 42 | return found.sorted { $0.range.lowerBound < $1.range.lowerBound } |
42 | return found.sorted { $0.range.lowerBound < $1.range.lowerBound } |
| 43 | } |
43 | } |
| 44 | |
44 | |
| |
45 | /// Links `#N`, `!N` and `owner/name#N` outside code spans and existing |
| |
46 | /// links. Code blocks never reach here: Markdown fences are split off |
| |
47 | /// before inline rendering and org src/example blocks are not prose. |
| |
48 | nonisolated func linkReferences(in attributed: inout AttributedString, repo: String) { |
| |
49 | for reference in bodyReferences(in: String(attributed.characters), repo: repo) { |
| |
50 | let lower = attributed.characters.index(attributed.startIndex, |
| |
51 | offsetBy: reference.range.lowerBound) |
| |
52 | let upper = attributed.characters.index(lower, offsetBy: reference.range.count) |
| |
53 | let skip = attributed[lower..<upper].runs.contains { |
| |
54 | $0.link != nil || $0.inlinePresentationIntent?.contains(.code) == true |
| |
55 | } |
| |
56 | if !skip, let url = referenceURL(reference.destination) { |
| |
57 | attributed[lower..<upper].link = url |
| |
58 | } |
| |
59 | } |
| |
60 | } |
| |
61 | |
| 45 | nonisolated private let referenceScheme = "gitbay-ref" |
62 | nonisolated private let referenceScheme = "gitbay-ref" |
| 46 | |
63 | |
| 47 | /// The link a reference carries: the web path under an app-only scheme, |
64 | /// The link a reference carries: the web path under an app-only scheme, |
gitbayTests/BodyReferenceTests.swift
+32
| @@ -1,4 +1,5 @@ |
| 1 | import Foundation |
1 | import Foundation |
| |
2 | import OrgSwift |
| 2 | import Testing |
3 | import Testing |
| 3 | @testable import gitbay |
4 | @testable import gitbay |
| 4 | |
5 | |
| @@ -89,3 +90,34 @@ struct BodyReferenceMarkdownTests { |
| 89 | #expect(MarkdownView.attributed("#12").runs.allSatisfy { $0.link == nil }) |
90 | #expect(MarkdownView.attributed("#12").runs.allSatisfy { $0.link == nil }) |
| 90 | } |
91 | } |
| 91 | } |
92 | } |
| |
93 | |
| |
94 | /// Org bodies reach the transform as `OrgSwiftUI` hands it prose: verbatim |
| |
95 | /// and code carry the `.code` intent, links carry `.link`. |
| |
96 | struct BodyReferenceOrgTests { |
| |
97 | |
| |
98 | private func links(_ org: String) -> [String: URL] { |
| |
99 | guard case .paragraph(let objects) = OrgParser.parse(org).elements.first else { return [:] } |
| |
100 | var attributed = OrgAttributedStringRenderer().inline(objects) |
| |
101 | linkReferences(in: &attributed, repo: "krz/gitbay") |
| |
102 | var found: [String: URL] = [:] |
| |
103 | for run in attributed.runs { |
| |
104 | if let link = run.link { |
| |
105 | found[String(attributed[run.range].characters)] = link |
| |
106 | } |
| |
107 | } |
| |
108 | return found |
| |
109 | } |
| |
110 | |
| |
111 | @Test func referencesBecomeLinks() { |
| |
112 | let found = links("closes #12, see krz/solar!3") |
| |
113 | #expect(found["#12"] == referenceURL(.issue(repo: "krz/gitbay", number: 12))) |
| |
114 | #expect(found["krz/solar!3"] == referenceURL(.mr(repo: "krz/solar", number: 3))) |
| |
115 | } |
| |
116 | |
| |
117 | @Test func verbatimCodeAndExistingLinksAreLeftAlone() { |
| |
118 | let found = links("=#12= ~!4~ and [[https://example.com][#13]] but *#14*") |
| |
119 | #expect(found.count == 2) |
| |
120 | #expect(found["#13"] == URL(string: "https://example.com")) |
| |
121 | #expect(found["#14"] == referenceURL(.issue(repo: "krz/gitbay", number: 14))) |
| |
122 | } |
| |
123 | } |