Commit 54f83256b5

54f83256b552959a29ee327f55728a9cc75070ba

parent: a615251f3a

Verified · cmc

cmc <hello@cleberg.net> · 2026-10-14 06:07 UTC

Definitions: one id for each code line; a full-width mark

The file view's rows are scrolled to by the index they are drawn with,
so a line far down a long file is found before it is drawn. The target
line's mark spans the row, and only that row carries an identifier and
the selected trait. A symbol's identity includes its kind, since two
can share a line.

Ref #28

Layout: unified · split

gitbay/Repos/SymbolSearchViewModel.swift +1 −1
@@ -12,7 +12,7 @@ final class SymbolSearchViewModel {
12 let kind: String 12 let kind: String
13 let path: String 13 let path: String
14 let line: Int 14 let line: Int
15 var id: String { "\(path):\(line):\(name)" } 15 var id: String { "\(path):\(line):\(kind):\(name)" }
16 } 16 }
17 17
18 /// The kinds `--kind` takes, as the command's help lists them. 18 /// The kinds `--kind` takes, as the command's help lists them.
gitbay/Views/Repos/FileView.swift +23 −4
@@ -107,12 +107,15 @@ private struct CodeScrollView: View {
107 ScrollViewReader { proxy in 107 ScrollViewReader { proxy in
108 ScrollView([.horizontal, .vertical]) { 108 ScrollView([.horizontal, .vertical]) {
109 LazyVStack(alignment: .leading, spacing: 0) { 109 LazyVStack(alignment: .leading, spacing: 0) {
110 // The row's identity is its 0-based index, the one
111 // scrollTo names: a lazy row not yet drawn has no other.
110 ForEach(lines.indices, id: \.self) { index in 112 ForEach(lines.indices, id: \.self) { index in
113 let target = index + 1 == line
111 Text(lines[index].characters.isEmpty ? " " : lines[index]) 114 Text(lines[index].characters.isEmpty ? " " : lines[index])
112 .fixedSize() 115 .fixedSize()
113 .background(index + 1 == line ? Color.gbMark.opacity(0.18) : .clear) 116 .frame(maxWidth: .infinity, alignment: .leading)
114 .id(index + 1) 117 .background(target ? Color.gbMark.opacity(0.18) : .clear)
115 .accessibilityIdentifier(index + 1 == line ? "file-target-line" : "") 118 .modifier(TargetLine(on: target))
116 } 119 }
117 } 120 }
118 .padding(12) 121 .padding(12)
@@ -125,7 +128,7 @@ private struct CodeScrollView: View {
125 // A frame for the highlighted lines to exist before the 128 // A frame for the highlighted lines to exist before the
126 // jump, or the lazy stack has nothing to scroll to. 129 // jump, or the lazy stack has nothing to scroll to.
127 await Task.yield() 130 await Task.yield()
128 proxy.scrollTo(line, anchor: UnitPoint(x: 0, y: 0.3)) 131 proxy.scrollTo(line - 1, anchor: UnitPoint(x: 0, y: 0.3))
129 } 132 }
130 } 133 }
131 } 134 }
@@ -141,6 +144,22 @@ private struct CodeScrollView: View {
141 } 144 }
142} 145}
143 146
147/// The line a definition or a match opened at: named for VoiceOver and
148/// for UI tests, and left alone on every other row.
149private struct TargetLine: ViewModifier {
150 let on: Bool
151
152 func body(content: Content) -> some View {
153 if on {
154 content
155 .accessibilityIdentifier("file-target-line")
156 .accessibilityAddTraits(.isSelected)
157 } else {
158 content
159 }
160 }
161}
162
144/// A file's lines, each keeping its highlighting. A trailing newline ends 163/// A file's lines, each keeping its highlighting. A trailing newline ends
145/// the last line rather than starting an empty one. 164/// the last line rather than starting an empty one.
146nonisolated func codeLines(_ source: NSAttributedString) -> [AttributedString] { 165nonisolated func codeLines(_ source: NSAttributedString) -> [AttributedString] {
gitbayTests/SymbolTests.swift +1 −1
@@ -31,7 +31,7 @@ struct SymbolSearchTests {
31 await model.search(" Dispatch ") 31 await model.search(" Dispatch ")
32 32
33 #expect(argv(try #require(stub.seen.first).url) == ["repo", "symbols", "krz/gitbay", "Dispatch"]) 33 #expect(argv(try #require(stub.seen.first).url) == ["repo", "symbols", "krz/gitbay", "Dispatch"])
34 #expect(model.state?.value?.map(\.id) == ["internal/control/control.go:120:Dispatch", "internal/control/test.go:9:DispatchIn"]) 34 #expect(model.state?.value?.map(\.id) == ["internal/control/control.go:120:function:Dispatch", "internal/control/test.go:9:function:DispatchIn"])
35 } 35 }
36 36
37 @Test func aKindNarrowsTheSearch() async throws { 37 @Test func aKindNarrowsTheSearch() async throws {