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 {
4343 .navigationBarTitleDisplayMode(.inline)
4444 .task { await model.load() }
4545 .refreshable { await model.load() }
46 .referenceNavigation()
4647 }
4748}
4849
gitbay/Views/MRs/MRView.swift +2 −1
@@ -689,7 +689,8 @@ struct ReviewThreadView: View {
689689 .font(.gbSans(.caption2))
690690 .foregroundStyle(.tertiary)
691691 }
692 Text(comment.body)
692 // Review comments carry no format; the web renders them as Markdown.
693 FormattedBodyView(text: comment.body, repo: model.repoPath)
693694 .font(.gbSans(.subheadline))
694695 }
695696 }
gitbay/Views/Repos/MarkdownView.swift −16
@@ -326,22 +326,6 @@ struct MarkdownView: View {
326326 return attributed
327327 }
328328
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
345329 private func headingFont(_ level: Int) -> Font {
346330 switch level {
347331 case 1: .title2.bold()
gitbay/Views/Shared/BodyReferences.swift +17
@@ -42,6 +42,23 @@ nonisolated func bodyReferences(in text: String, repo: String) -> [BodyReference
4242 return found.sorted { $0.range.lowerBound < $1.range.lowerBound }
4343}
4444
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.
48nonisolated 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
4562nonisolated private let referenceScheme = "gitbay-ref"
4663
4764/// The link a reference carries: the web path under an app-only scheme,
gitbayTests/BodyReferenceTests.swift +32
@@ -1,4 +1,5 @@
11import Foundation
2import OrgSwift
23import Testing
34@testable import gitbay
45
@@ -89,3 +90,34 @@ struct BodyReferenceMarkdownTests {
8990 #expect(MarkdownView.attributed("#12").runs.allSatisfy { $0.link == nil })
9091 }
9192}
93
94/// Org bodies reach the transform as `OrgSwiftUI` hands it prose: verbatim
95/// and code carry the `.code` intent, links carry `.link`.
96struct 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}