package web
import (
"encoding/json"
"net/http"
"net/url"
"strconv"
"strings"
"craftdesign.group/quarry/internal/ops"
)
// The Rich composer's pickers: after #, !, @ or [[ the composer asks
// what that sigil could name here, and inserts the chosen reference as
// the reader would have typed it. One small JSON list, because the
// script builds its own menu at the caret; the search boxes' lists are
// links and this one is insertions.
// refPick is one thing a picker offers: the reference as it is typed
// and a line saying what it names.
type refPick struct {
Tok string `json:"tok"`
Text string `json:"text"`
}
// refPickLimit is how many a picker shows: a glance, as suggestLimit is.
const refPickLimit = suggestLimit
// refPickPool is how many recent things a picker filters, newest
// first. A reference is nearly always to something current, so the
// most recently touched two hundred is the useful pool.
const refPickPool = 200
func refSuggest(r *ops.Registry, w http.ResponseWriter, req *http.Request) {
repo := req.PathValue("repo")
q := strings.ToLower(strings.TrimSpace(req.URL.Query().Get("q")))
picks := []refPick{}
switch req.URL.Query().Get("k") {
case "ticket":
out, err := invoke(req.Context(), r, "ticket.list", url.Values{
"repo": {repo}, "state": {"all"}, "limit": {strconv.Itoa(refPickPool)}})
if err != nil {
fail(r, w, req, err)
return
}
var v struct {
Tickets []struct {
ID string `json:"id"`
Title string `json:"title"`
Number int `json:"number"`
} `json:"tickets"`
}
remarshal(out, &v)
for _, t := range v.Tickets {
tok := "[" + shortHash(t.ID) + "]"
if t.Number > 0 {
tok = "#" + strconv.Itoa(t.Number)
}
picks = addPick(picks, q, tok, t.Title)
}
case "landing":
out, err := invoke(req.Context(), r, "landing.list", url.Values{
"repo": {repo}, "limit": {strconv.Itoa(refPickPool)}})
if err != nil {
fail(r, w, req, err)
return
}
var v struct {
Landings []struct {
Title string `json:"title"`
Number int `json:"number"`
} `json:"landings"`
}
remarshal(out, &v)
for _, l := range v.Landings {
if l.Number > 0 {
picks = addPick(picks, q, "!"+strconv.Itoa(l.Number), l.Title)
}
}
case "person":
out, err := invoke(req.Context(), r, "people.mention", url.Values{"repo": {repo}, "q": {q}})
if err != nil {
fail(r, w, req, err)
return
}
var v struct {
Handles []struct {
Handle string `json:"handle"`
} `json:"handles"`
}
remarshal(out, &v)
for _, h := range v.Handles {
picks = addPick(picks, "", "@"+h.Handle, "")
}
case "wiki":
out, err := invoke(req.Context(), r, "wiki.list", url.Values{"repo": {repo}})
if err != nil {
fail(r, w, req, err)
return
}
var v struct {
Pages []struct {
Title string `json:"title"`
} `json:"pages"`
}
remarshal(out, &v)
for _, p := range v.Pages {
// A check-in's or branch's own page annotates that thing;
// it is not a page anyone links to by name.
if isAssociatedWiki(p.Title) {
continue
}
picks = addPick(picks, q, "[["+p.Title+"]]", "")
}
default:
http.Error(w, "k is ticket, landing, person or wiki", http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
json.NewEncoder(w).Encode(struct {
Picks []refPick `json:"picks"`
}{picks})
}
// addPick appends tok when q (lower case) is in the reference or its
// text, until the list is full.
func addPick(picks []refPick, q, tok, text string) []refPick {
if len(picks) >= refPickLimit {
return picks
}
if q != "" && !strings.Contains(strings.ToLower(tok+" "+text), q) {
return picks
}
return append(picks, refPick{Tok: tok, Text: text})
}