Forge
csharp3407750f
1using AgentForge.Data;
2using AgentForge.Data.Entities;
3using AgentForge.Models;
4using AgentForge.Services;
5
6namespace AgentForge.Plugin.View.Components.Search;
7
8internal static class ForgeViewSearchPage
9{
10 internal static string Render(
11 string repoName,
12 string query,
13 ForgeRepoEntity repo,
14 ForgeRepository repository,
15 GitRepoService git)
16 {
17 var trimmed = query.Trim();
18 var form = ForgeHtml.Form(
19 "get",
20 $"/view/repos/{ForgeRepoNames.ToUrlPath(repoName)}/search",
21 "search-page-form",
22 ForgeHtml.Input(new ForgeInputSpec("q", Type: "search", Value: trimmed, Placeholder: "Issues, merge requests, files…")),
23 ForgeHtml.Button("submit", "Search"));
24
25 if (trimmed.Length == 0)
26 {
27 return ForgeHtml.Fragment(
28 ForgeHtml.H1("Search"),
29 form,
30 ForgeHtml.P("meta", ForgeHtml.Text("Enter a query to search this repository.")));
31 }
32
33 var issues = SearchIssues(repository, repo.Id, trimmed);
34 var mrs = SearchMergeRequests(repository, repo.Id, trimmed);
35 var files = SearchFiles(git, repo.Name, git.GetDefaultBranch(repo.Name), trimmed);
36
37 if (issues.Count == 0 && mrs.Count == 0 && files.Count == 0)
38 {
39 return ForgeHtml.Fragment(
40 ForgeHtml.H1("Search"),
41 form,
42 ForgeHtml.P("meta",
43 ForgeHtml.Text("No results for "),
44 ForgeHtml.Strong(trimmed),
45 ForgeHtml.Text(".")));
46 }
47
48 var sections = new List<string> { ForgeHtml.H1("Search"), form };
49
50 if (issues.Count > 0)
51 sections.Add(RenderIssueSection(repoName, issues));
52 if (mrs.Count > 0)
53 sections.Add(RenderMergeRequestSection(repoName, mrs));
54 if (files.Count > 0)
55 sections.Add(RenderFileSection(repoName, git.GetDefaultBranch(repo.Name) ?? "main", files));
56
57 return ForgeHtml.Fragment([.. sections]);
58 }
59
60 private static string RenderIssueSection(string repoName, IReadOnlyList<ForgeIssueEntity> issues)
61 {
62 var items = issues.Select(issue =>
63 {
64 var href = $"/view/repos/{ForgeRepoNames.ToUrlPath(repoName)}/issues/{issue.Number}";
65 return ForgeHtml.Li(null,
66 ForgeHtml.A(href, $"#{issue.Number}"),
67 ForgeHtml.Text(" "),
68 ForgeHtml.Text(issue.Title),
69 ForgeHtml.Text(" "),
70 ForgeHtml.Badge(issue.Status.ToSlug()));
71 }).ToArray();
72
73 return ForgeHtml.Fragment(
74 ForgeHtml.H2("Issues"),
75 ForgeHtml.Ul("plain", items));
76 }
77
78 private static string RenderMergeRequestSection(string repoName, IReadOnlyList<ForgeMergeRequestEntity> mergeRequests)
79 {
80 var items = mergeRequests.Select(mr =>
81 {
82 var href = $"/view/repos/{ForgeRepoNames.ToUrlPath(repoName)}/merge-requests/{mr.Number}";
83 return ForgeHtml.Li(null,
84 ForgeHtml.A(href, $"!{mr.Number}"),
85 ForgeHtml.Text(" "),
86 ForgeHtml.Text(mr.Title),
87 ForgeHtml.Text(" "),
88 ForgeHtml.Badge(mr.Status.ToSlug()));
89 }).ToArray();
90
91 return ForgeHtml.Fragment(
92 ForgeHtml.H2("Merge requests"),
93 ForgeHtml.Ul("plain", items));
94 }
95
96 private static string RenderFileSection(string repoName, string branch, IReadOnlyList<string> files)
97 {
98 var items = files.Select(path =>
99 {
100 var href = $"/view/repos/{ForgeRepoNames.ToUrlPath(repoName)}/blob?branch={Uri.EscapeDataString(branch)}&path={Uri.EscapeDataString(path)}";
101 return ForgeHtml.Li(null, ForgeHtml.A(href, path));
102 }).ToArray();
103
104 return ForgeHtml.Fragment(
105 ForgeHtml.H2("Files"),
106 ForgeHtml.Ul("plain", items));
107 }
108
109 private static List<ForgeIssueEntity> SearchIssues(ForgeRepository repository, string repoId, string query) =>
110 repository.ListIssues(repoId)
111 .Where(issue => Matches(issue.Title, query) || Matches(issue.Body, query))
112 .OrderByDescending(issue => issue.Number)
113 .Take(20)
114 .ToList();
115
116 private static List<ForgeMergeRequestEntity> SearchMergeRequests(ForgeRepository repository, string repoId, string query) =>
117 repository.ListMergeRequests(repoId)
118 .Where(mr => Matches(mr.Title, query))
119 .OrderByDescending(mr => mr.Number)
120 .Take(20)
121 .ToList();
122
123 private static List<string> SearchFiles(GitRepoService git, string repoName, string? branch, string query)
124 {
125 if (string.IsNullOrWhiteSpace(branch) || !git.BareRepoExists(repoName))
126 return [];
127
128 var results = new List<string>();
129 WalkFiles(git, repoName, branch, path: null, query, results, limit: 25);
130 return results;
131 }
132
133 private static void WalkFiles(
134 GitRepoService git,
135 string repoName,
136 string branch,
137 string? path,
138 string query,
139 List<string> results,
140 int limit)
141 {
142 if (results.Count >= limit)
143 return;
144
145 IReadOnlyList<GitRepoService.GitTreeItem> entries;
146 try
147 {
148 entries = git.ListDirectory(repoName, branch, path);
149 }
150 catch
151 {
152 return;
153 }
154
155 foreach (var entry in entries)
156 {
157 if (results.Count >= limit)
158 return;
159
160 if (entry.IsDirectory)
161 {
162 WalkFiles(git, repoName, branch, entry.Path, query, results, limit);
163 continue;
164 }
165
166 if (entry.Path.Contains(query, StringComparison.OrdinalIgnoreCase))
167 results.Add(entry.Path);
168 }
169 }
170
171 private static bool Matches(string? value, string query) =>
172 !string.IsNullOrWhiteSpace(value)
173 && value.Contains(query, StringComparison.OrdinalIgnoreCase);
174}
175
View only · write via MCP/CIDE