internal/httpd/symbols.go
157 lines · 4677 bytes
1package httpd
2
3import (
4 "errors"
5 "fmt"
6 "html/template"
7 "net/http"
8 "net/url"
9 "regexp"
10 "strconv"
11 "strings"
12
13 "gitbay.org/gitbay/internal/control"
14 "gitbay.org/gitbay/internal/store"
15 "gitbay.org/gitbay/internal/symbols"
16)
17
18// nameSpan is a name token as chroma writes it with classes: n, nx, nf,
19// nc and the rest of the Name family. Keywords, literals and punctuation
20// carry other classes and are never linked.
21var nameSpan = regexp.MustCompile(`<span class="n[a-z]?">([\p{L}_$][\p{L}\p{N}_$]*)</span>`)
22
23// escapePath escapes each segment of a slash-separated path for a URL.
24func escapePath(p string) string {
25 parts := strings.Split(p, "/")
26 for i, s := range parts {
27 parts[i] = url.PathEscape(s)
28 }
29 return strings.Join(parts, "/")
30}
31
32// blobSymbols finds the viewed file's symbols and links the names in its
33// highlighted source to their definitions, when commit, the ref being
34// viewed as the page resolved it, has the indexed tree. A name defined once links to that line at the same
35// ref; one defined more than once links to the results page. Names the
36// index does not hold stay plain. The whole page costs one lookup of the
37// file's distinct names, however many times each appears.
38func (s *Server) blobSymbols(p repoPage, commit, filePath string, code template.HTML) (template.HTML, []store.SymbolRow) {
39 idx, err := s.st.SymbolIndexFor(p.Repo.ID)
40 if err != nil || !control.IndexedTree(p.Dir, commit, idx) {
41 return code, nil
42 }
43 list, _ := s.st.SymbolsInFile(idx.ID, filePath)
44 if code == "" {
45 return code, list
46 }
47 seen := map[string]bool{}
48 var keys []string
49 for _, m := range nameSpan.FindAllStringSubmatch(string(code), -1) {
50 if !seen[m[1]] {
51 seen[m[1]] = true
52 keys = append(keys, m[1])
53 }
54 }
55 if len(keys) == 0 {
56 return code, list
57 }
58 targets, err := s.st.SymbolTargets(idx.ID, keys)
59 if err != nil || len(targets) == 0 {
60 return code, list
61 }
62 repoPath := "/" + p.Repo.Path()
63 ref := escapePath(p.Ref)
64 linked := nameSpan.ReplaceAllStringFunc(string(code), func(span string) string {
65 name := nameSpan.FindStringSubmatch(span)[1]
66 t, ok := targets[name]
67 if !ok {
68 return span
69 }
70 href := repoPath + "/symbols?q=" + url.QueryEscape(name)
71 if t.Count == 1 {
72 href = repoPath + "/blob/" + ref + "/" + escapePath(t.Path) + "#L" + strconv.Itoa(t.Line)
73 }
74 return `<a class="sym" href="` + template.HTMLEscapeString(href) + `">` + span + `</a>`
75 })
76 return template.HTML(linked), list
77}
78
79// symbolsPageSize is the results page's page length.
80const symbolsPageSize = 100
81
82// symbolsPage lists the definitions matching a query, ranked as `repo
83// symbols` ranks them, from the same store query.
84func (s *Server) symbolsPage(w http.ResponseWriter, r *http.Request) {
85 p, ok := s.repoFor(w, r, "")
86 if !ok {
87 return
88 }
89 p.Tab = "search"
90 q := strings.TrimSpace(r.URL.Query().Get("q"))
91 kind := r.URL.Query().Get("kind")
92 if !symbols.ValidKind(kind) {
93 kind = ""
94 }
95 var (
96 rows []store.SymbolRow
97 next string
98 problem string
99 note string
100 )
101 idx, err := s.st.SymbolIndexFor(p.Repo.ID)
102 switch {
103 case errors.Is(err, store.ErrNotFound):
104 problem = "no symbol index yet; one is built after a push to " + p.Repo.DefaultBranch
105 if f, ferr := s.st.SymbolFailureFor(p.Repo.ID); ferr == nil {
106 problem = "the symbol index could not be built: " + f.Note
107 }
108 case err != nil:
109 http.Error(w, "internal error", http.StatusInternalServerError)
110 return
111 case q == "":
112 case len(q) < control.MinSymbolQuery || len(q) > 200:
113 problem = fmt.Sprintf("query must be %d to 200 characters", control.MinSymbolQuery)
114 default:
115 if idx.State == "partial" {
116 note = "the index is partial: " + idx.Note
117 }
118 var after int64
119 if a := r.URL.Query().Get("after"); a != "" {
120 cursorIdx, id, ok := control.ParseSymbolCursor(a)
121 switch {
122 case !ok:
123 problem = "bad cursor"
124 case cursorIdx != idx.ID:
125 problem = control.StaleSymbolCursor
126 }
127 after = id
128 }
129 if problem != "" {
130 break
131 }
132 rows, err = s.st.SearchSymbols(idx.ID, q, kind, symbolsPageSize+1, after)
133 if err != nil {
134 http.Error(w, "internal error", http.StatusInternalServerError)
135 return
136 }
137 if len(rows) > symbolsPageSize {
138 rows = rows[:symbolsPageSize]
139 v := url.Values{"q": {q}, "after": {control.SymbolCursor(idx.ID, rows[len(rows)-1].ID)}}
140 if kind != "" {
141 v.Set("kind", kind)
142 }
143 next = "/" + p.Repo.Path() + "/symbols?" + v.Encode()
144 }
145 }
146 s.render(w, "symbols.html", struct {
147 repoPage
148 Query string
149 Kind string
150 Kinds []string
151 Problem string
152 Note string
153 Rows []store.SymbolRow
154 Next string
155 Commit string
156 }{p, q, kind, symbols.Kinds, problem, note, rows, next, idx.Commit})
157}