Skip to content
quarry Artifact [533e5e64ea]
Login

Artifact [533e5e64ea]

Artifact 533e5e64ead3e40654bd15aa4131d6a6f1ab68af4a890bde6facbf916b278e7a:


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})
}