internal/httpd/symbols.go

v1.40.0
gitbay/internal/httpd/symbols.go history · blame · raw

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}