package httpd import ( "errors" "fmt" "html/template" "net/http" "net/url" "regexp" "strconv" "strings" "gitbay.org/gitbay/internal/control" "gitbay.org/gitbay/internal/store" "gitbay.org/gitbay/internal/symbols" ) // nameSpan is a name token as chroma writes it with classes: n, nx, nf, // nc and the rest of the Name family. Keywords, literals and punctuation // carry other classes and are never linked. var nameSpan = regexp.MustCompile(`([\p{L}_$][\p{L}\p{N}_$]*)`) // escapePath escapes each segment of a slash-separated path for a URL. func escapePath(p string) string { parts := strings.Split(p, "/") for i, s := range parts { parts[i] = url.PathEscape(s) } return strings.Join(parts, "/") } // blobSymbols finds the viewed file's symbols and links the names in its // highlighted source to their definitions, when commit, the ref being // viewed as the page resolved it, has the indexed tree. A name defined once links to that line at the same // ref; one defined more than once links to the results page. Names the // index does not hold stay plain. The whole page costs one lookup of the // file's distinct names, however many times each appears. func (s *Server) blobSymbols(p repoPage, commit, filePath string, code template.HTML) (template.HTML, []store.SymbolRow) { idx, err := s.st.SymbolIndexFor(p.Repo.ID) if err != nil || !control.IndexedTree(p.Dir, commit, idx) { return code, nil } list, _ := s.st.SymbolsInFile(idx.ID, filePath) if code == "" { return code, list } seen := map[string]bool{} var keys []string for _, m := range nameSpan.FindAllStringSubmatch(string(code), -1) { if !seen[m[1]] { seen[m[1]] = true keys = append(keys, m[1]) } } if len(keys) == 0 { return code, list } targets, err := s.st.SymbolTargets(idx.ID, keys) if err != nil || len(targets) == 0 { return code, list } repoPath := "/" + p.Repo.Path() ref := escapePath(p.Ref) linked := nameSpan.ReplaceAllStringFunc(string(code), func(span string) string { name := nameSpan.FindStringSubmatch(span)[1] t, ok := targets[name] if !ok { return span } href := repoPath + "/symbols?q=" + url.QueryEscape(name) if t.Count == 1 { href = repoPath + "/blob/" + ref + "/" + escapePath(t.Path) + "#L" + strconv.Itoa(t.Line) } return `` + span + `` }) return template.HTML(linked), list } // symbolsPageSize is the results page's page length. const symbolsPageSize = 100 // symbolsPage lists the definitions matching a query, ranked as `repo // symbols` ranks them, from the same store query. func (s *Server) symbolsPage(w http.ResponseWriter, r *http.Request) { p, ok := s.repoFor(w, r, "") if !ok { return } p.Tab = "search" q := strings.TrimSpace(r.URL.Query().Get("q")) kind := r.URL.Query().Get("kind") if !symbols.ValidKind(kind) { kind = "" } var ( rows []store.SymbolRow next string problem string note string ) idx, err := s.st.SymbolIndexFor(p.Repo.ID) switch { case errors.Is(err, store.ErrNotFound): problem = "no symbol index yet; one is built after a push to " + p.Repo.DefaultBranch if f, ferr := s.st.SymbolFailureFor(p.Repo.ID); ferr == nil { problem = "the symbol index could not be built: " + f.Note } case err != nil: http.Error(w, "internal error", http.StatusInternalServerError) return case q == "": case len(q) < control.MinSymbolQuery || len(q) > 200: problem = fmt.Sprintf("query must be %d to 200 characters", control.MinSymbolQuery) default: if idx.State == "partial" { note = "the index is partial: " + idx.Note } var after int64 if a := r.URL.Query().Get("after"); a != "" { cursorIdx, id, ok := control.ParseSymbolCursor(a) switch { case !ok: problem = "bad cursor" case cursorIdx != idx.ID: problem = control.StaleSymbolCursor } after = id } if problem != "" { break } rows, err = s.st.SearchSymbols(idx.ID, q, kind, symbolsPageSize+1, after) if err != nil { http.Error(w, "internal error", http.StatusInternalServerError) return } if len(rows) > symbolsPageSize { rows = rows[:symbolsPageSize] v := url.Values{"q": {q}, "after": {control.SymbolCursor(idx.ID, rows[len(rows)-1].ID)}} if kind != "" { v.Set("kind", kind) } next = "/" + p.Repo.Path() + "/symbols?" + v.Encode() } } s.render(w, "symbols.html", struct { repoPage Query string Kind string Kinds []string Problem string Note string Rows []store.SymbolRow Next string Commit string }{p, q, kind, symbols.Kinds, problem, note, rows, next, idx.Commit}) }