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 | 43 | .navigationBarTitleDisplayMode(.inline) |
| 44 | 44 | .task { await model.load() } |
| 45 | 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 | 689 | .font(.gbSans(.caption2)) |
| 690 | 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 | 694 | .font(.gbSans(.subheadline)) |
| 694 | 695 | } |
| 695 | 696 | } |
gitbay/Views/Repos/MarkdownView.swift
−16
| @@ -326,22 +326,6 @@ struct MarkdownView: View { |
| 326 | 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 | 329 | private func headingFont(_ level: Int) -> Font { |
| 346 | 330 | switch level { |
| 347 | 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 | 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 | 62 | nonisolated private let referenceScheme = "gitbay-ref" |
| 46 | 63 | |
| 47 | 64 | /// The link a reference carries: the web path under an app-only scheme, |
gitbayTests/BodyReferenceTests.swift
+32
| @@ -1,4 +1,5 @@ |
| 1 | 1 | import Foundation |
| 2 | import OrgSwift |
| 2 | 3 | import Testing |
| 3 | 4 | @testable import gitbay |
| 4 | 5 | |
| @@ -89,3 +90,34 @@ struct BodyReferenceMarkdownTests { |
| 89 | 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 | } |