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 | 12 | let kind: String |
| 13 | 13 | let path: String |
| 14 | 14 | let line: Int |
| 15 | | var id: String { "\(path):\(line):\(name)" } |
| 15 | var id: String { "\(path):\(line):\(kind):\(name)" } |
| 16 | 16 | } |
| 17 | 17 | |
| 18 | 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 | 107 | ScrollViewReader { proxy in |
| 108 | 108 | ScrollView([.horizontal, .vertical]) { |
| 109 | 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 | 112 | ForEach(lines.indices, id: \.self) { index in |
| 113 | let target = index + 1 == line |
| 111 | 114 | Text(lines[index].characters.isEmpty ? " " : lines[index]) |
| 112 | 115 | .fixedSize() |
| 113 | | .background(index + 1 == line ? Color.gbMark.opacity(0.18) : .clear) |
| 114 | | .id(index + 1) |
| 115 | | .accessibilityIdentifier(index + 1 == line ? "file-target-line" : "") |
| 116 | .frame(maxWidth: .infinity, alignment: .leading) |
| 117 | .background(target ? Color.gbMark.opacity(0.18) : .clear) |
| 118 | .modifier(TargetLine(on: target)) |
| 116 | 119 | } |
| 117 | 120 | } |
| 118 | 121 | .padding(12) |
| @@ -125,7 +128,7 @@ private struct CodeScrollView: View { |
| 125 | 128 | // A frame for the highlighted lines to exist before the |
| 126 | 129 | // jump, or the lazy stack has nothing to scroll to. |
| 127 | 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. |
| 149 | private 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 | 163 | /// A file's lines, each keeping its highlighting. A trailing newline ends |
| 145 | 164 | /// the last line rather than starting an empty one. |
| 146 | 165 | nonisolated func codeLines(_ source: NSAttributedString) -> [AttributedString] { |
gitbayTests/SymbolTests.swift
+1 −1
| @@ -31,7 +31,7 @@ struct SymbolSearchTests { |
| 31 | 31 | await model.search(" Dispatch ") |
| 32 | 32 | |
| 33 | 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 | 37 | @Test func aKindNarrowsTheSearch() async throws { |