snapshot of c000b74a5b09c433f96a69fa102c5c8f6583dff0 Annotations about the code that implements koment.

internal/ui/view.go

1 package ui
2
3 import (
4 "net/url"
5 "strings"
6 "unicode/utf8"
7
8 "github.com/koment-dev/koment/internal/anchor"
9 "github.com/koment-dev/koment/internal/application"
10 "github.com/koment-dev/koment/internal/store"
11 )
12
13 const sourceURL = "https://github.com/koment-dev/koment"
14
15 type view struct {
16 Total int
17 Tally []tallyEntry
18 Tree []treeNode
19 Loose []entry
20 Repositories []repositoryLink
21 Repository string
22 Current string
23 File *fileView
24 Empty bool
25 NotFound bool
26 Home string
27 Stylesheet string
28 Script string
29 LogoSVG string
30 LogoPNG string
31 SourceURL string
32 Snapshot *snapshot
33 WriteToken string
34 CanWrite bool
35 CreatedID string
36 WriteWarning string
37 ReviewURL string
38 }
39
40 type snapshot struct {
41 Commit string
42 CommitURL string
43 Banner string
44 BannerHref string
45 }
46
47 type repositoryLink struct {
48 ID string
49 Name string
50 Href string
51 Current bool
52 }
53
54 type links struct {
55 file func(target string) string
56 home string
57 stylesheet string
58 script string
59 logoSVG string
60 logoPNG string
61 }
62
63 func servedLinks(repositoryID string) links {
64 base := repositoryPrefix + repositoryID + "/"
65 return links{
66 file: func(target string) string { return base + "f/" + escapedFilePath(target) },
67 home: base,
68 stylesheet: "/assets/style.css",
69 script: "/assets/koment.js",
70 logoSVG: "/assets/koment-logo.svg",
71 logoPNG: "/assets/koment-logo.png",
72 }
73 }
74
75 func escapedFilePath(file string) string {
76 parts := strings.Split(file, "/")
77 for index, part := range parts {
78 parts[index] = url.PathEscape(part)
79 }
80 return strings.Join(parts, "/")
81 }
82
83 type tallyEntry struct {
84 Status anchor.Status
85 Count int
86 }
87
88 type entry struct {
89 Path string
90 Name string
91 Href string
92 Count int
93 Worst anchor.Status
94 Current bool
95 Search string
96 }
97
98 type fileView struct {
99 Path string
100 Lines []line
101 Notes []note
102 Detached []note
103 Missing bool
104 }
105
106 type line struct {
107 Number int
108 Text string
109 Marker anchor.Status
110 }
111
112 type note struct {
113 ID string
114 Kind string
115 Title string
116 Status anchor.Status
117 Line int
118 Body []string
119 Rest []string
120 Created string
121 Excerpt string
122 Stale bool
123 Warning string
124 }
125
126 var statusOrder = []anchor.Status{
127 anchor.StatusOK, anchor.StatusAmbiguous, anchor.StatusDrifted, anchor.StatusOrphaned,
128 }
129
130 func build(repositorySnapshot *application.RepositorySnapshot, requested string, how links) (*view, error) {
131 if len(repositorySnapshot.Files) == 0 {
132 return &view{
133 Empty: true, Stylesheet: how.stylesheet, Script: how.script,
134 Home: how.home, LogoSVG: how.logoSVG, LogoPNG: how.logoPNG, SourceURL: sourceURL,
135 }, nil
136 }
137
138 current := requested
139 if current == "" {
140 current = repositorySnapshot.Files[0].Path
141 }
142
143 built := &view{
144 Current: current,
145 Home: how.home,
146 Stylesheet: how.stylesheet,
147 Script: how.script,
148 LogoSVG: how.logoSVG,
149 LogoPNG: how.logoPNG,
150 SourceURL: sourceURL,
151 }
152 counts := map[anchor.Status]int{}
153 listed := make([]entry, 0, len(repositorySnapshot.Files))
154
155 for _, file := range repositorySnapshot.Files {
156 worst := anchor.StatusOK
157 var searchable strings.Builder
158 searchable.WriteString(file.Path)
159 for _, annotation := range file.Annotations {
160 counts[annotation.Status]++
161 built.Total++
162 if statusSeverity[annotation.Status] > statusSeverity[worst] {
163 worst = annotation.Status
164 }
165 searchable.WriteString("\n" + string(annotation.Record.Spec.Type) + "\n" + annotation.Record.Headline() + "\n" + annotation.Record.Spec.Body + "\n" + annotation.Record.Spec.Author.Name)
166 }
167
168 listed = append(listed, entry{
169 Path: file.Path,
170 Name: baseName(file.Path),
171 Href: how.file(file.Path),
172 Count: len(file.Annotations),
173 Worst: worst,
174 Current: file.Path == current,
175 Search: strings.ToLower(searchable.String()),
176 })
177
178 if file.Path == current {
179 built.File = buildFile(file)
180 }
181 }
182
183 if built.File == nil {
184 built.NotFound = true
185 }
186 built.Tally = tallyOf(counts)
187 built.Tree, built.Loose = buildTree(listed, current)
188 return built, nil
189 }
190
191 func baseName(file string) string {
192 if cut := strings.LastIndex(file, "/"); cut >= 0 {
193 return file[cut+1:]
194 }
195 return file
196 }
197
198 func tallyOf(counts map[anchor.Status]int) []tallyEntry {
199 var tally []tallyEntry
200 for _, status := range statusOrder {
201 if counts[status] > 0 {
202 tally = append(tally, tallyEntry{Status: status, Count: counts[status]})
203 }
204 }
205 return tally
206 }
207
208 func buildFile(file application.FileSnapshot) *fileView {
209 built := &fileView{Path: file.Path}
210 if !file.Exists {
211 built.Missing = true
212 for _, annotation := range file.Annotations {
213 built.Detached = append(built.Detached, describe(annotation))
214 }
215 return built
216 }
217
218 marked := map[int]anchor.Status{}
219 for _, annotation := range file.Annotations {
220 described := describe(annotation)
221 if annotation.Line == 0 {
222 built.Detached = append(built.Detached, described)
223 continue
224 }
225 built.Notes = append(built.Notes, described)
226 worst, seen := marked[annotation.Line]
227 if !seen || statusSeverity[annotation.Status] > statusSeverity[worst] {
228 marked[annotation.Line] = annotation.Status
229 }
230 }
231
232 for i, text := range strings.Split(strings.TrimSuffix(string(file.Content), "\n"), "\n") {
233 number := i + 1
234 built.Lines = append(built.Lines, line{Number: number, Text: text, Marker: marked[number]})
235 }
236 return built
237 }
238
239 func describe(annotation application.AnnotationView) note {
240 stale := annotation.Status.IsFailure()
241 shown, folded := splitBody(store.Paragraphs(annotation.Record.Spec.Body))
242 return note{
243 ID: annotation.Record.Metadata.ID,
244 Kind: string(annotation.Record.Spec.Type),
245 Status: annotation.Status,
246 Line: annotation.Line,
247 Title: annotation.Record.Headline(),
248 Body: shown,
249 Rest: folded,
250 Created: annotation.Record.Metadata.Created.Format("2006-01-02"),
251 Excerpt: annotation.Record.Spec.Anchor.Excerpt,
252 Stale: stale,
253 Warning: annotation.Warning,
254 }
255 }
256
257 const (
258 visibleBodyBudget = 600
259 shortestWorthHiding = 160
260 )
261
262 func splitBody(paragraphs []string) (shown, folded []string) {
263 if len(paragraphs) < 2 {
264 return paragraphs, nil
265 }
266 spent := utf8.RuneCountInString(paragraphs[0])
267 for index := 1; index < len(paragraphs); index++ {
268 paragraphLength := utf8.RuneCountInString(paragraphs[index])
269 if spent+paragraphLength <= visibleBodyBudget {
270 spent += paragraphLength
271 continue
272 }
273 tail := paragraphs[index:]
274 if lengthOf(tail) < shortestWorthHiding {
275 return paragraphs, nil
276 }
277 return paragraphs[:index], tail
278 }
279 return paragraphs, nil
280 }
281
282 func lengthOf(paragraphs []string) int {
283 total := 0
284 for _, paragraph := range paragraphs {
285 total += utf8.RuneCountInString(paragraph)
286 }
287 return total
288 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close