diff --git a/.air/appview.toml b/.air/appview.toml index 83f14ca2..172d9313 100644 --- a/.air/appview.toml +++ b/.air/appview.toml @@ -6,5 +6,5 @@ cmd = "go build -o out/appview.out cmd/appview/main.go" bin = "out/appview.out" include_ext = ["go"] -exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp", "node_modules"] +exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp", "out", "target", "node_modules"] stop_on_error = true diff --git a/appview/pages/pages.go b/appview/pages/pages.go index 62fbef10..9bcbcdc2 100644 --- a/appview/pages/pages.go +++ b/appview/pages/pages.go @@ -930,6 +930,27 @@ func (p *Pages) RepoIndexPage(w io.Writer, params RepoIndexParams) error { return p.executeRepo("repo/index", w, params) } +type RepoSearchParams struct { + BaseParams + RepoInfo repoinfo.RepoInfo + Active string + FilterQuery string +} + +func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { + params.Active = "overview" + return p.executeRepo("repo/search", w, params) +} + +type RepoSearchResultsFragmentParams struct { + Results []SearchResult + ErrorMsg string +} + +func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { + return p.executePlain("repo/fragments/searchResults", w, params) +} + type RepoLogParams struct { BaseParams RepoInfo repoinfo.RepoInfo diff --git a/appview/pages/templates/repo/fragments/searchResults.html b/appview/pages/templates/repo/fragments/searchResults.html new file mode 100644 index 00000000..755fcabd --- /dev/null +++ b/appview/pages/templates/repo/fragments/searchResults.html @@ -0,0 +1,91 @@ +{{ define "repo/fragments/searchResults" }} +
+ {{ if .ErrorMsg }} +
+ {{ i "circle-alert" "size-4" }} + {{ .ErrorMsg }} +
+ {{ else }} + {{ range .Results }} + {{ template "resultCard" . }} + {{ else }} +
+ No results found. +
+ {{ end }} + {{ end }} +
+{{ end }} + +{{ define "resultCard" }} + {{ $owner := resolve .Repo.Did }} + {{ $slug := .Repo.Slug }} + {{ if .File }} + {{/* filename match */}} +
+
+ {{ i "file" "size-4 flex-shrink-0" }} + + {{- range .File.NameSpans -}} + {{- if .Match -}}{{ .Text }} + {{- else -}}{{ .Text }}{{- end -}} + {{- end -}} + +
+
+ {{ with .Language }} + {{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }} + {{ . }} + {{ end }} +
+
+ {{ else if .Chunks }} + {{/* chunk match */}} +
+
+
+ {{ i "file" "size-4 flex-shrink-0" }} + {{ .FilePath }} +
+
+ {{ with .Language }} + {{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }} + {{ . }} + {{ end }} +
+
+ {{ range $i, $chunk := .Chunks }} + {{ if lt $i 3 }} + {{ if gt $i 0 }} +
+ ··· +
+ {{ end }} + {{ template "search/fragments/chunkBody" (dict "Owner" $owner "Slug" $slug "Commit" $.Commit "FilePath" $.FilePath "Chunk" $chunk) }} + {{ end }} + {{ end }} + {{ if gt (len .Chunks) 3 }} +
+ +
+ {{ i "chevron-down" "size-3 m-1" }} + Show {{ (slice .Chunks 3).MatchCount }} more matches +
+ +
+ {{ range $i, $chunk := .Chunks }} + {{ if ge $i 3 }} +
+ ··· +
+ {{ template "search/fragments/chunkBody" (dict "Owner" $owner "Slug" $slug "Commit" $.Commit "FilePath" $.FilePath "Chunk" $chunk) }} + {{ end }} + {{ end }} +
+ {{ end }} +
+ {{ end }} +{{ end }} diff --git a/appview/pages/templates/repo/index.html b/appview/pages/templates/repo/index.html index 2e251b53..3972dd17 100644 --- a/appview/pages/templates/repo/index.html +++ b/appview/pages/templates/repo/index.html @@ -24,6 +24,9 @@ {{ i "tags" "w-4" "h-4" }} {{ scaleFmt (len .Tags) }} + + {{ i "search" "size-4" }} + {{ template "repo/fragments/cloneDropdown" . }} diff --git a/appview/pages/templates/repo/search.html b/appview/pages/templates/repo/search.html new file mode 100644 index 00000000..25fd566e --- /dev/null +++ b/appview/pages/templates/repo/search.html @@ -0,0 +1,31 @@ +{{ define "title" }}Search · {{ .RepoInfo.FullName }} · Tangled{{ end }} + +{{ define "extrameta" }} + {{ template "repo/fragments/meta" . }} + + {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }} +{{ end }} + +{{ define "repoContent" }} +
+
+
+ + {{ i "search-code" "size-4" }} + + +
+
+ {{ template "repo/fragments/searchResults" dict }} +
+{{ end }} diff --git a/appview/repo/filesearch.go b/appview/repo/filesearch.go new file mode 100644 index 00000000..dd1bcd1f --- /dev/null +++ b/appview/repo/filesearch.go @@ -0,0 +1,95 @@ +package repo + +import ( + "fmt" + "net/http" + "slices" + + "github.com/bluesky-social/indigo/atproto/syntax" + "tangled.org/core/appview/models" + "tangled.org/core/appview/pages" + "tangled.org/core/appview/pagination" +) + +func (rp *Repo) Search(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Hx-Request") == "true" { + rp.searchResultsFragment(w, r) + return + } + if err := rp.pages.RepoSearchPage(w, pages.RepoSearchParams{ + BaseParams: pages.BaseParamsFromContext(r.Context()), + RepoInfo: rp.repoResolver.GetRepoInfo(r, rp.oauth.GetMultiAccountUser(r)), + FilterQuery: "", + }); err != nil { + rp.logger.Error("failed to render", "err", err) + } +} + +func (rp *Repo) searchResultsFragment(w http.ResponseWriter, r *http.Request) { + l := rp.logger.With("handler", "Search") + repo, err := rp.repoResolver.Resolve(r) + if err != nil { + l.Error("failed to get repo", "err", err) + return + } + q := r.URL.Query().Get("q") + + var params pages.RepoSearchResultsFragmentParams + defer func() { + if err := rp.pages.RepoSearchResultsFragment(w, params); err != nil { + l.Error("failed to render", "err", err) + } + }() + + if q == "" { + return + } + + q = fmt.Sprintf(`meta.did:%s %q`, repo.RepoDid, q) + + ctx := r.Context() + + res, err := rp.codesearch.Search(ctx, q, pagination.Page{Limit: 50}) + if err != nil { + l.Error("failed to search files", "err", err) + params.ErrorMsg = "Failed to perform search. Please try again later." + return + } + + if len(res.Results) == 0 { + return + } + + out := make([]pages.SearchResult, 0, len(res.Results)) + for _, res := range res.Results { + if res.RepoDID != syntax.DID(repo.RepoDid) { + continue + } + csr := pages.SearchResult{ + RepoDID: res.RepoDID, + Repo: repo, + FilePath: res.FilePath, + Branches: res.Branches, + Commit: res.Commit, + Language: res.Language, + } + if f := res.File; f != nil { + csr.File = &pages.CodeSearchResult_File{ + NameSpans: pages.FileNameSpans(res.FilePath, f.Ranges), + } + } + slices.SortStableFunc(res.Chunks, func(a, b models.Result_ChunkMatch) int { + return a.ContentStartLine - b.ContentStartLine + }) + for _, c := range res.Chunks { + csr.Chunks = append(csr.Chunks, pages.CodeSearchResult_Chunk{ + Lines: pages.ChunkLines(c.Content, c.ContentStartLine, c.Ranges), + MatchCount: len(c.Ranges), + }) + } + out = append(out, csr) + } + + l.Debug("repo file search result", "len", len(out), "duration", res.Stats.Duration) + params.Results = out +} diff --git a/appview/repo/repo.go b/appview/repo/repo.go index 771170ea..696578e2 100644 --- a/appview/repo/repo.go +++ b/appview/repo/repo.go @@ -13,6 +13,7 @@ import ( "time" "tangled.org/core/appview/cloudflare" + "tangled.org/core/appview/codesearch" "tangled.org/core/api/tangled" "tangled.org/core/appview/config" @@ -61,6 +62,7 @@ type Repo struct { validator *validator.Validator cfClient *cloudflare.Client ogreClient *ogre.Client + codesearch *codesearch.CodeSearch } func New( @@ -77,6 +79,7 @@ func New( logger *slog.Logger, validator *validator.Validator, cfClient *cloudflare.Client, + codesearch *codesearch.CodeSearch, ) *Repo { return &Repo{ oauth: oauth, @@ -93,6 +96,7 @@ func New( validator: validator, cfClient: cfClient, ogreClient: ogre.NewClient(config.Ogre.Host), + codesearch: codesearch, } } diff --git a/appview/repo/router.go b/appview/repo/router.go index 606010d0..ba9a313a 100644 --- a/appview/repo/router.go +++ b/appview/repo/router.go @@ -76,6 +76,8 @@ func (rp *Repo) Router(mw *middleware.Middleware) http.Handler { r.Get("/edit", rp.EditLabelPanel) }) + r.Get("/search", rp.Search) + // settings routes, needs auth r.Group(func(r chi.Router) { r.Use(middleware.AuthMiddleware(rp.oauth)) diff --git a/appview/state/router.go b/appview/state/router.go index 0f30598b..1b1e5a6a 100644 --- a/appview/state/router.go +++ b/appview/state/router.go @@ -412,6 +412,7 @@ func (s *State) RepoRouter(mw *middleware.Middleware) http.Handler { log.SubLogger(s.logger, "repo"), s.validator, s.cfClient, + s.codesearch, ) return repo.Router(mw) }