Monorepo for Tangled tangled.org
11

Configure Feed

Select the types of activity you want to include in your feed.

appview: repo file search

Signed-off-by: Seongmin Lee <git@boltless.me>

author
Seongmin Lee
date (Jun 26, 2026, 2:31 AM +0900) commit 85dc4c66 parent a10f89b3 change-id ktyrmzyp
+249 -1
+1 -1
.air/appview.toml
··· 6 6 bin = "out/appview.out" 7 7 8 8 include_ext = ["go"] 9 - exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp", "node_modules"] 9 + exclude_dir = ["avatar", "camo", "sites", "indexes", "nix", "tmp", "out", "target", "node_modules"] 10 10 stop_on_error = true
+21
appview/pages/pages.go
··· 930 930 return p.executeRepo("repo/index", w, params) 931 931 } 932 932 933 + type RepoSearchParams struct { 934 + BaseParams 935 + RepoInfo repoinfo.RepoInfo 936 + Active string 937 + FilterQuery string 938 + } 939 + 940 + func (p *Pages) RepoSearchPage(w io.Writer, params RepoSearchParams) error { 941 + params.Active = "overview" 942 + return p.executeRepo("repo/search", w, params) 943 + } 944 + 945 + type RepoSearchResultsFragmentParams struct { 946 + Results []SearchResult 947 + ErrorMsg string 948 + } 949 + 950 + func (p *Pages) RepoSearchResultsFragment(w io.Writer, params RepoSearchResultsFragmentParams) error { 951 + return p.executePlain("repo/fragments/searchResults", w, params) 952 + } 953 + 933 954 type RepoLogParams struct { 934 955 BaseParams 935 956 RepoInfo repoinfo.RepoInfo
+91
appview/pages/templates/repo/fragments/searchResults.html
··· 1 + {{ define "repo/fragments/searchResults" }} 2 + <div id="repo-files" class="space-y-4 mb-6"> 3 + {{ if .ErrorMsg }} 4 + <div class="flex items-center gap-2 my-2 bg-red-50 dark:bg-red-900/30 border border-red-300 dark:border-red-700 rounded px-3 py-2 text-red-600 dark:text-red-300 text-sm"> 5 + {{ i "circle-alert" "size-4" }} 6 + <span>{{ .ErrorMsg }}</span> 7 + </div> 8 + {{ else }} 9 + {{ range .Results }} 10 + {{ template "resultCard" . }} 11 + {{ else }} 12 + <div class="text-base text-gray-500 flex items-center justify-center italic p-12 border border-gray-200 dark:border-gray-700 rounded"> 13 + <span>No results found.</span> 14 + </div> 15 + {{ end }} 16 + {{ end }} 17 + </div> 18 + {{ end }} 19 + 20 + {{ define "resultCard" }} 21 + {{ $owner := resolve .Repo.Did }} 22 + {{ $slug := .Repo.Slug }} 23 + {{ if .File }} 24 + {{/* filename match */}} 25 + <div class="px-3.5 py-2 text-sm border border-gray-200 dark:border-gray-700 rounded-sm flex items-center justify-between"> 26 + <div class="flex items-center gap-2 min-w-0"> 27 + {{ i "file" "size-4 flex-shrink-0" }} 28 + <a href="/{{ $owner }}/{{ $slug }}/blob/{{ .Commit }}/{{ .FilePath }}" class="truncate font-mono"> 29 + {{- range .File.NameSpans -}} 30 + {{- if .Match -}}<span class="chunk-match-hl">{{ .Text }}</span> 31 + {{- else -}}{{ .Text }}{{- end -}} 32 + {{- end -}} 33 + </a> 34 + </div> 35 + <div class="flex items-center gap-3.5"> 36 + {{ with .Language }} 37 + {{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }} 38 + <span>{{ . }}</span> 39 + {{ end }} 40 + </div> 41 + </div> 42 + {{ else if .Chunks }} 43 + {{/* chunk match */}} 44 + <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-sm divide-y divide-gray-200 dark:divide-gray-700"> 45 + <div class="px-3.5 py-2 text-sm flex items-center justify-between"> 46 + <div class="flex items-center gap-2 min-w-0"> 47 + {{ i "file" "size-4 flex-shrink-0" }} 48 + <a href="/{{ $owner }}/{{ $slug }}/blob/{{ .Commit }}/{{ .FilePath }}" class="truncate font-mono">{{ .FilePath }}</a> 49 + </div> 50 + <div class="flex items-center gap-3.5"> 51 + {{ with .Language }} 52 + {{ template "repo/fragments/colorBall" (dict "color" (langColor .)) }} 53 + <span>{{ . }}</span> 54 + {{ end }} 55 + </div> 56 + </div> 57 + {{ range $i, $chunk := .Chunks }} 58 + {{ if lt $i 3 }} 59 + {{ if gt $i 0 }} 60 + <div class="bg-gray-50 dark:bg-gray-700 text-center"> 61 + <span class="text-sm text-gray-500 dark:text-gray-400 select-none">···</span> 62 + </div> 63 + {{ end }} 64 + {{ template "search/fragments/chunkBody" (dict "Owner" $owner "Slug" $slug "Commit" $.Commit "FilePath" $.FilePath "Chunk" $chunk) }} 65 + {{ end }} 66 + {{ end }} 67 + {{ if gt (len .Chunks) 3 }} 68 + <details class="group flex flex-col divide-y divide-gray-200 dark:divide-gray-700"> 69 + <summary class="bg-gray-50 dark:bg-gray-700 text-gray-500 dark:text-gray-400 px-1 py-0.5 cursor-pointer list-none group-open:order-last group-open:border-t border-gray-200 dark:border-gray-700"> 70 + <div class="flex group-open:hidden"> 71 + {{ i "chevron-down" "size-3 m-1" }} 72 + <span class="text-sm select-none">Show {{ (slice .Chunks 3).MatchCount }} more matches</span> 73 + </div> 74 + <div class="hidden group-open:flex"> 75 + {{ i "chevron-up" "size-3 m-1" }} 76 + <span class="text-sm select-none">Show less</span> 77 + </div> 78 + </summary> 79 + {{ range $i, $chunk := .Chunks }} 80 + {{ if ge $i 3 }} 81 + <div class="bg-gray-50 dark:bg-gray-700 text-center"> 82 + <span class="text-sm text-gray-500 dark:text-gray-400 select-none">···</span> 83 + </div> 84 + {{ template "search/fragments/chunkBody" (dict "Owner" $owner "Slug" $slug "Commit" $.Commit "FilePath" $.FilePath "Chunk" $chunk) }} 85 + {{ end }} 86 + {{ end }} 87 + </details> 88 + {{ end }} 89 + </div> 90 + {{ end }} 91 + {{ end }}
+3
appview/pages/templates/repo/index.html
··· 24 24 <a href="/{{ .RepoInfo.FullName }}/tags" class="inline-flex md:hidden items-center text-sm gap-1 font-bold"> 25 25 {{ i "tags" "w-4" "h-4" }} {{ scaleFmt (len .Tags) }} 26 26 </a> 27 + <a href="/{{ .RepoInfo.FullName }}/search" class="btn"> 28 + {{ i "search" "size-4" }} 29 + </a> 27 30 {{ template "repo/fragments/cloneDropdown" . }} 28 31 </div> 29 32 </div>
+31
appview/pages/templates/repo/search.html
··· 1 + {{ define "title" }}Search &middot; {{ .RepoInfo.FullName }} &middot; Tangled{{ end }} 2 + 3 + {{ define "extrameta" }} 4 + {{ template "repo/fragments/meta" . }} 5 + 6 + {{ template "repo/fragments/og" (dict "RepoInfo" .RepoInfo) }} 7 + {{ end }} 8 + 9 + {{ define "repoContent" }} 10 + <main> 11 + <div class="flex relative w-full mb-4"> 12 + <div class="flex-1 flex relative"> 13 + <span class="absolute left-2 top-1/2 -translate-y-1/2 text-gray-400"> 14 + {{ i "search-code" "size-4" }} 15 + </span> 16 + <input 17 + type="text" 18 + name="q" 19 + value="" 20 + placeholder="Find files or code" 21 + hx-get="" 22 + hx-trigger="input changed delay:100ms from:input" 23 + hx-target="#repo-files" 24 + hx-swap="outerHTML" 25 + class="font-mono flex-1 py-1 pl-8 pr-2 peer" 26 + > 27 + </div> 28 + </div> 29 + {{ template "repo/fragments/searchResults" dict }} 30 + </main> 31 + {{ end }}
+95
appview/repo/filesearch.go
··· 1 + package repo 2 + 3 + import ( 4 + "fmt" 5 + "net/http" 6 + "slices" 7 + 8 + "github.com/bluesky-social/indigo/atproto/syntax" 9 + "tangled.org/core/appview/models" 10 + "tangled.org/core/appview/pages" 11 + "tangled.org/core/appview/pagination" 12 + ) 13 + 14 + func (rp *Repo) Search(w http.ResponseWriter, r *http.Request) { 15 + if r.Header.Get("Hx-Request") == "true" { 16 + rp.searchResultsFragment(w, r) 17 + return 18 + } 19 + if err := rp.pages.RepoSearchPage(w, pages.RepoSearchParams{ 20 + BaseParams: pages.BaseParamsFromContext(r.Context()), 21 + RepoInfo: rp.repoResolver.GetRepoInfo(r, rp.oauth.GetMultiAccountUser(r)), 22 + FilterQuery: "", 23 + }); err != nil { 24 + rp.logger.Error("failed to render", "err", err) 25 + } 26 + } 27 + 28 + func (rp *Repo) searchResultsFragment(w http.ResponseWriter, r *http.Request) { 29 + l := rp.logger.With("handler", "Search") 30 + repo, err := rp.repoResolver.Resolve(r) 31 + if err != nil { 32 + l.Error("failed to get repo", "err", err) 33 + return 34 + } 35 + q := r.URL.Query().Get("q") 36 + 37 + var params pages.RepoSearchResultsFragmentParams 38 + defer func() { 39 + if err := rp.pages.RepoSearchResultsFragment(w, params); err != nil { 40 + l.Error("failed to render", "err", err) 41 + } 42 + }() 43 + 44 + if q == "" { 45 + return 46 + } 47 + 48 + q = fmt.Sprintf(`meta.did:%s %q`, repo.RepoDid, q) 49 + 50 + ctx := r.Context() 51 + 52 + res, err := rp.codesearch.Search(ctx, q, pagination.Page{Limit: 50}) 53 + if err != nil { 54 + l.Error("failed to search files", "err", err) 55 + params.ErrorMsg = "Failed to perform search. Please try again later." 56 + return 57 + } 58 + 59 + if len(res.Results) == 0 { 60 + return 61 + } 62 + 63 + out := make([]pages.SearchResult, 0, len(res.Results)) 64 + for _, res := range res.Results { 65 + if res.RepoDID != syntax.DID(repo.RepoDid) { 66 + continue 67 + } 68 + csr := pages.SearchResult{ 69 + RepoDID: res.RepoDID, 70 + Repo: repo, 71 + FilePath: res.FilePath, 72 + Branches: res.Branches, 73 + Commit: res.Commit, 74 + Language: res.Language, 75 + } 76 + if f := res.File; f != nil { 77 + csr.File = &pages.CodeSearchResult_File{ 78 + NameSpans: pages.FileNameSpans(res.FilePath, f.Ranges), 79 + } 80 + } 81 + slices.SortStableFunc(res.Chunks, func(a, b models.Result_ChunkMatch) int { 82 + return a.ContentStartLine - b.ContentStartLine 83 + }) 84 + for _, c := range res.Chunks { 85 + csr.Chunks = append(csr.Chunks, pages.CodeSearchResult_Chunk{ 86 + Lines: pages.ChunkLines(c.Content, c.ContentStartLine, c.Ranges), 87 + MatchCount: len(c.Ranges), 88 + }) 89 + } 90 + out = append(out, csr) 91 + } 92 + 93 + l.Debug("repo file search result", "len", len(out), "duration", res.Stats.Duration) 94 + params.Results = out 95 + }
+4
appview/repo/repo.go
··· 13 13 "time" 14 14 15 15 "tangled.org/core/appview/cloudflare" 16 + "tangled.org/core/appview/codesearch" 16 17 17 18 "tangled.org/core/api/tangled" 18 19 "tangled.org/core/appview/config" ··· 61 62 validator *validator.Validator 62 63 cfClient *cloudflare.Client 63 64 ogreClient *ogre.Client 65 + codesearch *codesearch.CodeSearch 64 66 } 65 67 66 68 func New( ··· 77 79 logger *slog.Logger, 78 80 validator *validator.Validator, 79 81 cfClient *cloudflare.Client, 82 + codesearch *codesearch.CodeSearch, 80 83 ) *Repo { 81 84 return &Repo{ 82 85 oauth: oauth, ··· 93 96 validator: validator, 94 97 cfClient: cfClient, 95 98 ogreClient: ogre.NewClient(config.Ogre.Host), 99 + codesearch: codesearch, 96 100 } 97 101 } 98 102
+2
appview/repo/router.go
··· 76 76 r.Get("/edit", rp.EditLabelPanel) 77 77 }) 78 78 79 + r.Get("/search", rp.Search) 80 + 79 81 // settings routes, needs auth 80 82 r.Group(func(r chi.Router) { 81 83 r.Use(middleware.AuthMiddleware(rp.oauth))
+1
appview/state/router.go
··· 412 412 log.SubLogger(s.logger, "repo"), 413 413 s.validator, 414 414 s.cfClient, 415 + s.codesearch, 415 416 ) 416 417 return repo.Router(mw) 417 418 }