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.
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
45nonisolated private let referenceScheme = "gitbay-ref" 62nonisolated 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 @@
1import Foundation 1import Foundation
2import OrgSwift
2import Testing 3import 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`.
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}