Commit 7aa3869f79

7aa3869f79c7664c3fef5d2a798f98fc014aa158

parent: 9b8ed56d54

Verified · cmc

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

Jump to a definition: link per drawn row; prose is not linked

Linking runs as a row is drawn rather than over the whole file when the
definitions arrive. Markdown, org and text files are not linked: a word
in a sentence that names a section would link everywhere it appears. A
one-letter name defined more than once opens its first definition,
since repo symbols takes two characters at least.

Ref #29

Layout: unified · split

gitbay/Repos/FileViewModel.swift +4
@@ -44,6 +44,10 @@ final class FileViewModel {
44 state = .from(error) 44 state = .from(error)
45 return 45 return
46 } 46 }
47 // Prose is not code: a word in it that names a section or a
48 // function would link from every sentence it appears in.
49 guard MarkupPreview.format(forFile: fileName) == nil,
50 !["txt", "text"].contains((fileName as NSString).pathExtension.lowercased()) else { return }
47 // After the file, which draws first; the links follow. 51 // After the file, which draws first; the links follow.
48 let found = (try? await client.readList(defs, of: Definition.self)) ?? [] 52 let found = (try? await client.readList(defs, of: Definition.self)) ?? []
49 definitions = Dictionary(found.map { ($0.name, $0) }, uniquingKeysWith: { a, _ in a }) 53 definitions = Dictionary(found.map { ($0.name, $0) }, uniquingKeysWith: { a, _ in a })
gitbay/Views/Repos/FileView.swift +8 −11
@@ -94,7 +94,9 @@ struct FileView: View {
94 } 94 }
95 // Defined once: that line, at the ref on screen, as the 95 // Defined once: that line, at the ref on screen, as the
96 // web links it. More often: the search, to choose. 96 // web links it. More often: the search, to choose.
97 if def.count == 1 { 97 // repo symbols takes two characters at least, so a
98 // one-letter name defined more often opens its first.
99 if def.count == 1 || name.count < 2 {
98 pushRoute(RepoRoute.file(repo: model.repoPath, path: def.path, ref: model.ref, line: def.line)) 100 pushRoute(RepoRoute.file(repo: model.repoPath, path: def.path, ref: model.ref, line: def.line))
99 } else { 101 } else {
100 pushRoute(RepoRoute.symbols(repo: model.repoPath, query: name)) 102 pushRoute(RepoRoute.symbols(repo: model.repoPath, query: name))
@@ -117,7 +119,6 @@ private struct CodeScrollView: View {
117 var line: Int? 119 var line: Int?
118 var definitions: [String: Definition] = [:] 120 var definitions: [String: Definition] = [:]
119 121
120 @State private var highlighted: [AttributedString] = []
121 @State private var lines: [AttributedString] = [] 122 @State private var lines: [AttributedString] = []
122 123
123 var body: some View { 124 var body: some View {
@@ -128,7 +129,10 @@ private struct CodeScrollView: View {
128 // scrollTo names: a lazy row not yet drawn has no other. 129 // scrollTo names: a lazy row not yet drawn has no other.
129 ForEach(lines.indices, id: \.self) { index in 130 ForEach(lines.indices, id: \.self) { index in
130 let target = index + 1 == line 131 let target = index + 1 == line
131 Text(lines[index].characters.isEmpty ? " " : lines[index]) 132 // Linked as the row is drawn, so a long file costs
133 // only the rows on screen.
134 let shown = definitions.isEmpty ? lines[index] : Definition.link(lines[index], definitions: definitions)
135 Text(shown.characters.isEmpty ? " " : shown)
132 .fixedSize() 136 .fixedSize()
133 .frame(maxWidth: .infinity, alignment: .leading) 137 .frame(maxWidth: .infinity, alignment: .leading)
134 .background(target ? Color.gbMark.opacity(0.18) : .clear) 138 .background(target ? Color.gbMark.opacity(0.18) : .clear)
@@ -140,8 +144,7 @@ private struct CodeScrollView: View {
140 .defaultScrollAnchor(.topLeading) 144 .defaultScrollAnchor(.topLeading)
141 .font(.gbMono(.caption)) 145 .font(.gbMono(.caption))
142 .task(id: colorScheme) { 146 .task(id: colorScheme) {
143 highlighted = highlightedLines() 147 lines = highlightedLines()
144 lines = linked(highlighted)
145 if let line, line <= lines.count { 148 if let line, line <= lines.count {
146 // A frame for the highlighted lines to exist before the 149 // A frame for the highlighted lines to exist before the
147 // jump, or the lazy stack has nothing to scroll to. 150 // jump, or the lazy stack has nothing to scroll to.
@@ -149,15 +152,9 @@ private struct CodeScrollView: View {
149 proxy.scrollTo(line - 1, anchor: UnitPoint(x: 0, y: 0.3)) 152 proxy.scrollTo(line - 1, anchor: UnitPoint(x: 0, y: 0.3))
150 } 153 }
151 } 154 }
152 // The definitions arrive after the file; link what is drawn.
153 .onChange(of: definitions) { lines = linked(highlighted) }
154 } 155 }
155 } 156 }
156 157
157 private func linked(_ lines: [AttributedString]) -> [AttributedString] {
158 definitions.isEmpty ? lines : lines.map { Definition.link($0, definitions: definitions) }
159 }
160
161 private func highlightedLines() -> [AttributedString] { 158 private func highlightedLines() -> [AttributedString] {
162 let size = UIFont.preferredFont(forTextStyle: .caption1).pointSize 159 let size = UIFont.preferredFont(forTextStyle: .caption1).pointSize
163 let font = UIFont(name: "Atkinson Hyperlegible Mono", size: size) 160 let font = UIFont(name: "Atkinson Hyperlegible Mono", size: size)
gitbayTests/DefinitionLinkTests.swift +14
@@ -82,4 +82,18 @@ struct FileDefinitionsTests {
82 #expect(model.state.value?.content == "Parse()\n") 82 #expect(model.state.value?.content == "Parse()\n")
83 #expect(model.definitions.isEmpty) 83 #expect(model.definitions.isEmpty)
84 } 84 }
85
86 @Test func proseFilesAreNotLinked() async throws {
87 let (client, stub) = try makeClient()
88 stub.enqueue(.init(status: 200, json: """
89 {"protocol_version":1,"data":{"path":"krz/app","ref":"main","file":"README.md","size":10,\
90 "binary":false,"content":"# Parse\\n"},"exit_code":0}
91 """, match: "argv=cat"))
92 let model = FileViewModel(client: client, repoPath: "krz/app", filePath: "README.md", ref: "main")
93
94 await model.load()
95
96 #expect(model.state.value != nil)
97 #expect(!stub.seen.contains { $0.url.absoluteString.contains("argv=definitions") })
98 }
85} 99}