@youtyan/code-viewer 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web/app.js CHANGED
@@ -835,6 +835,281 @@ Details: ${JSON.stringify(output)}` : "";
835
835
  applyTrailingResult
836
836
  };
837
837
 
838
+ // web-src/core/fuzzy-search.ts
839
+ function basenameStart(path) {
840
+ const slash = path.lastIndexOf("/");
841
+ return slash < 0 ? 0 : slash + 1;
842
+ }
843
+ function isBoundary(path, index) {
844
+ if (index <= 0) return true;
845
+ const prev = path[index - 1];
846
+ return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
847
+ }
848
+ function toRanges(indices) {
849
+ const ranges = [];
850
+ for (const index of indices) {
851
+ const last = ranges[ranges.length - 1];
852
+ if (last && last.end === index) {
853
+ last.end = index + 1;
854
+ } else {
855
+ ranges.push({ start: index, end: index + 1 });
856
+ }
857
+ }
858
+ return ranges;
859
+ }
860
+ function basenameMatchTier(loweredQuery, loweredBasename) {
861
+ if (loweredBasename === loweredQuery) return 4;
862
+ if (loweredBasename.startsWith(`${loweredQuery}.`)) return 3;
863
+ if (loweredBasename.startsWith(loweredQuery)) return 2;
864
+ if (loweredBasename.includes(loweredQuery)) return 1;
865
+ return 0;
866
+ }
867
+ function pathMatchTier(loweredQuery, loweredPath, loweredBasename) {
868
+ if (loweredQuery.includes("/") && (loweredPath === loweredQuery || loweredPath.endsWith(`/${loweredQuery}`)))
869
+ return 4;
870
+ return basenameMatchTier(loweredQuery, loweredBasename);
871
+ }
872
+ function contiguousPathRange(loweredQuery, loweredPath, baseStart) {
873
+ const loweredBasename = loweredPath.slice(baseStart);
874
+ const basenameMatchStart = loweredBasename.indexOf(loweredQuery);
875
+ if (basenameMatchStart >= 0) {
876
+ const start = baseStart + basenameMatchStart;
877
+ return { start, end: start + loweredQuery.length };
878
+ }
879
+ if (loweredQuery.includes("/")) {
880
+ const pathMatchStart = loweredPath.endsWith(`/${loweredQuery}`) ? loweredPath.length - loweredQuery.length : loweredPath === loweredQuery ? 0 : -1;
881
+ if (pathMatchStart >= 0)
882
+ return {
883
+ start: pathMatchStart,
884
+ end: pathMatchStart + loweredQuery.length
885
+ };
886
+ }
887
+ return null;
888
+ }
889
+ function computeFuzzyMatch(query, path) {
890
+ const q = query.trim().toLowerCase();
891
+ if (!q) return { score: 0, ranges: [], tier: 0 };
892
+ const lowerPath = path.toLowerCase();
893
+ const baseStart = basenameStart(path);
894
+ const indices = [];
895
+ let from = 0;
896
+ let score = 0;
897
+ for (const ch of q) {
898
+ const index = lowerPath.indexOf(ch, from);
899
+ if (index < 0) return null;
900
+ indices.push(index);
901
+ score += 10;
902
+ if (index >= baseStart) score += 8;
903
+ if (isBoundary(path, index)) score += 6;
904
+ const prev = indices[indices.length - 2];
905
+ if (prev != null && prev + 1 === index) score += 12;
906
+ from = index + 1;
907
+ }
908
+ const first = indices[0];
909
+ score -= Math.min(first, 40);
910
+ if (indices[0] >= baseStart) score += 20;
911
+ const basename = lowerPath.slice(baseStart);
912
+ const tier = pathMatchTier(q, lowerPath, basename);
913
+ const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
914
+ return {
915
+ score,
916
+ ranges: contiguousRange ? [contiguousRange] : toRanges(indices),
917
+ tier
918
+ };
919
+ }
920
+ function fuzzyMatchPath(query, path) {
921
+ const match2 = computeFuzzyMatch(query, path);
922
+ return match2 ? { score: match2.score, ranges: match2.ranges } : null;
923
+ }
924
+ function rankFuzzyPaths(query, items, limit, stats) {
925
+ const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
926
+ const compare = (a2, b2) => b2.tier - a2.tier || b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
927
+ if (!bounded) {
928
+ const ranked = items.map((item) => {
929
+ const match2 = computeFuzzyMatch(query, item.path);
930
+ return match2 ? { item, score: match2.score, ranges: match2.ranges, tier: match2.tier } : null;
931
+ }).filter(
932
+ (item) => item !== null
933
+ ).sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
934
+ if (stats) stats.total = ranked.length;
935
+ return ranked;
936
+ }
937
+ const top = [];
938
+ let total = 0;
939
+ for (const item of items) {
940
+ const match2 = computeFuzzyMatch(query, item.path);
941
+ if (!match2) continue;
942
+ total++;
943
+ const ranked = {
944
+ item,
945
+ score: match2.score,
946
+ ranges: match2.ranges,
947
+ tier: match2.tier
948
+ };
949
+ pushBoundedTop(top, ranked, bounded, compare);
950
+ }
951
+ if (stats) stats.total = total;
952
+ return top.sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
953
+ }
954
+ function pushBoundedTop(heap, item, limit, compareBestFirst) {
955
+ const isWorse = (a2, b2) => compareBestFirst(a2, b2) > 0;
956
+ const siftUp = (index) => {
957
+ while (index > 0) {
958
+ const parent = Math.floor((index - 1) / 2);
959
+ if (!isWorse(heap[index], heap[parent])) break;
960
+ [heap[index], heap[parent]] = [heap[parent], heap[index]];
961
+ index = parent;
962
+ }
963
+ };
964
+ const siftDown = (index) => {
965
+ while (true) {
966
+ const left = index * 2 + 1;
967
+ const right = left + 1;
968
+ let worst = index;
969
+ if (left < heap.length && isWorse(heap[left], heap[worst])) worst = left;
970
+ if (right < heap.length && isWorse(heap[right], heap[worst]))
971
+ worst = right;
972
+ if (worst === index) break;
973
+ [heap[index], heap[worst]] = [heap[worst], heap[index]];
974
+ index = worst;
975
+ }
976
+ };
977
+ if (heap.length < limit) {
978
+ heap.push(item);
979
+ siftUp(heap.length - 1);
980
+ return;
981
+ }
982
+ if (compareBestFirst(item, heap[0]) >= 0) return;
983
+ heap[0] = item;
984
+ siftDown(0);
985
+ }
986
+ function rankGlobPathMatches(query, items, limit, stats) {
987
+ const matchPath = createGlobPathMatcher(query);
988
+ if (!matchPath) {
989
+ if (stats) stats.total = 0;
990
+ return [];
991
+ }
992
+ const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
993
+ const compare = (a2, b2) => b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
994
+ if (!bounded) {
995
+ const ranked = items.map((item) => {
996
+ const match2 = matchPath(item.path);
997
+ return match2 ? {
998
+ item,
999
+ score: match2.score,
1000
+ ranges: match2.ranges,
1001
+ mode: "glob"
1002
+ } : null;
1003
+ }).filter((item) => item !== null).sort(compare);
1004
+ if (stats) stats.total = ranked.length;
1005
+ return ranked;
1006
+ }
1007
+ const top = [];
1008
+ let total = 0;
1009
+ for (const item of items) {
1010
+ const match2 = matchPath(item.path);
1011
+ if (!match2) continue;
1012
+ total++;
1013
+ const ranked = {
1014
+ item,
1015
+ score: match2.score,
1016
+ ranges: match2.ranges,
1017
+ mode: "glob"
1018
+ };
1019
+ pushBoundedTop(top, ranked, bounded, compare);
1020
+ }
1021
+ if (stats) stats.total = total;
1022
+ return top.sort(compare);
1023
+ }
1024
+ function rankPathMatches(query, items, limit, stats) {
1025
+ if (isGlobPathQuery(query)) {
1026
+ return rankGlobPathMatches(query, items, limit, stats);
1027
+ }
1028
+ return rankFuzzyPaths(query, items, limit, stats).map((item) => ({
1029
+ ...item,
1030
+ mode: "fuzzy"
1031
+ }));
1032
+ }
1033
+ function isGlobPathQuery(query) {
1034
+ return /[*?]/.test(query.trim());
1035
+ }
1036
+ function escapeRegexChar(ch) {
1037
+ return /[\\^$+?.()|{}]/.test(ch) ? `\\${ch}` : ch;
1038
+ }
1039
+ function globToRegExp(query) {
1040
+ const pattern = query.trim();
1041
+ if (!pattern) return null;
1042
+ let source = "^";
1043
+ for (let i2 = 0; i2 < pattern.length; i2++) {
1044
+ const ch = pattern[i2];
1045
+ if (ch === "*") {
1046
+ if (pattern[i2 + 1] === "*") {
1047
+ if (pattern[i2 + 2] === "/") {
1048
+ source += "(?:.*/)?";
1049
+ i2 += 2;
1050
+ } else {
1051
+ source += ".*";
1052
+ i2++;
1053
+ }
1054
+ } else {
1055
+ source += "[^/]*";
1056
+ }
1057
+ } else if (ch === "?") {
1058
+ source += "[^/]";
1059
+ } else if (ch === "[") {
1060
+ const close = pattern.indexOf("]", i2 + 1);
1061
+ if (close < 0) {
1062
+ source += "\\[";
1063
+ } else {
1064
+ const body = pattern.slice(i2 + 1, close).replace(/\\/g, "\\\\");
1065
+ source += `[${body}]`;
1066
+ i2 = close;
1067
+ }
1068
+ } else {
1069
+ source += escapeRegexChar(ch);
1070
+ }
1071
+ }
1072
+ source += "$";
1073
+ try {
1074
+ return new RegExp(source, "i");
1075
+ } catch {
1076
+ return null;
1077
+ }
1078
+ }
1079
+ function globMatchPath(query, path) {
1080
+ return createGlobPathMatcher(query)?.(path) ?? null;
1081
+ }
1082
+ function createGlobPathMatcher(query) {
1083
+ const regex = globToRegExp(query);
1084
+ if (!regex) return null;
1085
+ const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
1086
+ const suffix = query.replace(/^\*+/, "").toLowerCase();
1087
+ return (path) => {
1088
+ const baseStart = basenameStart(path);
1089
+ const basename = path.slice(baseStart);
1090
+ if (!regex.test(path) && (query.includes("/") || !regex.test(basename)))
1091
+ return null;
1092
+ const ranges = [];
1093
+ const lowerPath = path.toLowerCase();
1094
+ for (const part of literal) {
1095
+ const start = lowerPath.indexOf(part.toLowerCase());
1096
+ if (start >= 0) ranges.push({ start, end: start + part.length });
1097
+ }
1098
+ ranges.sort((a2, b2) => a2.start - b2.start || a2.end - b2.end);
1099
+ const mergedRanges = [];
1100
+ for (const range of ranges) {
1101
+ const last = mergedRanges[mergedRanges.length - 1];
1102
+ if (last && last.end >= range.start) {
1103
+ last.end = Math.max(last.end, range.end);
1104
+ } else {
1105
+ mergedRanges.push({ ...range });
1106
+ }
1107
+ }
1108
+ const score = 1e3 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(suffix) ? 50 : 0);
1109
+ return { score, ranges: mergedRanges };
1110
+ };
1111
+ }
1112
+
838
1113
  // web-src/core/file-filter.ts
839
1114
  function normalizeFileFilterQuery(value) {
840
1115
  return (value || "").toLowerCase().trim();
@@ -868,6 +1143,14 @@ Details: ${JSON.stringify(output)}` : "";
868
1143
  };
869
1144
  }
870
1145
  }
1146
+ if (raw.startsWith("~")) {
1147
+ const needle = raw.slice(1).trim();
1148
+ if (!needle) return { kind: "empty", match: () => true };
1149
+ return { kind: "fuzzy", match: (path) => !!fuzzyMatchPath(needle, path) };
1150
+ }
1151
+ if (isGlobPathQuery(raw)) {
1152
+ return { kind: "glob", match: (path) => !!globMatchPath(raw, path) };
1153
+ }
871
1154
  const q = normalizeFileFilterQuery(raw.startsWith("/") ? raw.slice(1) : raw);
872
1155
  return {
873
1156
  kind: "substring",
@@ -894,6 +1177,8 @@ Details: ${JSON.stringify(output)}` : "";
894
1177
  }
895
1178
  function keymapScope(target) {
896
1179
  if (target?.closest("#app-panel")) return "panel";
1180
+ if (target?.closest("#history-panel, .gdp-file-history-panel"))
1181
+ return "history";
897
1182
  if (target?.closest("#content")) return "main";
898
1183
  if (target?.closest("#sidebar")) return "sidebar";
899
1184
  return "global";
@@ -1158,7 +1443,13 @@ Details: ${JSON.stringify(output)}` : "";
1158
1443
  }
1159
1444
 
1160
1445
  // web-src/core/keymap.ts
1161
- var KEYMAP_SCOPES = ["global", "sidebar", "main", "panel"];
1446
+ var KEYMAP_SCOPES = [
1447
+ "global",
1448
+ "sidebar",
1449
+ "main",
1450
+ "panel",
1451
+ "history"
1452
+ ];
1162
1453
  var KEYMAP_ACTIONS = [
1163
1454
  "open-file-palette",
1164
1455
  "open-grep-palette",
@@ -1198,6 +1489,8 @@ Details: ${JSON.stringify(output)}` : "";
1198
1489
  "previous-hunk",
1199
1490
  "goto-diff",
1200
1491
  "goto-history",
1492
+ "history-next-commit",
1493
+ "history-previous-commit",
1201
1494
  "goto-repo",
1202
1495
  "toggle-terminal-panel",
1203
1496
  "toggle-sidebar",
@@ -1334,6 +1627,10 @@ Details: ${JSON.stringify(output)}` : "";
1334
1627
  { action: "previous-hunk", key: "{", shift: true },
1335
1628
  { action: "goto-diff", key: "d", pendingG: true },
1336
1629
  { action: "goto-history", key: "h", pendingG: true },
1630
+ { action: "history-next-commit", key: "arrowdown" },
1631
+ { action: "history-previous-commit", key: "arrowup" },
1632
+ { action: "history-next-commit", key: "j", scope: "history" },
1633
+ { action: "history-previous-commit", key: "k", scope: "history" },
1337
1634
  { action: "goto-repo", key: "r", pendingG: true },
1338
1635
  { action: "toggle-sidebar", key: "b" },
1339
1636
  { action: "toggle-terminal-panel", key: "`", ctrl: true },
@@ -1638,6 +1935,14 @@ Details: ${JSON.stringify(output)}` : "";
1638
1935
  if (remote.hostname.toLowerCase() !== "github.com") {
1639
1936
  return { url: base2, provider: "web" };
1640
1937
  }
1938
+ if (options.kind === "commit") {
1939
+ const sha = (options.ref || "").trim();
1940
+ if (!sha || sha === "worktree") return { url: base2, provider: "github" };
1941
+ return {
1942
+ url: `${base2}/commit/${encodeURIComponent(sha)}`,
1943
+ provider: "github"
1944
+ };
1945
+ }
1641
1946
  const ref = encodePath(resolvedRef(options.ref, options.fallbackRef));
1642
1947
  const path = encodePath(options.path || "");
1643
1948
  if (!ref || options.kind === "blob" && !path) {
@@ -1660,6 +1965,61 @@ Details: ${JSON.stringify(output)}` : "";
1660
1965
  return { url: `${target}${lineHash}`, provider: "github" };
1661
1966
  }
1662
1967
 
1968
+ // web-src/core/history.ts
1969
+ function parseHistoryLineRange(value) {
1970
+ const match2 = /^(\d+)-(\d+)$/.exec(value || "");
1971
+ if (!match2) return void 0;
1972
+ const a2 = Number(match2[1]);
1973
+ const b2 = Number(match2[2]);
1974
+ if (!(a2 > 0) || !(b2 > 0)) return void 0;
1975
+ return { start: Math.min(a2, b2), end: Math.max(a2, b2) };
1976
+ }
1977
+ function formatHistoryLineRange(range) {
1978
+ return `${range.start}-${range.end}`;
1979
+ }
1980
+ var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
1981
+ var HISTORY_PAGE_SIZE = 50;
1982
+ var HISTORY_AUTO_LOAD_MAX_PAGES = 20;
1983
+ function commitDiffRange(commit) {
1984
+ return { from: commit.parents[0] || EMPTY_TREE_SHA, to: commit.sha };
1985
+ }
1986
+ function shouldContinueAutoLoad(state) {
1987
+ if (state.found) return false;
1988
+ if (!state.hasMore) return false;
1989
+ return state.pagesLoaded < HISTORY_AUTO_LOAD_MAX_PAGES;
1990
+ }
1991
+ var MONTH_NAMES = [
1992
+ "January",
1993
+ "February",
1994
+ "March",
1995
+ "April",
1996
+ "May",
1997
+ "June",
1998
+ "July",
1999
+ "August",
2000
+ "September",
2001
+ "October",
2002
+ "November",
2003
+ "December"
2004
+ ];
2005
+ var DAY_MS = 24 * 60 * 60 * 1e3;
2006
+ function historyGroupLabel(whenIso, now) {
2007
+ const t2 = Date.parse(whenIso);
2008
+ if (!Number.isFinite(t2)) return "Unknown date";
2009
+ const dayStart = new Date(
2010
+ now.getFullYear(),
2011
+ now.getMonth(),
2012
+ now.getDate()
2013
+ ).getTime();
2014
+ if (t2 >= dayStart) return "Today";
2015
+ if (t2 >= dayStart - DAY_MS) return "Yesterday";
2016
+ if (t2 >= dayStart - 6 * DAY_MS) return "This week";
2017
+ const d2 = new Date(t2);
2018
+ if (d2.getFullYear() === now.getFullYear() && d2.getMonth() === now.getMonth())
2019
+ return "This month";
2020
+ return `${MONTH_NAMES[d2.getMonth()]} ${d2.getFullYear()}`;
2021
+ }
2022
+
1663
2023
  // web-src/core/shell.ts
1664
2024
  var MIN_SHELL_COLS = 20;
1665
2025
  var MAX_SHELL_COLS = 1e3;
@@ -1756,6 +2116,7 @@ Details: ${JSON.stringify(output)}` : "";
1756
2116
  };
1757
2117
  const rawView = params.get("view");
1758
2118
  const preview = params.get("preview") === "1";
2119
+ const hl = params.get("hl") || "";
1759
2120
  if (rawView === "blob") {
1760
2121
  return {
1761
2122
  screen: "file",
@@ -1765,6 +2126,7 @@ Details: ${JSON.stringify(output)}` : "";
1765
2126
  view: "blob",
1766
2127
  ...preview ? { preview: true } : {},
1767
2128
  ...line ? { line } : {},
2129
+ ...line && hl ? { hl } : {},
1768
2130
  ...params.get("virtual") === "off" ? { virtual: "off" } : {}
1769
2131
  };
1770
2132
  }
@@ -1786,6 +2148,9 @@ Details: ${JSON.stringify(output)}` : "";
1786
2148
  range,
1787
2149
  view: "history",
1788
2150
  ...params.get("commit") ? { commit: params.get("commit") || "" } : {},
2151
+ ...params.get("compare") ? { compare: params.get("compare") || "" } : {},
2152
+ ...params.get("q") ? { q: params.get("q") || "" } : {},
2153
+ ...parseHistoryLineRange(params.get("lines")) ? { lines: parseHistoryLineRange(params.get("lines")) } : {},
1789
2154
  ...line ? { line } : {}
1790
2155
  };
1791
2156
  }
@@ -1829,10 +2194,18 @@ Details: ${JSON.stringify(output)}` : "";
1829
2194
  };
1830
2195
  case "/history": {
1831
2196
  const commit = params.get("commit") || "";
2197
+ const compare = params.get("compare") || "";
2198
+ const q = params.get("q") || "";
2199
+ const path = params.get("path") || "";
2200
+ const lines = parseHistoryLineRange(params.get("lines"));
1832
2201
  return {
1833
2202
  screen: "history",
1834
2203
  ref: params.get("ref") || "HEAD",
1835
2204
  ...commit ? { commit } : {},
2205
+ ...compare ? { compare } : {},
2206
+ ...q ? { q } : {},
2207
+ ...path ? { path } : {},
2208
+ ...path && lines ? { lines } : {},
1836
2209
  range
1837
2210
  };
1838
2211
  }
@@ -1891,14 +2264,14 @@ Details: ${JSON.stringify(output)}` : "";
1891
2264
  }
1892
2265
  case "file":
1893
2266
  if (route.view === "blob") {
1894
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
2267
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=blob" + (route.preview ? "&preview=1" : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.line && route.hl ? `&hl=${encodeURIComponent(route.hl)}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
1895
2268
  }
1896
2269
  if (route.view === "blame") {
1897
2270
  const ref = route.ref || "worktree";
1898
2271
  return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(ref) + "&view=blame" + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
1899
2272
  }
1900
2273
  if (route.view === "history") {
1901
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
2274
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + "&view=history" + (route.commit ? `&commit=${encodeURIComponent(route.commit)}` : "") + (route.compare ? `&compare=${encodeURIComponent(route.compare)}` : "") + (route.q ? `&q=${encodeURIComponent(route.q)}` : "") + (route.lines ? `&lines=${encodeURIComponent(formatHistoryLineRange(route.lines))}` : "") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "");
1902
2275
  }
1903
2276
  return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.line ? `&line=${encodeURIComponent(formatLineTarget(route.line))}` : "") + (route.virtual === "off" ? "&virtual=off" : "");
1904
2277
  case "diff":
@@ -1922,7 +2295,12 @@ Details: ${JSON.stringify(output)}` : "";
1922
2295
  case "history": {
1923
2296
  const params = new URLSearchParams();
1924
2297
  if (route.ref && route.ref !== "HEAD") params.set("ref", route.ref);
2298
+ if (route.path) params.set("path", route.path);
2299
+ if (route.path && route.lines)
2300
+ params.set("lines", formatHistoryLineRange(route.lines));
1925
2301
  if (route.commit) params.set("commit", route.commit);
2302
+ if (route.compare) params.set("compare", route.compare);
2303
+ if (route.q) params.set("q", route.q);
1926
2304
  const qs = params.toString();
1927
2305
  return `/history${qs ? `?${qs}` : ""}`;
1928
2306
  }
@@ -1989,6 +2367,64 @@ Details: ${JSON.stringify(output)}` : "";
1989
2367
  function withTerminalOverlay(url, state) {
1990
2368
  return withQueryParam(url, "terminal", state);
1991
2369
  }
2370
+ function parseSearchResultsOverlay(search) {
2371
+ return new URLSearchParams(search).get("results");
2372
+ }
2373
+ function withSearchResultsOverlay(url, query) {
2374
+ return withQueryParam(url, "results", query);
2375
+ }
2376
+
2377
+ // web-src/core/search-palette.ts
2378
+ var PALETTE_RESULT_LIMIT = 50;
2379
+ var GREP_SELECTION_HISTORY_LIMIT = 100;
2380
+ var MIN_GREP_PALETTE_WIDTH = 720;
2381
+ var MAX_GREP_PALETTE_WIDTH = 2400;
2382
+ var MIN_GREP_PALETTE_HEIGHT = 420;
2383
+ var MAX_GREP_PALETTE_HEIGHT = 1400;
2384
+ function limitPaletteResults(items) {
2385
+ return items.slice(0, PALETTE_RESULT_LIMIT);
2386
+ }
2387
+ function movePaletteSelection(index, count, direction) {
2388
+ if (count <= 0) return -1;
2389
+ if (index < 0) return direction > 0 ? 0 : count - 1;
2390
+ return Math.max(0, Math.min(count - 1, index + direction));
2391
+ }
2392
+ function rememberPaletteSelection(history2, path) {
2393
+ const next = history2.filter((entry) => entry !== path);
2394
+ next.push(path);
2395
+ return next.slice(-GREP_SELECTION_HISTORY_LIMIT);
2396
+ }
2397
+ function rankPaletteResultsByHistory(items, history2) {
2398
+ const priority = new Map(history2.map((path, index) => [path, index + 1]));
2399
+ return items.map((item, index) => ({ item, index })).sort(
2400
+ (a2, b2) => (priority.get(b2.item.path) ?? 0) - (priority.get(a2.item.path) ?? 0) || a2.index - b2.index
2401
+ ).map(({ item }) => item);
2402
+ }
2403
+ function parseGrepQuery(raw) {
2404
+ const paths = [];
2405
+ const words = [];
2406
+ for (const token of raw.trim().split(/\s+/)) {
2407
+ if (!token) continue;
2408
+ const scoped = /^path:(.*)$/.exec(token);
2409
+ if (scoped) {
2410
+ if (scoped[1]) paths.push(scoped[1]);
2411
+ continue;
2412
+ }
2413
+ words.push(token);
2414
+ }
2415
+ return { term: words.join(" "), paths };
2416
+ }
2417
+ function buildGrepRequestParams(options) {
2418
+ const params = new URLSearchParams();
2419
+ params.set("ref", options.ref);
2420
+ params.set("q", options.term);
2421
+ params.set("max", String(options.max));
2422
+ if (options.regex) params.set("regex", "1");
2423
+ if (options.caseSensitive) params.set("case", "1");
2424
+ if (options.wholeWord) params.set("word", "1");
2425
+ if (options.hideTests) params.set("exclude_tests", "1");
2426
+ return params;
2427
+ }
1992
2428
 
1993
2429
  // web-src/views/media-player.ts
1994
2430
  var PLAYBACK_RATES = [1, 1.25, 1.5, 2];
@@ -11534,6 +11970,8 @@ ${frontmatter.yaml}
11534
11970
  header.appendChild(name);
11535
11971
  const repositoryWebLink = deps.createRepositoryWebLink?.(target);
11536
11972
  if (repositoryWebLink) header.appendChild(repositoryWebLink);
11973
+ const revisionNav = deps.createRevisionNav?.(target, activeTab);
11974
+ if (revisionNav) header.appendChild(revisionNav);
11537
11975
  sticky.appendChild(header);
11538
11976
  const tabsHost = document.createElement("div");
11539
11977
  tabsHost.className = "gdp-file-detail-tabs";
@@ -11753,7 +12191,8 @@ ${frontmatter.yaml}
11753
12191
  setRoute: deps.setRoute,
11754
12192
  setPreferredSourceTab: deps.setPreferredSourceTab,
11755
12193
  createFileBreadcrumb: deps.createFileBreadcrumb,
11756
- createRepositoryWebLink: deps.createRepositoryWebLink
12194
+ createRepositoryWebLink: deps.createRepositoryWebLink,
12195
+ createRevisionNav: deps.createRevisionNav
11757
12196
  },
11758
12197
  target,
11759
12198
  "blame"
@@ -27301,50 +27740,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27301
27740
  deps.setStatus("live");
27302
27741
  }
27303
27742
 
27304
- // web-src/core/history.ts
27305
- var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
27306
- var HISTORY_PAGE_SIZE = 50;
27307
- var HISTORY_AUTO_LOAD_MAX_PAGES = 20;
27308
- function commitDiffRange(commit) {
27309
- return { from: commit.parents[0] || EMPTY_TREE_SHA, to: commit.sha };
27310
- }
27311
- function shouldContinueAutoLoad(state) {
27312
- if (state.found) return false;
27313
- if (!state.hasMore) return false;
27314
- return state.pagesLoaded < HISTORY_AUTO_LOAD_MAX_PAGES;
27315
- }
27316
- var MONTH_NAMES = [
27317
- "January",
27318
- "February",
27319
- "March",
27320
- "April",
27321
- "May",
27322
- "June",
27323
- "July",
27324
- "August",
27325
- "September",
27326
- "October",
27327
- "November",
27328
- "December"
27329
- ];
27330
- var DAY_MS = 24 * 60 * 60 * 1e3;
27331
- function historyGroupLabel(whenIso, now) {
27332
- const t2 = Date.parse(whenIso);
27333
- if (!Number.isFinite(t2)) return "Unknown date";
27334
- const dayStart = new Date(
27335
- now.getFullYear(),
27336
- now.getMonth(),
27337
- now.getDate()
27338
- ).getTime();
27339
- if (t2 >= dayStart) return "Today";
27340
- if (t2 >= dayStart - DAY_MS) return "Yesterday";
27341
- if (t2 >= dayStart - 6 * DAY_MS) return "This week";
27342
- const d2 = new Date(t2);
27343
- if (d2.getFullYear() === now.getFullYear() && d2.getMonth() === now.getMonth())
27344
- return "This month";
27345
- return `${MONTH_NAMES[d2.getMonth()]} ${d2.getFullYear()}`;
27346
- }
27347
-
27348
27743
  // web-src/views/history-view.ts
27349
27744
  var HISTORY_BODY_COLLAPSE_LINES = 10;
27350
27745
  var HISTORY_WORKTREE_COMMIT = "worktree";
@@ -27357,7 +27752,21 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27357
27752
  refreshTitle: "Refresh commit history",
27358
27753
  refreshTitlePending: "History may have changed. Refresh",
27359
27754
  filterClearLabel: "Clear",
27360
- filterClearTitle: "Clear commit filter"
27755
+ filterClearTitle: "Clear commit filter",
27756
+ filterPlaceholder: "filter… text author: path: since: code:",
27757
+ filterTitle: 'Filter commits. Words match the message ("quoted" keeps spaces), sha prefixes match commits, author:<name>, path:<part>, since:/after:<date>, until:/before:<date>, code:<text> (lines added or removed), merges:no / merges:only. Kinds combine with AND.',
27758
+ commitsTitle: "Commits",
27759
+ commitsIn: (path) => `Commits · ${path}`,
27760
+ commitsForLines: (start, end) => start === end ? `Commits · line ${start}` : `Commits · lines ${start}-${end}`,
27761
+ copyShaTitle: "Copy full commit sha",
27762
+ copiedTitle: "Copied",
27763
+ copyFailedTitle: "Copy failed",
27764
+ mergeBadge: "merge",
27765
+ compareWith: "Compare with",
27766
+ parentLabel: (index, shortSha) => `parent ${index} (${shortSha})`,
27767
+ comparing: (from, to) => `Comparing ${from}..${to}`,
27768
+ clearCompare: "Clear",
27769
+ refTitle: (kind, name) => kind === "tag" ? `Tag ${name}` : kind === "head" ? "HEAD" : `Branch ${name}`
27361
27770
  },
27362
27771
  ja: {
27363
27772
  worktreeLabel: "未コミット変更 (Working tree)",
@@ -27367,7 +27776,21 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27367
27776
  refreshTitle: "コミット履歴を更新",
27368
27777
  refreshTitlePending: "新しい履歴がある可能性があります。更新",
27369
27778
  filterClearLabel: "解除",
27370
- filterClearTitle: "コミットフィルタを解除"
27779
+ filterClearTitle: "コミットフィルタを解除",
27780
+ filterPlaceholder: "絞り込み… 文字列 author: path: since: code:",
27781
+ filterTitle: 'コミットを絞り込みます。語はメッセージに一致("引用" で空白を含む句)、sha の前方一致、author:<名前>、path:<一部>、since:/after:<日付>、until:/before:<日付>、code:<文字列>(追加・削除された行)、merges:no / merges:only。種類が違う条件は AND。',
27782
+ commitsTitle: "コミット",
27783
+ commitsIn: (path) => `コミット · ${path}`,
27784
+ commitsForLines: (start, end) => start === end ? `コミット · ${start} 行目` : `コミット · ${start}-${end} 行`,
27785
+ copyShaTitle: "コミットの sha をコピー",
27786
+ copiedTitle: "コピーしました",
27787
+ copyFailedTitle: "コピーに失敗しました",
27788
+ mergeBadge: "merge",
27789
+ compareWith: "比較対象",
27790
+ parentLabel: (index, shortSha) => `親 ${index} (${shortSha})`,
27791
+ comparing: (from, to) => `${from}..${to} を比較中`,
27792
+ clearCompare: "解除",
27793
+ refTitle: (kind, name) => kind === "tag" ? `タグ ${name}` : kind === "head" ? "HEAD" : `ブランチ ${name}`
27371
27794
  }
27372
27795
  };
27373
27796
  function historyText(lang) {
@@ -27387,6 +27810,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27387
27810
  panel.className = "gdp-file-history-panel";
27388
27811
  }
27389
27812
  panel.setAttribute("aria-label", "Commit history");
27813
+ panel.tabIndex = -1;
27390
27814
  const panelHead = document.createElement("div");
27391
27815
  panelHead.className = "history-head";
27392
27816
  const title = document.createElement("span");
@@ -27422,8 +27846,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27422
27846
  if (page) filterInput.id = "history-filter";
27423
27847
  else filterInput.className = "history-filter";
27424
27848
  filterInput.type = "search";
27425
- filterInput.placeholder = page ? "filter commits… (message, sha, author:name, path:file)" : "filter commits… (message, sha, author:name)";
27849
+ filterInput.placeholder = HISTORY_TEXT.en.filterPlaceholder;
27850
+ filterInput.title = HISTORY_TEXT.en.filterTitle;
27426
27851
  filterInput.autocomplete = "off";
27852
+ const authorList = document.createElement("datalist");
27853
+ authorList.id = page ? "history-filter-authors" : `history-filter-authors-${Math.random().toString(36).slice(2, 8)}`;
27854
+ filterInput.setAttribute("list", authorList.id);
27427
27855
  const filterClearButton = document.createElement("button");
27428
27856
  filterClearButton.type = "button";
27429
27857
  if (page) filterClearButton.id = "history-filter-clear";
@@ -27435,7 +27863,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27435
27863
  "aria-label",
27436
27864
  HISTORY_TEXT.en.filterClearTitle
27437
27865
  );
27438
- filterWrap.append(filterInput, filterClearButton);
27866
+ filterWrap.append(filterInput, filterClearButton, authorList);
27439
27867
  const banner = document.createElement("div");
27440
27868
  if (page) banner.id = "history-banner";
27441
27869
  banner.className = "history-banner";
@@ -27462,6 +27890,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27462
27890
  sentinel,
27463
27891
  filterInput,
27464
27892
  filterClearButton,
27893
+ authorList,
27465
27894
  refreshButton,
27466
27895
  refreshResult
27467
27896
  };
@@ -27485,7 +27914,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27485
27914
  const date = document.createElement("span");
27486
27915
  if (page) date.id = "hci-date";
27487
27916
  date.className = "hci-date";
27488
- head.append(sha, author, date);
27917
+ const actions = document.createElement("span");
27918
+ actions.className = "hci-actions";
27919
+ head.append(sha, author, date, actions);
27489
27920
  const subject = document.createElement("h2");
27490
27921
  if (page) subject.id = "hci-subject";
27491
27922
  subject.className = "hci-subject";
@@ -27587,6 +28018,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27587
28018
  let pathFilter = "";
27588
28019
  let refreshStatus = { type: "none" };
27589
28020
  let freshSha = "";
28021
+ let compareSha = "";
28022
+ let authorsLoadedFor = "";
28023
+ let lineRange2;
28024
+ function lineRangeKey(range) {
28025
+ return range ? formatHistoryLineRange(range) : "";
28026
+ }
27590
28027
  function historyScopeFromRoute(route = deps.getRoute()) {
27591
28028
  if (route.screen === "history") {
27592
28029
  const nextRef = route.ref || "HEAD";
@@ -27594,8 +28031,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27594
28031
  mode: "history",
27595
28032
  logRef: nextRef,
27596
28033
  routeRef: nextRef,
27597
- pathFilter: "",
27598
- commit: route.commit
28034
+ pathFilter: route.path || "",
28035
+ query: route.q || "",
28036
+ lines: route.path ? route.lines : void 0,
28037
+ commit: route.commit,
28038
+ compare: route.compare
27599
28039
  };
27600
28040
  }
27601
28041
  if (route.screen === "file" && route.view === "history") {
@@ -27605,11 +28045,51 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27605
28045
  logRef: nextRouteRef === "worktree" ? "HEAD" : nextRouteRef,
27606
28046
  routeRef: nextRouteRef,
27607
28047
  pathFilter: route.path,
27608
- commit: route.commit
28048
+ query: route.q || "",
28049
+ lines: route.lines,
28050
+ commit: route.commit,
28051
+ compare: route.compare
27609
28052
  };
27610
28053
  }
27611
28054
  return null;
27612
28055
  }
28056
+ function routeFor(options) {
28057
+ const commit = options.commit;
28058
+ const compare = options.compare || void 0;
28059
+ const q = (options.query ?? query) || void 0;
28060
+ const range = commit && commit !== HISTORY_WORKTREE_COMMIT ? diffRangeFor(commit, compare) : worktreeDiffRange();
28061
+ if (mode === "file") {
28062
+ return {
28063
+ screen: "file",
28064
+ path: pathFilter,
28065
+ ref: options.ref ?? routeRef,
28066
+ view: "history",
28067
+ ...commit ? { commit } : {},
28068
+ ...compare ? { compare } : {},
28069
+ ...q ? { q } : {},
28070
+ ...lineRange2 ? { lines: lineRange2 } : {},
28071
+ range
28072
+ };
28073
+ }
28074
+ return {
28075
+ screen: "history",
28076
+ ref: options.ref ?? ref,
28077
+ ...pathFilter ? { path: pathFilter } : {},
28078
+ ...pathFilter && lineRange2 ? { lines: lineRange2 } : {},
28079
+ ...commit ? { commit } : {},
28080
+ ...compare ? { compare } : {},
28081
+ ...q ? { q } : {},
28082
+ range
28083
+ };
28084
+ }
28085
+ function diffRangeFor(sha, compare) {
28086
+ const commit = commits.find((item) => item.sha === sha);
28087
+ if (compare) return { from: compare, to: sha };
28088
+ return commit ? commitDiffRange(commit) : { from: `${sha}^`, to: sha };
28089
+ }
28090
+ function shortSha(sha) {
28091
+ return sha.slice(0, 7);
28092
+ }
27613
28093
  function currentRefreshScopeKey() {
27614
28094
  const scope = historyScopeFromRoute();
27615
28095
  if (!scope) return "";
@@ -27618,6 +28098,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27618
28098
  scope.logRef,
27619
28099
  scope.routeRef,
27620
28100
  scope.pathFilter,
28101
+ lineRangeKey(scope.lines),
27621
28102
  query
27622
28103
  ].join("\0");
27623
28104
  }
@@ -27699,6 +28180,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27699
28180
  if (query) params.set("q", query);
27700
28181
  if (pathFilter) {
27701
28182
  params.set("path", pathFilter);
28183
+ if (lineRange2) params.set("lines", formatHistoryLineRange(lineRange2));
27702
28184
  if (routeRef === "worktree") params.set("worktree", "1");
27703
28185
  }
27704
28186
  const url = `/_log?${params.toString()}`;
@@ -27715,10 +28197,43 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27715
28197
  return null;
27716
28198
  });
27717
28199
  }
27718
- function commitRow(commit) {
28200
+ function refChipsHtml(commit) {
28201
+ const text3 = historyText(deps.getLanguage());
28202
+ const chips = [];
28203
+ for (const ref2 of commit.refs ?? []) {
28204
+ const classes = [
28205
+ "history-ref",
28206
+ `history-ref-${ref2.kind}`,
28207
+ ref2.head ? "history-ref-head" : ""
28208
+ ].filter(Boolean).join(" ");
28209
+ chips.push(
28210
+ `<span class="${classes}" title="${deps.escapeHtml(text3.refTitle(ref2.kind, ref2.name))}">${deps.escapeHtml(ref2.name)}</span>`
28211
+ );
28212
+ }
28213
+ if (commit.parents.length > 1) {
28214
+ chips.push(
28215
+ `<span class="history-ref history-ref-merge" title="${deps.escapeHtml(text3.mergeBadge)}">${deps.escapeHtml(text3.mergeBadge)}</span>`
28216
+ );
28217
+ }
28218
+ return chips.length ? `<span class="history-refs">${chips.join("")}</span>` : "";
28219
+ }
28220
+ function commitRow(commit, inRange) {
27719
28221
  const active = commit.sha === selectedSha ? " active" : "";
27720
28222
  const fresh = commit.sha === freshSha ? " history-item-fresh" : "";
27721
- return `<li class="history-item${active}${fresh}" data-sha="${deps.escapeHtml(commit.sha)}"><span class="subject" title="${deps.escapeHtml(commit.subject)}">${deps.escapeHtml(commit.subject)}</span><span class="meta2"><span class="sha">${deps.escapeHtml(commit.sha.slice(0, 7))}</span><span class="author">${deps.escapeHtml(commit.author)}</span><span class="when">${deps.escapeHtml(displayWhen(commit.when))}</span></span></li>`;
28223
+ const ranged = inRange ? " history-item-in-range" : "";
28224
+ return `<li class="history-item${active}${fresh}${ranged}" data-sha="${deps.escapeHtml(commit.sha)}"><span class="subject" title="${deps.escapeHtml(commit.subject)}">${deps.escapeHtml(commit.subject)}</span><span class="meta2"><span class="sha">${deps.escapeHtml(shortSha(commit.sha))}</span><span class="author">${deps.escapeHtml(commit.author)}</span><span class="when">${deps.escapeHtml(displayWhen(commit.when))}</span>` + refChipsHtml(commit) + `</span></li>`;
28225
+ }
28226
+ function rangeShas() {
28227
+ const shas = /* @__PURE__ */ new Set();
28228
+ if (!compareSha || !selectedSha) return shas;
28229
+ const start = commits.findIndex((commit) => commit.sha === selectedSha);
28230
+ if (start < 0) return shas;
28231
+ const end = commits.findIndex(
28232
+ (commit, index) => index >= start && commit.parents[0] === compareSha
28233
+ );
28234
+ if (end < 0) return shas;
28235
+ for (let index = start; index <= end; index++) shas.add(commits[index].sha);
28236
+ return shas;
27722
28237
  }
27723
28238
  function worktreeRow() {
27724
28239
  const active = selectedSha === HISTORY_WORKTREE_COMMIT ? " active" : "";
@@ -27729,6 +28244,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27729
28244
  syncRefreshResult(activeMount.refreshResult);
27730
28245
  const now = /* @__PURE__ */ new Date();
27731
28246
  const html = mode === "history" ? [worktreeRow()] : [];
28247
+ const inRange = rangeShas();
27732
28248
  let lastGroup = "";
27733
28249
  for (const commit of commits) {
27734
28250
  if (commit.sha === HISTORY_WORKTREE_COMMIT) {
@@ -27742,7 +28258,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27742
28258
  );
27743
28259
  lastGroup = group;
27744
28260
  }
27745
- html.push(commitRow(commit));
28261
+ html.push(commitRow(commit, inRange.has(commit.sha)));
27746
28262
  }
27747
28263
  list2.innerHTML = html.join("");
27748
28264
  activeHistoryRow = list2.querySelector(".history-item.active");
@@ -27792,6 +28308,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27792
28308
  info.querySelector(".hci-head")?.removeAttribute("hidden");
27793
28309
  set2(".hci-sha", commit.sha);
27794
28310
  set2(".hci-author", commit.author);
28311
+ renderCommitActions(info, commit);
27795
28312
  const t2 = Date.parse(commit.when);
27796
28313
  set2(
27797
28314
  ".hci-date",
@@ -27818,6 +28335,80 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27818
28335
  }
27819
28336
  info.hidden = false;
27820
28337
  }
28338
+ function renderCommitActions(info, commit) {
28339
+ const actions = info.querySelector(".hci-actions");
28340
+ if (!actions) return;
28341
+ actions.replaceChildren();
28342
+ const text3 = historyText(deps.getLanguage());
28343
+ if (deps.copyText) {
28344
+ const copy = document.createElement("button");
28345
+ copy.type = "button";
28346
+ copy.className = "gdp-btn gdp-btn-sm hci-copy";
28347
+ copy.title = text3.copyShaTitle;
28348
+ copy.setAttribute("aria-label", text3.copyShaTitle);
28349
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
28350
+ let resetTimer = null;
28351
+ const copyText = deps.copyText;
28352
+ copy.addEventListener("click", () => {
28353
+ void copyText(commit.sha).then(() => {
28354
+ copy.innerHTML = iconSvg("octicon-check", CHECK_16_PATHS);
28355
+ copy.title = text3.copiedTitle;
28356
+ copy.classList.add("copied");
28357
+ }).catch((err) => {
28358
+ console.error("Failed to copy commit sha", err);
28359
+ copy.title = text3.copyFailedTitle;
28360
+ copy.classList.add("failed");
28361
+ }).finally(() => {
28362
+ if (resetTimer) clearTimeout(resetTimer);
28363
+ resetTimer = setTimeout(() => {
28364
+ copy.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
28365
+ copy.title = text3.copyShaTitle;
28366
+ copy.classList.remove("copied", "failed");
28367
+ }, 1200);
28368
+ });
28369
+ });
28370
+ actions.appendChild(copy);
28371
+ }
28372
+ const link2 = deps.commitWebLink?.(commit.sha);
28373
+ if (link2) {
28374
+ link2.classList.add("hci-link");
28375
+ actions.appendChild(link2);
28376
+ }
28377
+ if (commit.parents.length > 1) {
28378
+ const label = document.createElement("span");
28379
+ label.className = "hci-compare-label";
28380
+ label.textContent = text3.compareWith;
28381
+ actions.appendChild(label);
28382
+ commit.parents.forEach((parent, index) => {
28383
+ const button = document.createElement("button");
28384
+ button.type = "button";
28385
+ button.className = "gdp-btn gdp-btn-sm hci-parent";
28386
+ button.textContent = text3.parentLabel(index + 1, shortSha(parent));
28387
+ const active = compareSha ? compareSha === parent : index === 0;
28388
+ button.setAttribute("aria-pressed", String(active));
28389
+ button.addEventListener("click", () => {
28390
+ void selectCommit(commit, { compare: index === 0 ? "" : parent });
28391
+ });
28392
+ actions.appendChild(button);
28393
+ });
28394
+ }
28395
+ if (compareSha && !commit.parents.includes(compareSha)) {
28396
+ const label = document.createElement("span");
28397
+ label.className = "hci-compare-label";
28398
+ label.textContent = text3.comparing(
28399
+ shortSha(compareSha),
28400
+ shortSha(commit.sha)
28401
+ );
28402
+ const clear = document.createElement("button");
28403
+ clear.type = "button";
28404
+ clear.className = "gdp-btn gdp-btn-sm hci-compare-clear";
28405
+ clear.textContent = text3.clearCompare;
28406
+ clear.addEventListener("click", () => {
28407
+ void selectCommit(commit, { compare: "" });
28408
+ });
28409
+ actions.append(label, clear);
28410
+ }
28411
+ }
27821
28412
  function updateWorktreeInfo() {
27822
28413
  const info = commitInfoElement();
27823
28414
  if (!info) return;
@@ -27843,11 +28434,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27843
28434
  }
27844
28435
  activeHistoryRow = selectedSha ? list2.querySelector(historyItemSelector(selectedSha)) : null;
27845
28436
  activeHistoryRow?.classList.add("active");
28437
+ updateRangeRows();
27846
28438
  }
27847
- function isEditableTarget(target) {
27848
- return typeof Element === "function" && target instanceof Element && !!target.closest(
27849
- 'input, textarea, select, button, [contenteditable="true"]'
27850
- );
28439
+ function updateRangeRows() {
28440
+ const inRange = rangeShas();
28441
+ list2.querySelectorAll(".history-item").forEach((row) => {
28442
+ row.classList.toggle(
28443
+ "history-item-in-range",
28444
+ inRange.has(row.dataset.sha || "")
28445
+ );
28446
+ });
27851
28447
  }
27852
28448
  function selectableShas() {
27853
28449
  const shas = mode === "history" ? [HISTORY_WORKTREE_COMMIT] : [];
@@ -27909,63 +28505,46 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27909
28505
  const selectionGen = ++selectionGeneration;
27910
28506
  const gen = generation;
27911
28507
  selectedSha = commit.sha;
28508
+ compareSha = options.compare ?? "";
28509
+ if (compareSha === commit.parents[0]) compareSha = "";
27912
28510
  updateActiveRow();
27913
28511
  await updateCommitInfo(commit);
27914
28512
  if (selectionGen !== selectionGeneration || gen !== generation) return;
28513
+ const range = compareSha ? { from: compareSha, to: commit.sha } : commitDiffRange(commit);
27915
28514
  if (options.updateUrl !== false) {
27916
- const range = commitDiffRange(commit);
27917
- if (mode === "file") {
27918
- deps.setRoute(
27919
- {
27920
- screen: "file",
27921
- path: pathFilter,
27922
- ref: routeRef,
27923
- view: "history",
27924
- commit: commit.sha,
27925
- range
27926
- },
27927
- true
27928
- );
27929
- } else {
27930
- deps.setRoute(
27931
- { screen: "history", ref, commit: commit.sha, range },
27932
- true
27933
- );
27934
- }
28515
+ deps.setRoute(
28516
+ routeFor({ commit: commit.sha, compare: compareSha }),
28517
+ true
28518
+ );
27935
28519
  }
27936
28520
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27937
- await deps.applyCommitRange(
27938
- commitDiffRange(commit),
27939
- pathFilter || void 0
27940
- );
28521
+ await deps.applyCommitRange(range, pathFilter || void 0);
27941
28522
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27942
28523
  }
28524
+ async function selectRange(clicked) {
28525
+ const anchor = commits.find((item) => item.sha === selectedSha);
28526
+ if (!anchor || anchor.sha === clicked.sha) {
28527
+ await selectCommit(clicked, { compare: "" });
28528
+ return;
28529
+ }
28530
+ const anchorIndex = commits.indexOf(anchor);
28531
+ const clickedIndex = commits.indexOf(clicked);
28532
+ const newer = anchorIndex < clickedIndex ? anchor : clicked;
28533
+ const older = newer === anchor ? clicked : anchor;
28534
+ await selectCommit(newer, {
28535
+ compare: older.parents[0] || EMPTY_TREE_SHA
28536
+ });
28537
+ }
27943
28538
  async function selectWorktree(options = {}) {
27944
28539
  const selectionGen = ++selectionGeneration;
27945
28540
  const gen = generation;
27946
28541
  selectedSha = HISTORY_WORKTREE_COMMIT;
28542
+ compareSha = "";
27947
28543
  updateActiveRow();
27948
28544
  updateWorktreeInfo();
27949
28545
  const range = worktreeDiffRange();
27950
28546
  if (options.updateUrl !== false) {
27951
- if (mode === "file") {
27952
- deps.setRoute(
27953
- {
27954
- screen: "file",
27955
- path: pathFilter,
27956
- ref: routeRef,
27957
- view: "history",
27958
- commit: selectedSha,
27959
- range
27960
- },
27961
- true
27962
- );
27963
- } else {
27964
- deps.setRoute(
27965
- { screen: "history", ref, commit: selectedSha, range },
27966
- true
27967
- );
27968
- }
28547
+ deps.setRoute(routeFor({ commit: selectedSha }), true);
27969
28548
  }
27970
28549
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27971
28550
  await deps.applyCommitRange(range, pathFilter || void 0);
@@ -28003,10 +28582,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28003
28582
  } else {
28004
28583
  pagesLoaded = 1;
28005
28584
  }
28585
+ const compare = historyScopeFromRoute()?.compare || "";
28006
28586
  for (; ; ) {
28007
28587
  const found = commits.find((c2) => c2.sha.startsWith(sha));
28008
28588
  if (found) {
28009
- await selectCommit(found, { updateUrl: false });
28589
+ await selectCommit(found, { updateUrl: false, compare });
28010
28590
  scrollToSelected();
28011
28591
  return;
28012
28592
  }
@@ -28030,7 +28610,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28030
28610
  setBanner(`showing commit outside the loaded ${ref} log`);
28031
28611
  commits = [single, ...commits];
28032
28612
  renderList();
28033
- await selectCommit(single, { updateUrl: false });
28613
+ await selectCommit(single, { updateUrl: false, compare });
28034
28614
  scrollToSelected();
28035
28615
  }
28036
28616
  function scrollToSelected() {
@@ -28047,14 +28627,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28047
28627
  async function doEnterHistory(force = false) {
28048
28628
  const scope = historyScopeFromRoute();
28049
28629
  if (!scope) return;
28050
- const scopeChanged = scope.logRef !== ref || scope.routeRef !== routeRef || scope.pathFilter !== pathFilter || scope.mode !== mode;
28630
+ const scopeChanged = scope.logRef !== ref || scope.routeRef !== routeRef || scope.pathFilter !== pathFilter || lineRangeKey(scope.lines) !== lineRangeKey(lineRange2) || scope.query !== query || scope.mode !== mode;
28051
28631
  if (scopeChanged || force || commits.length === 0) {
28052
28632
  generation++;
28053
28633
  const gen = generation;
28054
28634
  ref = scope.logRef;
28055
28635
  routeRef = scope.routeRef;
28056
28636
  pathFilter = scope.pathFilter;
28637
+ lineRange2 = scope.lines;
28638
+ query = scope.query;
28057
28639
  mode = scope.mode;
28640
+ compareSha = "";
28641
+ syncFilterInputValue();
28642
+ syncPanelTitle();
28058
28643
  commits = [];
28059
28644
  hasMore = false;
28060
28645
  loading = false;
@@ -28084,33 +28669,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28084
28669
  }
28085
28670
  function onRefPicked(nextRef) {
28086
28671
  const value = nextRef && nextRef !== "worktree" ? nextRef : "HEAD";
28087
- if (mode === "file" && pathFilter) {
28088
- deps.setRoute(
28089
- {
28090
- screen: "file",
28091
- path: pathFilter,
28092
- ref: nextRef || "worktree",
28093
- view: "history",
28094
- range: { from: "HEAD", to: "worktree" }
28095
- },
28096
- false
28097
- );
28098
- } else {
28099
- deps.setRoute(
28100
- {
28101
- screen: "history",
28102
- ref: value,
28103
- range: { from: "HEAD", to: "worktree" }
28104
- },
28105
- false
28106
- );
28107
- }
28672
+ deps.setRoute(
28673
+ routeFor({
28674
+ ref: mode === "file" ? nextRef || "worktree" : value
28675
+ }),
28676
+ false
28677
+ );
28108
28678
  void enterHistory({ force: true });
28109
28679
  }
28110
28680
  function leaveHistory() {
28111
28681
  generation++;
28112
28682
  loading = false;
28113
28683
  inFlight = null;
28684
+ compareSha = "";
28114
28685
  if (filterTimer) {
28115
28686
  clearTimeout(filterTimer);
28116
28687
  filterTimer = void 0;
@@ -28125,17 +28696,33 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28125
28696
  function handleListClick(e2) {
28126
28697
  const row = e2.target.closest(".history-item");
28127
28698
  if (!row?.dataset.sha) return;
28699
+ panel.focus?.();
28128
28700
  if (row.dataset.sha === HISTORY_WORKTREE_COMMIT) {
28129
28701
  void selectWorktree();
28130
28702
  return;
28131
28703
  }
28132
28704
  const commit = commits.find((c2) => c2.sha === row.dataset.sha);
28133
- if (commit) selectCommit(commit);
28705
+ if (!commit) return;
28706
+ if (e2.shiftKey && selectedSha && selectedSha !== HISTORY_WORKTREE_COMMIT) {
28707
+ void selectRange(commit);
28708
+ return;
28709
+ }
28710
+ void selectCommit(commit, { compare: "" });
28134
28711
  }
28135
28712
  function applyFilter(next) {
28136
28713
  const value = next.trim();
28137
28714
  if (value === query) return;
28138
28715
  query = value;
28716
+ if (historyScopeFromRoute()) {
28717
+ deps.setRoute(
28718
+ routeFor({
28719
+ commit: selectedSha || void 0,
28720
+ compare: compareSha,
28721
+ query: value
28722
+ }),
28723
+ true
28724
+ );
28725
+ }
28139
28726
  generation++;
28140
28727
  selectionGeneration++;
28141
28728
  clearRefreshResult();
@@ -28224,6 +28811,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28224
28811
  attachedFilterInput.removeEventListener("input", handleFilterInput);
28225
28812
  attachedFilterInput.removeEventListener("change", handleFilterInput);
28226
28813
  attachedFilterInput.removeEventListener("keydown", handleFilterKeydown);
28814
+ attachedFilterInput.removeEventListener("focus", handleFilterFocus);
28227
28815
  attachedFilterInput = null;
28228
28816
  }
28229
28817
  if (attachedFilterClearButton) {
@@ -28256,8 +28844,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28256
28844
  input2.addEventListener("input", handleFilterInput);
28257
28845
  input2.addEventListener("change", handleFilterInput);
28258
28846
  input2.addEventListener("keydown", handleFilterKeydown);
28847
+ input2.addEventListener("focus", handleFilterFocus);
28259
28848
  attachedFilterInput = input2;
28260
28849
  }
28850
+ syncPanelTitle();
28851
+ syncFilterChrome();
28261
28852
  const filterClearButton = mount.filterClearButton ?? null;
28262
28853
  if (filterClearButton) {
28263
28854
  syncFilterClearButton(filterClearButton);
@@ -28281,14 +28872,50 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28281
28872
  );
28282
28873
  observer.observe(sentinel);
28283
28874
  }
28875
+ function syncFilterInputValue() {
28876
+ const input2 = activeMount.filterInput ?? null;
28877
+ if (input2 && input2.value !== query) input2.value = query;
28878
+ syncFilterClearButton();
28879
+ }
28880
+ function syncFilterChrome() {
28881
+ const input2 = activeMount.filterInput ?? null;
28882
+ if (!input2) return;
28883
+ const text3 = historyText(deps.getLanguage());
28884
+ input2.placeholder = text3.filterPlaceholder;
28885
+ input2.title = text3.filterTitle;
28886
+ }
28887
+ function syncPanelTitle() {
28888
+ const title = panel.querySelector?.(".history-title");
28889
+ if (!title) return;
28890
+ const text3 = historyText(deps.getLanguage());
28891
+ title.textContent = lineRange2 ? text3.commitsForLines(lineRange2.start, lineRange2.end) : mode === "history" && pathFilter ? text3.commitsIn(pathFilter) : text3.commitsTitle;
28892
+ }
28893
+ function handleFilterFocus() {
28894
+ const list3 = activeMount.authorList ?? null;
28895
+ if (!list3 || typeof fetch !== "function") return;
28896
+ const key = ref;
28897
+ if (authorsLoadedFor === key) return;
28898
+ authorsLoadedFor = key;
28899
+ void deps.trackLoad(
28900
+ fetch(`/_authors?ref=${encodeURIComponent(ref)}`).then(async (r2) => {
28901
+ if (!r2.ok) throw new Error(await r2.text());
28902
+ return await r2.json();
28903
+ })
28904
+ ).then((res) => {
28905
+ if (activeMount.authorList !== list3 || key !== ref) return;
28906
+ list3.replaceChildren(
28907
+ ...res.authors.map((author) => {
28908
+ const option = document.createElement("option");
28909
+ option.value = `author:${author.name}`;
28910
+ return option;
28911
+ })
28912
+ );
28913
+ }).catch((err) => {
28914
+ authorsLoadedFor = "";
28915
+ console.error("Failed to load history authors", err);
28916
+ });
28917
+ }
28284
28918
  const docWithEvents = document;
28285
- docWithEvents.addEventListener?.("keydown", (e2) => {
28286
- if (isImeComposing(e2) || isEditableTarget(e2.target)) return;
28287
- if (e2.key !== "ArrowDown" && e2.key !== "ArrowUp") return;
28288
- if (!historyScopeFromRoute()) return;
28289
- e2.preventDefault();
28290
- void moveSelection(e2.key === "ArrowDown" ? 1 : -1);
28291
- });
28292
28919
  docWithEvents.addEventListener?.("input", (e2) => {
28293
28920
  if (e2.target !== activeMount.filterInput) return;
28294
28921
  handleFilterInput(e2);
@@ -28306,8 +28933,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28306
28933
  syncRefreshButton(activeMount.refreshButton);
28307
28934
  syncRefreshResult(activeMount.refreshResult);
28308
28935
  syncFilterClearButton(activeMount.filterClearButton);
28936
+ syncFilterChrome();
28937
+ syncPanelTitle();
28309
28938
  renderList();
28310
28939
  },
28940
+ // Keyboard stepping through the commit list (keymap actions
28941
+ // history-next-commit / history-previous-commit).
28942
+ moveCommitSelection: (delta) => moveSelection(delta),
28311
28943
  // Called from the SSE "update" listener when a history panel is on
28312
28944
  // screen. Only updates existing local UI hints — no fetch, no list redraw,
28313
28945
  // no generation bump, so it can't race with trackLoad/cancelInFlightRequests.
@@ -28563,8 +29195,28 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28563
29195
  {
28564
29196
  selectors: [{ action: "goto-history" }],
28565
29197
  description: {
28566
- en: "Go to the history screen",
28567
- ja: "履歴画面へ移動"
29198
+ en: "Go to the history screen (keeps the ref you are viewing)",
29199
+ ja: "履歴画面へ移動(見ている ref を引き継ぐ)"
29200
+ }
29201
+ },
29202
+ {
29203
+ selectors: [
29204
+ { action: "history-next-commit", key: "arrowdown" },
29205
+ { action: "history-previous-commit", key: "arrowup" }
29206
+ ],
29207
+ description: {
29208
+ en: "Select the next / previous commit in the history list",
29209
+ ja: "履歴一覧で次 / 前のコミットを選ぶ"
29210
+ }
29211
+ },
29212
+ {
29213
+ selectors: [
29214
+ { action: "history-next-commit", scope: "history" },
29215
+ { action: "history-previous-commit", scope: "history" }
29216
+ ],
29217
+ description: {
29218
+ en: "Same, while the commit list has focus",
29219
+ ja: "同上(コミット一覧にフォーカスがあるとき)"
28568
29220
  }
28569
29221
  },
28570
29222
  {
@@ -29247,6 +29899,23 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
29247
29899
  }
29248
29900
  ]
29249
29901
  },
29902
+ {
29903
+ title: "Search and history",
29904
+ blocks: [
29905
+ {
29906
+ kind: "paragraph",
29907
+ text: "Ctrl+K opens the file palette and Ctrl+G the text palette; the search button at the left of the header icons does the same (Shift+click for text). The two share one window: switching keeps what you typed, and reopening restores the last query, selected so typing replaces it. With an empty query the file palette lists the files you opened most recently, and the result line says when the ranking was cut at 50. The text palette has regex (Alt+R), match-case (Alt+C) and whole-word (Alt+W) toggles, and path:<dir or glob> tokens in the query narrow the search; matching is case-insensitive on every engine unless match-case is on. Opening a hit marks the matched text on the target line, and in a large virtualized file it pre-fills the in-file find bar. Pin (or Ctrl+Enter) moves the query into the bottom panel's Search tab, where the grouped result list stays open while you browse files; the query is part of the URL (?results=) so a reload re-runs it."
29908
+ },
29909
+ {
29910
+ kind: "paragraph",
29911
+ text: "The sidebar filter takes plain text (anywhere in the path), /pattern/ for a regex, ~text for a fuzzy match and *.ts or src/** for a glob, and shows matching / all file counts while active."
29912
+ },
29913
+ {
29914
+ kind: "paragraph",
29915
+ text: `The History screen filter understands words (message text, "quoted" keeps spaces), sha prefixes, author:<name>, path:<part>, since:/after:<date>, until:/before:<date>, code:<text> (lines added or removed) and merges:no / merges:only; different kinds combine with AND, author: offers the repository's authors as suggestions, and the filter is part of the URL (?q=) so a reload or shared link keeps it. Rows show branch and tag labels and a merge marker. The selected commit can be copied (sha) or opened on GitHub, Shift+click a second commit to diff the whole range between them, and a merge commit lets you pick which parent to compare against. ↑ / ↓ step commits anywhere on the screen, j / k do when the commit list has focus, and g h opens the history of the ref you are looking at. A folder page has a History button that opens the log restricted to that folder; selected source lines offer a Line history action (git log -L) in the line-reference pill, file pages have older / newer revision buttons that step through the commits that touched that file, and the ref picker keeps the refs you picked last as quick chips.`
29916
+ }
29917
+ ]
29918
+ },
29250
29919
  {
29251
29920
  title: "Scratch on pasted text",
29252
29921
  blocks: [
@@ -30014,6 +30683,23 @@ code-viewer annotate add-db --db app.db --tab query \\
30014
30683
  }
30015
30684
  ]
30016
30685
  },
30686
+ {
30687
+ title: "検索と履歴",
30688
+ blocks: [
30689
+ {
30690
+ kind: "paragraph",
30691
+ text: "Ctrl+K でファイルパレット、Ctrl+G でコード検索パレットが開きます。ヘッダのアイコン列の左端にある検索ボタンでも同じです(Shift+クリックでコード検索)。2 つは 1 つのウィンドウを共有し、切り替えても入力中の検索語は残り、閉じて開き直すと前回の検索語が選択状態で戻ります。ファイルパレットは空のとき最近開いたファイルを並べ、結果が 50 件で切られたときはその旨を表示します。コード検索には正規表現(Alt+R)・大文字小文字の区別(Alt+C)・単語単位(Alt+W)の切り替えがあり、検索語の中の path:<ディレクトリ or glob> で対象を絞れます。大文字小文字は「区別する」を押さない限りどのエンジンでも区別しません。ヒットを開くと該当行の一致箇所が強調され、大きな仮想表示のファイルではファイル内検索バーに検索語が入ります。「固定」(または Ctrl+Enter)を押すと検索語が下パネルの「検索」タブに移り、ファイルを開いて回る間も結果一覧が残ります。検索語は URL(?results=)に載るのでリロードしても同じ検索が走ります。"
30692
+ },
30693
+ {
30694
+ kind: "paragraph",
30695
+ text: "サイドバーの絞り込みは文字列(パスの部分一致)、/pattern/(正規表現)、~text(あいまい一致)、*.ts や src/**(glob)を受け付け、絞り込み中は「一致 / 全体」の件数を表示します。"
30696
+ },
30697
+ {
30698
+ kind: "paragraph",
30699
+ text: '履歴画面の絞り込みは、語(メッセージ本文。"引用" で空白を含む句)、sha の前方一致、author:<名前>、path:<一部>、since:/after:<日付>、until:/before:<日付>、code:<文字列>(追加・削除された行)、merges:no / merges:only を解釈し、種類の違う条件は AND で組み合わさります。author: にはリポジトリの著者名が候補として出て、絞り込みは URL(?q=)に載るのでリロードや共有でも残ります。各行にはブランチ / タグのラベルとマージ印が付きます。選択したコミットは sha をコピーしたり GitHub で開いたりでき、別のコミットを Shift+クリックするとその区間全体の差分、マージコミットでは比較する親を選べます。↑ / ↓ は画面のどこでも、j / k はコミット一覧にフォーカスがあるときにコミットを送り、g h は見ている ref の履歴を開きます。フォルダページの「履歴」ボタンでそのフォルダに絞った履歴が開きます。ソースの行を選択すると行参照ピルに「この行の履歴」(git log -L)が出て、ファイルページには前後のリビジョンへ移るボタン、ref ピッカーには最近使った ref のチップが並びます。'
30700
+ }
30701
+ ]
30702
+ },
30017
30703
  {
30018
30704
  title: "貼り付けたテキストを扱う",
30019
30705
  blocks: [
@@ -33373,7 +34059,12 @@ code-viewer annotate add-db --db app.db --tab query \\
33373
34059
  githubCopy.type = "button";
33374
34060
  githubCopy.innerHTML = `<span class="lrp-github-copy-icon">${iconSvg("octicon-copy", COPY_16_PATHS)}</span><span class="lrp-github-copy-label"></span>`;
33375
34061
  githubActions.append(githubOpen, githubCopy);
33376
- pill.append(copyButton, githubActions, closeButton);
34062
+ const historyButton = document.createElement("button");
34063
+ historyButton.id = "line-ref-pill-history";
34064
+ historyButton.type = "button";
34065
+ historyButton.hidden = !deps.openLineHistory;
34066
+ historyButton.innerHTML = iconSvg("octicon-git-branch", GIT_BRANCH_16_PATH) + '<span class="lrp-history-label"></span>';
34067
+ pill.append(copyButton, githubActions, historyButton, closeButton);
33377
34068
  document.body.appendChild(pill);
33378
34069
  let refText = "";
33379
34070
  let currentPath = "";
@@ -33382,6 +34073,14 @@ code-viewer annotate add-db --db app.db --tab query \\
33382
34073
  let githubUrl = "";
33383
34074
  let feedbackTimer = null;
33384
34075
  let githubFeedbackTimer = null;
34076
+ function renderHistoryAction() {
34077
+ if (!deps.openLineHistory) return;
34078
+ const title = deps.lineHistoryTitle?.() ?? "Line history";
34079
+ historyButton.title = title;
34080
+ historyButton.setAttribute("aria-label", title);
34081
+ const label = historyButton.querySelector(".lrp-history-label");
34082
+ if (label) label.textContent = title;
34083
+ }
33385
34084
  function renderGithubActions() {
33386
34085
  const openTitle = deps.githubOpenTitle();
33387
34086
  const copyTitle = deps.githubCopyTitle();
@@ -33473,6 +34172,10 @@ code-viewer annotate add-db --db app.db --tab query \\
33473
34172
  if (icon) icon.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
33474
34173
  }, 1200);
33475
34174
  });
34175
+ historyButton.addEventListener("click", () => {
34176
+ if (!currentPath || !deps.openLineHistory) return;
34177
+ deps.openLineHistory(currentPath, currentStart, currentEnd);
34178
+ });
33476
34179
  closeButton.addEventListener("click", () => {
33477
34180
  deps.onClose();
33478
34181
  });
@@ -33487,6 +34190,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33487
34190
  currentEnd = Math.max(1, Math.floor(Math.max(start, end)));
33488
34191
  githubUrl = deps.githubUrlForSelection(currentPath, currentStart, currentEnd) || "";
33489
34192
  renderGithubActions();
34193
+ renderHistoryAction();
33490
34194
  if (feedbackTimer) {
33491
34195
  clearTimeout(feedbackTimer);
33492
34196
  feedbackTimer = null;
@@ -33594,6 +34298,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33594
34298
  }
33595
34299
 
33596
34300
  // web-src/views/ref-picker.ts
34301
+ var QUICK_REF_VALUES = /* @__PURE__ */ new Set(["worktree", "HEAD", "--staged"]);
33597
34302
  function createRefPicker(deps) {
33598
34303
  function wireRefSelectorInput(input2, onPick) {
33599
34304
  const wrap = input2.closest("[data-ref-selector]");
@@ -33613,6 +34318,37 @@ code-viewer annotate add-db --db app.db --tab query \\
33613
34318
  });
33614
34319
  if (onPick)
33615
34320
  input2.addEventListener("change", () => onPick(input2.value || "worktree"));
34321
+ input2.addEventListener("change", () => rememberRef(input2.value));
34322
+ }
34323
+ function rememberRef(value) {
34324
+ const ref = (value || "").trim();
34325
+ if (!ref || QUICK_REF_VALUES.has(ref)) return;
34326
+ deps.rememberRecentRef?.(ref);
34327
+ }
34328
+ function renderRecentChips(current) {
34329
+ let row = popover.querySelector(".rp-recent");
34330
+ const recent = (deps.getRecentRefs?.() ?? []).filter((ref) => ref && !QUICK_REF_VALUES.has(ref)).reverse();
34331
+ if (!row) {
34332
+ row = document.createElement("div");
34333
+ row.className = "rp-recent";
34334
+ const quick = popover.querySelector(".rp-quick");
34335
+ if (quick) quick.after(row);
34336
+ else popover.prepend(row);
34337
+ }
34338
+ row.replaceChildren();
34339
+ row.hidden = recent.length === 0;
34340
+ const title = deps.recentRefTitle?.() ?? "";
34341
+ for (const ref of recent) {
34342
+ const chip = document.createElement("button");
34343
+ chip.type = "button";
34344
+ chip.className = "rp-chip rp-chip-recent";
34345
+ chip.dataset.val = ref;
34346
+ chip.textContent = ref;
34347
+ if (title) chip.title = title;
34348
+ chip.classList.toggle("current", ref === current);
34349
+ chip.addEventListener("click", () => handlePicked(ref));
34350
+ row.appendChild(chip);
34351
+ }
33616
34352
  }
33617
34353
  const REFS = {
33618
34354
  branches: [],
@@ -33837,6 +34573,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33837
34573
  popover.querySelectorAll(".rp-chip").forEach((c2) => {
33838
34574
  c2.classList.toggle("current", c2.dataset.val === cur);
33839
34575
  });
34576
+ renderRecentChips(cur);
33840
34577
  popover.hidden = false;
33841
34578
  const r2 = input2.getBoundingClientRect();
33842
34579
  const popWidth = Math.min(560, Math.floor(window.innerWidth * 0.9));
@@ -34720,6 +35457,8 @@ code-viewer annotate add-db --db app.db --tab query \\
34720
35457
  input2.title = invalid ? filter.error || "invalid regular expression" : "";
34721
35458
  const filterActive = filter.kind !== "empty" && !invalid;
34722
35459
  const matches2 = invalid ? () => true : filter.match;
35460
+ let totalFiles = 0;
35461
+ let visibleFiles = 0;
34723
35462
  const walk = (node, depth) => {
34724
35463
  let subtreeVisible = false;
34725
35464
  const rows = [];
@@ -34744,7 +35483,9 @@ code-viewer annotate add-db --db app.db --tab query \\
34744
35483
  } else {
34745
35484
  const testHidden = STATE.hideTests && !isRepositorySidebarMode() && isTestPath(item.file.path || "");
34746
35485
  const visible = !testHidden && matches2(item.file.path || "");
35486
+ if (!testHidden) totalFiles++;
34747
35487
  if (visible) {
35488
+ visibleFiles++;
34748
35489
  rows.push({
34749
35490
  kind: "file",
34750
35491
  path: item.file.path,
@@ -34759,6 +35500,31 @@ code-viewer annotate add-db --db app.db --tab query \\
34759
35500
  return { visible: subtreeVisible, rows };
34760
35501
  };
34761
35502
  SIDEBAR_VISIBLE_ROWS = walk(SIDEBAR_TREE_ROOT, 0).rows;
35503
+ syncSidebarFilterCount(filterActive, visibleFiles, totalFiles);
35504
+ }
35505
+ let SIDEBAR_TOTALS_BASE = "";
35506
+ let SIDEBAR_TOTALS_FILTER_TEXT = "";
35507
+ function setSidebarTotals(text3) {
35508
+ SIDEBAR_TOTALS_BASE = text3;
35509
+ const el2 = $("#totals");
35510
+ if (el2.textContent === SIDEBAR_TOTALS_FILTER_TEXT) el2.textContent = text3;
35511
+ if (!SIDEBAR_TOTALS_FILTER_TEXT) el2.textContent = text3;
35512
+ }
35513
+ function syncSidebarFilterCount(active, visible, total) {
35514
+ const el2 = document.querySelector("#totals");
35515
+ if (!el2) return;
35516
+ if (!active) {
35517
+ if (SIDEBAR_TOTALS_FILTER_TEXT) {
35518
+ if (el2.textContent === SIDEBAR_TOTALS_FILTER_TEXT)
35519
+ el2.textContent = SIDEBAR_TOTALS_BASE;
35520
+ el2.removeAttribute("title");
35521
+ SIDEBAR_TOTALS_FILTER_TEXT = "";
35522
+ }
35523
+ return;
35524
+ }
35525
+ SIDEBAR_TOTALS_FILTER_TEXT = `${visible} / ${total}`;
35526
+ el2.textContent = SIDEBAR_TOTALS_FILTER_TEXT;
35527
+ el2.title = deps.filterCountTitle(visible, total);
34762
35528
  }
34763
35529
  function sidebarVirtualRange() {
34764
35530
  const sidebar = document.querySelector("#sidebar");
@@ -35028,7 +35794,9 @@ code-viewer annotate add-db --db app.db --tab query \\
35028
35794
  } else {
35029
35795
  renderFlat(files, ul, onFileClick);
35030
35796
  }
35031
- $("#totals").textContent = !repoSidebar && files.length ? `${files.length} file${files.length === 1 ? "" : "s"}` : "";
35797
+ setSidebarTotals(
35798
+ !repoSidebar && files.length ? `${files.length} file${files.length === 1 ? "" : "s"}` : ""
35799
+ );
35032
35800
  const effectiveView = treeMode ? "tree" : STATE.sbView;
35033
35801
  $$(".sb-view-seg button").forEach((b2) => {
35034
35802
  b2.classList.toggle("active", b2.dataset.view === effectiveView);
@@ -35143,10 +35911,16 @@ code-viewer annotate add-db --db app.db --tab query \\
35143
35911
  input2.title = invalid ? filter.error || "invalid regular expression" : "";
35144
35912
  const matches2 = invalid ? () => true : filter.match;
35145
35913
  const filterActive = filter.kind !== "empty" && !invalid;
35914
+ let totalFiles = 0;
35915
+ let visibleFiles = 0;
35146
35916
  $$("#filelist li[data-path]").forEach((li) => {
35147
35917
  const match2 = matches2(li.dataset.path || "");
35148
35918
  li.classList.toggle("hidden", !match2);
35919
+ if (li.classList.contains("hidden-by-tests")) return;
35920
+ totalFiles++;
35921
+ if (match2) visibleFiles++;
35149
35922
  });
35923
+ syncSidebarFilterCount(filterActive, visibleFiles, totalFiles);
35150
35924
  if (!isRepositorySidebarMode()) {
35151
35925
  document.querySelectorAll(".gdp-file-shell").forEach((card) => {
35152
35926
  const match2 = matches2(card.dataset.path || "");
@@ -35652,6 +36426,9 @@ code-viewer annotate add-db --db app.db --tab query \\
35652
36426
  repositoryWebTarget,
35653
36427
  openGithubLabel,
35654
36428
  openRepositoryWebLabel,
36429
+ folderHistoryLabel,
36430
+ folderHistoryTitle,
36431
+ openFolderHistory,
35655
36432
  fileBadge
35656
36433
  } = deps;
35657
36434
  let REPO_SORT = {
@@ -36068,6 +36845,15 @@ code-viewer annotate add-db --db app.db --tab query \\
36068
36845
  meta.ref
36069
36846
  );
36070
36847
  if (repositoryWebLink) toolbar.appendChild(repositoryWebLink);
36848
+ const historyButton = document.createElement("button");
36849
+ historyButton.type = "button";
36850
+ historyButton.className = "gdp-btn gdp-btn-sm gdp-repo-history-btn";
36851
+ historyButton.textContent = folderHistoryLabel();
36852
+ historyButton.title = folderHistoryTitle();
36853
+ historyButton.addEventListener("click", () => {
36854
+ openFolderHistory(meta.ref, meta.path || "");
36855
+ });
36856
+ toolbar.appendChild(historyButton);
36071
36857
  if (canTrashWorktreeRef(meta.ref)) {
36072
36858
  toolbar.appendChild(
36073
36859
  createNewFolderButton(meta.path || "", () => loadRepo())
@@ -36633,294 +37419,12 @@ code-viewer annotate add-db --db app.db --tab query \\
36633
37419
  };
36634
37420
  }
36635
37421
 
36636
- // web-src/core/fuzzy-search.ts
36637
- function basenameStart(path) {
36638
- const slash = path.lastIndexOf("/");
36639
- return slash < 0 ? 0 : slash + 1;
36640
- }
36641
- function isBoundary(path, index) {
36642
- if (index <= 0) return true;
36643
- const prev = path[index - 1];
36644
- return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
36645
- }
36646
- function toRanges(indices) {
36647
- const ranges = [];
36648
- for (const index of indices) {
36649
- const last = ranges[ranges.length - 1];
36650
- if (last && last.end === index) {
36651
- last.end = index + 1;
36652
- } else {
36653
- ranges.push({ start: index, end: index + 1 });
36654
- }
36655
- }
36656
- return ranges;
36657
- }
36658
- function basenameMatchTier(loweredQuery, loweredBasename) {
36659
- if (loweredBasename === loweredQuery) return 4;
36660
- if (loweredBasename.startsWith(`${loweredQuery}.`)) return 3;
36661
- if (loweredBasename.startsWith(loweredQuery)) return 2;
36662
- if (loweredBasename.includes(loweredQuery)) return 1;
36663
- return 0;
36664
- }
36665
- function pathMatchTier(loweredQuery, loweredPath, loweredBasename) {
36666
- if (loweredQuery.includes("/") && (loweredPath === loweredQuery || loweredPath.endsWith(`/${loweredQuery}`)))
36667
- return 4;
36668
- return basenameMatchTier(loweredQuery, loweredBasename);
36669
- }
36670
- function contiguousPathRange(loweredQuery, loweredPath, baseStart) {
36671
- const loweredBasename = loweredPath.slice(baseStart);
36672
- const basenameMatchStart = loweredBasename.indexOf(loweredQuery);
36673
- if (basenameMatchStart >= 0) {
36674
- const start = baseStart + basenameMatchStart;
36675
- return { start, end: start + loweredQuery.length };
36676
- }
36677
- if (loweredQuery.includes("/")) {
36678
- const pathMatchStart = loweredPath.endsWith(`/${loweredQuery}`) ? loweredPath.length - loweredQuery.length : loweredPath === loweredQuery ? 0 : -1;
36679
- if (pathMatchStart >= 0)
36680
- return {
36681
- start: pathMatchStart,
36682
- end: pathMatchStart + loweredQuery.length
36683
- };
36684
- }
36685
- return null;
36686
- }
36687
- function computeFuzzyMatch(query, path) {
36688
- const q = query.trim().toLowerCase();
36689
- if (!q) return { score: 0, ranges: [], tier: 0 };
36690
- const lowerPath = path.toLowerCase();
36691
- const baseStart = basenameStart(path);
36692
- const indices = [];
36693
- let from = 0;
36694
- let score = 0;
36695
- for (const ch of q) {
36696
- const index = lowerPath.indexOf(ch, from);
36697
- if (index < 0) return null;
36698
- indices.push(index);
36699
- score += 10;
36700
- if (index >= baseStart) score += 8;
36701
- if (isBoundary(path, index)) score += 6;
36702
- const prev = indices[indices.length - 2];
36703
- if (prev != null && prev + 1 === index) score += 12;
36704
- from = index + 1;
36705
- }
36706
- const first = indices[0];
36707
- score -= Math.min(first, 40);
36708
- if (indices[0] >= baseStart) score += 20;
36709
- const basename = lowerPath.slice(baseStart);
36710
- const tier = pathMatchTier(q, lowerPath, basename);
36711
- const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
36712
- return {
36713
- score,
36714
- ranges: contiguousRange ? [contiguousRange] : toRanges(indices),
36715
- tier
36716
- };
36717
- }
36718
- function fuzzyMatchPath(query, path) {
36719
- const match2 = computeFuzzyMatch(query, path);
36720
- return match2 ? { score: match2.score, ranges: match2.ranges } : null;
36721
- }
36722
- function rankFuzzyPaths(query, items, limit) {
36723
- const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
36724
- const compare = (a2, b2) => b2.tier - a2.tier || b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
36725
- if (!bounded) {
36726
- return items.map((item) => {
36727
- const match2 = computeFuzzyMatch(query, item.path);
36728
- return match2 ? { item, score: match2.score, ranges: match2.ranges, tier: match2.tier } : null;
36729
- }).filter(
36730
- (item) => item !== null
36731
- ).sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
36732
- }
36733
- const top = [];
36734
- for (const item of items) {
36735
- const match2 = computeFuzzyMatch(query, item.path);
36736
- if (!match2) continue;
36737
- const ranked = {
36738
- item,
36739
- score: match2.score,
36740
- ranges: match2.ranges,
36741
- tier: match2.tier
36742
- };
36743
- pushBoundedTop(top, ranked, bounded, compare);
36744
- }
36745
- return top.sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
36746
- }
36747
- function pushBoundedTop(heap, item, limit, compareBestFirst) {
36748
- const isWorse = (a2, b2) => compareBestFirst(a2, b2) > 0;
36749
- const siftUp = (index) => {
36750
- while (index > 0) {
36751
- const parent = Math.floor((index - 1) / 2);
36752
- if (!isWorse(heap[index], heap[parent])) break;
36753
- [heap[index], heap[parent]] = [heap[parent], heap[index]];
36754
- index = parent;
36755
- }
36756
- };
36757
- const siftDown = (index) => {
36758
- while (true) {
36759
- const left = index * 2 + 1;
36760
- const right = left + 1;
36761
- let worst = index;
36762
- if (left < heap.length && isWorse(heap[left], heap[worst])) worst = left;
36763
- if (right < heap.length && isWorse(heap[right], heap[worst]))
36764
- worst = right;
36765
- if (worst === index) break;
36766
- [heap[index], heap[worst]] = [heap[worst], heap[index]];
36767
- index = worst;
36768
- }
36769
- };
36770
- if (heap.length < limit) {
36771
- heap.push(item);
36772
- siftUp(heap.length - 1);
36773
- return;
36774
- }
36775
- if (compareBestFirst(item, heap[0]) >= 0) return;
36776
- heap[0] = item;
36777
- siftDown(0);
36778
- }
36779
- function rankGlobPathMatches(query, items, limit) {
36780
- const matchPath = createGlobPathMatcher(query);
36781
- if (!matchPath) return [];
36782
- const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
36783
- const compare = (a2, b2) => b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
36784
- if (!bounded) {
36785
- return items.map((item) => {
36786
- const match2 = matchPath(item.path);
36787
- return match2 ? {
36788
- item,
36789
- score: match2.score,
36790
- ranges: match2.ranges,
36791
- mode: "glob"
36792
- } : null;
36793
- }).filter((item) => item !== null).sort(compare);
36794
- }
36795
- const top = [];
36796
- for (const item of items) {
36797
- const match2 = matchPath(item.path);
36798
- if (!match2) continue;
36799
- const ranked = {
36800
- item,
36801
- score: match2.score,
36802
- ranges: match2.ranges,
36803
- mode: "glob"
36804
- };
36805
- pushBoundedTop(top, ranked, bounded, compare);
36806
- }
36807
- return top.sort(compare);
36808
- }
36809
- function rankPathMatches(query, items, limit) {
36810
- if (isGlobPathQuery(query)) {
36811
- return rankGlobPathMatches(query, items, limit);
36812
- }
36813
- return rankFuzzyPaths(query, items, limit).map((item) => ({
36814
- ...item,
36815
- mode: "fuzzy"
36816
- }));
36817
- }
36818
- function isGlobPathQuery(query) {
36819
- return /[*?]/.test(query.trim());
36820
- }
36821
- function escapeRegexChar(ch) {
36822
- return /[\\^$+?.()|{}]/.test(ch) ? `\\${ch}` : ch;
36823
- }
36824
- function globToRegExp(query) {
36825
- const pattern = query.trim();
36826
- if (!pattern) return null;
36827
- let source = "^";
36828
- for (let i2 = 0; i2 < pattern.length; i2++) {
36829
- const ch = pattern[i2];
36830
- if (ch === "*") {
36831
- if (pattern[i2 + 1] === "*") {
36832
- source += ".*";
36833
- i2++;
36834
- } else {
36835
- source += "[^/]*";
36836
- }
36837
- } else if (ch === "?") {
36838
- source += "[^/]";
36839
- } else if (ch === "[") {
36840
- const close = pattern.indexOf("]", i2 + 1);
36841
- if (close < 0) {
36842
- source += "\\[";
36843
- } else {
36844
- const body = pattern.slice(i2 + 1, close).replace(/\\/g, "\\\\");
36845
- source += `[${body}]`;
36846
- i2 = close;
36847
- }
36848
- } else {
36849
- source += escapeRegexChar(ch);
36850
- }
36851
- }
36852
- source += "$";
36853
- try {
36854
- return new RegExp(source, "i");
36855
- } catch {
36856
- return null;
36857
- }
36858
- }
36859
- function globMatchPath(query, path) {
36860
- return createGlobPathMatcher(query)?.(path) ?? null;
36861
- }
36862
- function createGlobPathMatcher(query) {
36863
- const regex = globToRegExp(query);
36864
- if (!regex) return null;
36865
- const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
36866
- const suffix = query.replace(/^\*+/, "").toLowerCase();
36867
- return (path) => {
36868
- const baseStart = basenameStart(path);
36869
- const basename = path.slice(baseStart);
36870
- if (!regex.test(path) && (query.includes("/") || !regex.test(basename)))
36871
- return null;
36872
- const ranges = [];
36873
- const lowerPath = path.toLowerCase();
36874
- for (const part of literal) {
36875
- const start = lowerPath.indexOf(part.toLowerCase());
36876
- if (start >= 0) ranges.push({ start, end: start + part.length });
36877
- }
36878
- ranges.sort((a2, b2) => a2.start - b2.start || a2.end - b2.end);
36879
- const mergedRanges = [];
36880
- for (const range of ranges) {
36881
- const last = mergedRanges[mergedRanges.length - 1];
36882
- if (last && last.end >= range.start) {
36883
- last.end = Math.max(last.end, range.end);
36884
- } else {
36885
- mergedRanges.push({ ...range });
36886
- }
36887
- }
36888
- const score = 1e3 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(suffix) ? 50 : 0);
36889
- return { score, ranges: mergedRanges };
36890
- };
36891
- }
36892
-
36893
- // web-src/core/search-palette.ts
36894
- var PALETTE_RESULT_LIMIT = 50;
36895
- var GREP_SELECTION_HISTORY_LIMIT = 100;
36896
- var MIN_GREP_PALETTE_WIDTH = 720;
36897
- var MAX_GREP_PALETTE_WIDTH = 2400;
36898
- var MIN_GREP_PALETTE_HEIGHT = 420;
36899
- var MAX_GREP_PALETTE_HEIGHT = 1400;
36900
- function limitPaletteResults(items) {
36901
- return items.slice(0, PALETTE_RESULT_LIMIT);
36902
- }
36903
- function movePaletteSelection(index, count, direction) {
36904
- if (count <= 0) return -1;
36905
- if (index < 0) return direction > 0 ? 0 : count - 1;
36906
- return Math.max(0, Math.min(count - 1, index + direction));
36907
- }
36908
- function rememberPaletteSelection(history2, path) {
36909
- const next = history2.filter((entry) => entry !== path);
36910
- next.push(path);
36911
- return next.slice(-GREP_SELECTION_HISTORY_LIMIT);
36912
- }
36913
- function rankPaletteResultsByHistory(items, history2) {
36914
- const priority = new Map(history2.map((path, index) => [path, index + 1]));
36915
- return items.map((item, index) => ({ item, index })).sort(
36916
- (a2, b2) => (priority.get(b2.item.path) ?? 0) - (priority.get(a2.item.path) ?? 0) || a2.index - b2.index
36917
- ).map(({ item }) => item);
36918
- }
36919
-
36920
37422
  // web-src/views/search-palette-i18n.ts
36921
37423
  var EN2 = {
36922
37424
  files: "Files",
36923
37425
  grep: "Grep",
37426
+ switchToFiles: "Switch to file search (Ctrl+K)",
37427
+ switchToGrep: "Switch to text search (Ctrl+G)",
36924
37428
  searchFiles: "Search files",
36925
37429
  searchText: "Search text",
36926
37430
  fileCodePreview: "File code preview",
@@ -36931,14 +37435,20 @@ code-viewer annotate add-db --db app.db --tab query \\
36931
37435
  fuzzyHint: "Fuzzy path search",
36932
37436
  plain: "Plain",
36933
37437
  regex: ".* Regex",
37438
+ matchCase: "Aa",
37439
+ matchCaseTitle: "Match case (Alt+C)",
37440
+ wholeWord: "Word",
37441
+ wholeWordTitle: "Match whole words only (Alt+W)",
36934
37442
  excludeTests: "No test",
36935
37443
  excludeTestsTitle: "Exclude test/spec files",
36936
37444
  groupFiles: "Group files",
36937
37445
  groupFilesTitle: "Group matching lines by file",
36938
- regexHint: "Alt+R regex",
37446
+ grepHint: "path:<dir or glob> narrows · Alt+R regex",
36939
37447
  windowWidth: "window width",
36940
37448
  windowHeight: "window height",
36941
37449
  regexMode: "regex mode",
37450
+ caseSensitivity: "case sensitivity",
37451
+ wordMatching: "whole-word matching",
36942
37452
  fileGrouping: "file grouping",
36943
37453
  testExclusion: "test exclusion",
36944
37454
  saveFailed: (label, error2) => `Failed to save ${label}: ${error2}`,
@@ -36952,22 +37462,42 @@ code-viewer annotate add-db --db app.db --tab query \\
36952
37462
  line: (line, column) => `Line ${line}:${column}`,
36953
37463
  diffFiles: (count) => `${count} diff files`,
36954
37464
  typeToSearchFiles: "Type to search repository files",
37465
+ recentFiles: (count) => `Recent files - ${count}`,
36955
37466
  loadingFiles: "Loading files...",
36956
- results: (count) => `${count} results`,
37467
+ results: (count, total, candidatesTruncated) => (total === void 0 ? `${count} results` : `${count} of ${total} results`) + (candidatesTruncated ? " · file list truncated" : ""),
36957
37468
  noResults: "No results",
36958
37469
  typeToGrep: "Type to grep",
36959
37470
  invalidRegex: "Invalid regular expression",
36960
37471
  searching: "Searching...",
36961
37472
  repositoryChanged: "Repository changed; search again",
36962
- grepSummary: ({ engine, regex, testsExcluded, truncated, count }) => engine + (regex ? " regex" : " plain") + (testsExcluded ? " · tests excluded" : "") + (truncated ? " truncated" : "") + ` - ${count} results`,
37473
+ grepSummary: ({
37474
+ engine,
37475
+ regex,
37476
+ caseSensitive,
37477
+ wholeWord,
37478
+ testsExcluded,
37479
+ truncated,
37480
+ count,
37481
+ paths
37482
+ }) => engine + (regex ? " regex" : " plain") + (caseSensitive ? " · match case" : "") + (wholeWord ? " · whole word" : "") + (testsExcluded ? " · tests excluded" : "") + (paths.length ? ` · in ${paths.join(" ")}` : "") + (truncated ? " truncated" : "") + ` - ${count} results`,
36963
37483
  searchFailed: (error2) => `Search failed: ${error2}`,
36964
37484
  savingSelection: "Saving selection...",
36965
37485
  selectionSaveFailed: (error2) => `Failed to save selection: ${error2}`,
36966
- unknownError: "unknown error"
37486
+ unknownError: "unknown error",
37487
+ pinResults: "Pin",
37488
+ pinResultsTitle: "Keep these results open in the bottom panel while you browse (Ctrl+Enter)",
37489
+ resultsTitle: "Search",
37490
+ resultsOpen: "Open the search results panel",
37491
+ resultsRun: "Search",
37492
+ resultsPlaceholder: "Search text (path:<dir or glob> narrows)",
37493
+ resultsIdle: "Type a search and press Enter",
37494
+ resultsScope: (ref) => `in ${ref}`
36967
37495
  };
36968
37496
  var JA2 = {
36969
37497
  files: "ファイル",
36970
37498
  grep: "GREP",
37499
+ switchToFiles: "ファイル名検索に切り替え (Ctrl+K)",
37500
+ switchToGrep: "コード検索に切り替え (Ctrl+G)",
36971
37501
  searchFiles: "ファイルを検索",
36972
37502
  searchText: "コードを検索",
36973
37503
  fileCodePreview: "ファイルのコード表示",
@@ -36978,14 +37508,20 @@ code-viewer annotate add-db --db app.db --tab query \\
36978
37508
  fuzzyHint: "パスのあいまい検索",
36979
37509
  plain: "通常",
36980
37510
  regex: ".* 正規表現",
37511
+ matchCase: "Aa",
37512
+ matchCaseTitle: "大文字と小文字を区別 (Alt+C)",
37513
+ wholeWord: "単語",
37514
+ wholeWordTitle: "単語単位で一致 (Alt+W)",
36981
37515
  excludeTests: "テスト除外",
36982
37516
  excludeTestsTitle: "test/spec ファイルを除外",
36983
37517
  groupFiles: "ファイル別",
36984
37518
  groupFilesTitle: "一致した行をファイル別に表示",
36985
- regexHint: "Alt+R 正規表現",
37519
+ grepHint: "path:<ディレクトリ or glob> で絞り込み · Alt+R 正規表現",
36986
37520
  windowWidth: "ウィンドウの幅",
36987
37521
  windowHeight: "ウィンドウの高さ",
36988
37522
  regexMode: "正規表現モード",
37523
+ caseSensitivity: "大文字小文字の区別",
37524
+ wordMatching: "単語単位の一致",
36989
37525
  fileGrouping: "ファイル別表示",
36990
37526
  testExclusion: "テスト除外",
36991
37527
  saveFailed: (label, error2) => `${label}を保存できませんでした: ${error2}`,
@@ -36999,23 +37535,92 @@ code-viewer annotate add-db --db app.db --tab query \\
36999
37535
  line: (line, column) => `${line} 行:${column}`,
37000
37536
  diffFiles: (count) => `差分ファイル ${count} 件`,
37001
37537
  typeToSearchFiles: "リポジトリ内のファイル名を入力してください",
37538
+ recentFiles: (count) => `最近開いたファイル - ${count} 件`,
37002
37539
  loadingFiles: "ファイルを読み込み中...",
37003
- results: (count) => `${count} 件`,
37540
+ results: (count, total, candidatesTruncated) => (total === void 0 ? `${count} 件` : `${total} 件中 ${count} 件`) + (candidatesTruncated ? "・ファイル一覧は上限で打ち切り" : ""),
37004
37541
  noResults: "該当なし",
37005
37542
  typeToGrep: "検索するコードを入力してください",
37006
37543
  invalidRegex: "正規表現が正しくありません",
37007
37544
  searching: "検索中...",
37008
37545
  repositoryChanged: "リポジトリが変更されました。もう一度検索してください",
37009
- grepSummary: ({ engine, regex, testsExcluded, truncated, count }) => engine + (regex ? "・正規表現" : "・通常") + (testsExcluded ? "・テスト除外" : "") + (truncated ? "・一部表示" : "") + ` - ${count} 件`,
37546
+ grepSummary: ({
37547
+ engine,
37548
+ regex,
37549
+ caseSensitive,
37550
+ wholeWord,
37551
+ testsExcluded,
37552
+ truncated,
37553
+ count,
37554
+ paths
37555
+ }) => engine + (regex ? "・正規表現" : "・通常") + (caseSensitive ? "・大文字小文字を区別" : "") + (wholeWord ? "・単語単位" : "") + (testsExcluded ? "・テスト除外" : "") + (paths.length ? `・${paths.join(" ")} 内` : "") + (truncated ? "・一部表示" : "") + ` - ${count} 件`,
37010
37556
  searchFailed: (error2) => `検索に失敗しました: ${error2}`,
37011
37557
  savingSelection: "選択履歴を保存中...",
37012
37558
  selectionSaveFailed: (error2) => `選択履歴を保存できませんでした: ${error2}`,
37013
- unknownError: "不明なエラー"
37559
+ unknownError: "不明なエラー",
37560
+ pinResults: "固定",
37561
+ pinResultsTitle: "この結果を下パネルに出したまま閲覧を続ける (Ctrl+Enter)",
37562
+ resultsTitle: "検索",
37563
+ resultsOpen: "検索結果パネルを開く",
37564
+ resultsRun: "検索",
37565
+ resultsPlaceholder: "検索するコード(path:<ディレクトリ or glob> で絞り込み)",
37566
+ resultsIdle: "検索語を入力して Enter",
37567
+ resultsScope: (ref) => `${ref} 内`
37014
37568
  };
37015
37569
  function searchPaletteText(language) {
37016
37570
  return language === "ja" ? JA2 : EN2;
37017
37571
  }
37018
37572
 
37573
+ // web-src/views/text-mark.ts
37574
+ function findTextRange(lineText, needle, options = {}) {
37575
+ if (!needle) return null;
37576
+ const haystack = options.caseSensitive ? lineText : lineText.toLowerCase();
37577
+ const target = options.caseSensitive ? needle : needle.toLowerCase();
37578
+ const expectedStart = Math.max(0, (options.nearColumn ?? 1) - 1);
37579
+ let bestStart = -1;
37580
+ let cursor = haystack.indexOf(target);
37581
+ while (cursor >= 0) {
37582
+ if (bestStart < 0 || Math.abs(cursor - expectedStart) < Math.abs(bestStart - expectedStart))
37583
+ bestStart = cursor;
37584
+ cursor = haystack.indexOf(target, cursor + Math.max(1, target.length));
37585
+ }
37586
+ return bestStart < 0 ? null : { start: bestStart, end: bestStart + needle.length };
37587
+ }
37588
+ function markTextRange(cell, start, end, className) {
37589
+ if (end <= start) return;
37590
+ const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
37591
+ const parts = [];
37592
+ let offset = 0;
37593
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
37594
+ const textNode = node;
37595
+ const nextOffset = offset + textNode.data.length;
37596
+ const partStart = Math.max(start, offset);
37597
+ const partEnd = Math.min(end, nextOffset);
37598
+ if (partStart < partEnd)
37599
+ parts.push({
37600
+ node: textNode,
37601
+ start: partStart - offset,
37602
+ end: partEnd - offset
37603
+ });
37604
+ offset = nextOffset;
37605
+ }
37606
+ for (const part of parts.reverse()) {
37607
+ const selected = part.node.splitText(part.start);
37608
+ selected.splitText(part.end - part.start);
37609
+ const mark = document.createElement("mark");
37610
+ mark.className = className;
37611
+ const parent = selected.parentNode;
37612
+ if (!parent) throw new Error("marked text is detached");
37613
+ parent.insertBefore(mark, selected);
37614
+ mark.appendChild(selected);
37615
+ }
37616
+ }
37617
+ function markNeedleInCell(cell, lineText, needle, className, options = {}) {
37618
+ const range = findTextRange(lineText, needle, options);
37619
+ if (!range) return false;
37620
+ markTextRange(cell, range.start, range.end, className);
37621
+ return true;
37622
+ }
37623
+
37019
37624
  // web-src/views/search-palette-ui.ts
37020
37625
  function createSearchPalette(deps) {
37021
37626
  const {
@@ -37039,14 +37644,18 @@ code-viewer annotate add-db --db app.db --tab query \\
37039
37644
  getFileSelectionHistory,
37040
37645
  getGrepSelectionHistory,
37041
37646
  getGrepRegex,
37647
+ getGrepCaseSensitive,
37648
+ getGrepWholeWord,
37042
37649
  getGrepHideTests,
37043
37650
  getGrepGroupByFile,
37044
37651
  getGrepPaletteWidth,
37045
37652
  getGrepPaletteHeight,
37046
37653
  persistGrepSettings,
37047
- applyGrepHideTests
37654
+ applyGrepHideTests,
37655
+ openSearchResults
37048
37656
  } = deps;
37049
37657
  let PALETTE = null;
37658
+ const LAST_QUERY = { file: "", grep: "" };
37050
37659
  let repoFileRequestGeneration = 0;
37051
37660
  const GREP_CONTEXT_RADIUS = 5;
37052
37661
  const FILE_PREVIEW_LINE_LIMIT = 200;
@@ -37083,6 +37692,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37083
37692
  }
37084
37693
  function closeSearchPalette() {
37085
37694
  if (!PALETTE) return;
37695
+ LAST_QUERY[PALETTE.mode] = PALETTE.input.value;
37086
37696
  const previousFocusScope = PALETTE.previousFocusScope;
37087
37697
  PALETTE.controller?.abort();
37088
37698
  PALETTE.previewController?.abort();
@@ -37094,6 +37704,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37094
37704
  }
37095
37705
  function createPalette(mode) {
37096
37706
  const previousFocusScope = PALETTE ? PALETTE.previousFocusScope : getPanelFocusScope();
37707
+ const initialQuery = PALETTE ? PALETTE.input.value : LAST_QUERY[mode];
37097
37708
  closeSearchPalette();
37098
37709
  const root = document.createElement("div");
37099
37710
  root.className = "gdp-palette-backdrop";
@@ -37112,7 +37723,19 @@ code-viewer annotate add-db --db app.db --tab query \\
37112
37723
  dialog.style.height = `${clampGrepPaletteHeight(savedHeight)}px`;
37113
37724
  const label = document.createElement("div");
37114
37725
  label.className = "gdp-palette-label";
37115
- label.textContent = mode === "file" ? text3().files : text3().grep;
37726
+ for (const switchMode of ["file", "grep"]) {
37727
+ const button = document.createElement("button");
37728
+ button.type = "button";
37729
+ button.className = "gdp-palette-mode-button gdp-palette-mode-switch";
37730
+ button.setAttribute("aria-pressed", String(switchMode === mode));
37731
+ button.textContent = switchMode === "file" ? text3().files : text3().grep;
37732
+ button.title = switchMode === "file" ? text3().switchToFiles : text3().switchToGrep;
37733
+ button.addEventListener("mousedown", (event) => {
37734
+ event.preventDefault();
37735
+ if (switchMode !== mode) createPalette(switchMode);
37736
+ });
37737
+ label.appendChild(button);
37738
+ }
37116
37739
  const input2 = document.createElement("input");
37117
37740
  input2.className = "gdp-palette-input";
37118
37741
  input2.type = "search";
@@ -37122,6 +37745,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37122
37745
  input2.setAttribute("role", "combobox");
37123
37746
  input2.setAttribute("aria-expanded", "true");
37124
37747
  input2.setAttribute("aria-controls", "gdp-palette-list");
37748
+ input2.value = initialQuery;
37125
37749
  const status = document.createElement("div");
37126
37750
  status.className = "gdp-palette-status";
37127
37751
  const controls = document.createElement("div");
@@ -37165,6 +37789,8 @@ code-viewer annotate add-db --db app.db --tab query \\
37165
37789
  preview,
37166
37790
  mode,
37167
37791
  grepRegex: getGrepRegex(),
37792
+ grepCaseSensitive: getGrepCaseSensitive(),
37793
+ grepWholeWord: getGrepWholeWord(),
37168
37794
  grepHideTests: getGrepHideTests(),
37169
37795
  grepGroupByFile: getGrepGroupByFile(),
37170
37796
  grepPaletteWidth: clampGrepPaletteWidth(savedWidth ?? dialogRect.width),
@@ -37236,6 +37862,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37236
37862
  input2.addEventListener("input", () => updatePaletteResults(state));
37237
37863
  input2.addEventListener("keydown", (e2) => handlePaletteKeydown(e2, state));
37238
37864
  input2.focus();
37865
+ if (input2.value) input2.select();
37239
37866
  updatePaletteResults(state);
37240
37867
  return state;
37241
37868
  }
@@ -37283,6 +37910,38 @@ code-viewer annotate add-db --db app.db --tab query \\
37283
37910
  e2.preventDefault();
37284
37911
  void updateGrepRegex(state, true);
37285
37912
  });
37913
+ const matchCase = document.createElement("button");
37914
+ matchCase.type = "button";
37915
+ matchCase.className = "gdp-palette-mode-button";
37916
+ matchCase.setAttribute("aria-pressed", String(state.grepCaseSensitive));
37917
+ matchCase.textContent = text3().matchCase;
37918
+ matchCase.title = text3().matchCaseTitle;
37919
+ matchCase.disabled = state.settingsPending;
37920
+ matchCase.addEventListener("mousedown", (e2) => {
37921
+ e2.preventDefault();
37922
+ void updateGrepFlag(
37923
+ state,
37924
+ "grepCaseSensitive",
37925
+ !state.grepCaseSensitive,
37926
+ text3().caseSensitivity
37927
+ );
37928
+ });
37929
+ const wholeWord = document.createElement("button");
37930
+ wholeWord.type = "button";
37931
+ wholeWord.className = "gdp-palette-mode-button";
37932
+ wholeWord.setAttribute("aria-pressed", String(state.grepWholeWord));
37933
+ wholeWord.textContent = text3().wholeWord;
37934
+ wholeWord.title = text3().wholeWordTitle;
37935
+ wholeWord.disabled = state.settingsPending;
37936
+ wholeWord.addEventListener("mousedown", (e2) => {
37937
+ e2.preventDefault();
37938
+ void updateGrepFlag(
37939
+ state,
37940
+ "grepWholeWord",
37941
+ !state.grepWholeWord,
37942
+ text3().wordMatching
37943
+ );
37944
+ });
37286
37945
  const excludeTests = createExcludeTestsButton(state);
37287
37946
  const groupFiles = document.createElement("button");
37288
37947
  groupFiles.type = "button";
@@ -37297,8 +37956,62 @@ code-viewer annotate add-db --db app.db --tab query \\
37297
37956
  });
37298
37957
  const hint = document.createElement("span");
37299
37958
  hint.className = "gdp-palette-mode-hint";
37300
- hint.textContent = text3().regexHint;
37301
- state.controls.append(plain, regex, excludeTests, groupFiles, hint);
37959
+ hint.textContent = text3().grepHint;
37960
+ state.controls.append(
37961
+ plain,
37962
+ regex,
37963
+ matchCase,
37964
+ wholeWord,
37965
+ excludeTests,
37966
+ groupFiles
37967
+ );
37968
+ if (openSearchResults) {
37969
+ const pin = document.createElement("button");
37970
+ pin.type = "button";
37971
+ pin.className = "gdp-palette-mode-button gdp-palette-pin";
37972
+ pin.textContent = text3().pinResults;
37973
+ pin.title = text3().pinResultsTitle;
37974
+ pin.addEventListener("mousedown", (e2) => {
37975
+ e2.preventDefault();
37976
+ pinResults(state);
37977
+ });
37978
+ state.controls.append(pin);
37979
+ }
37980
+ state.controls.append(hint);
37981
+ }
37982
+ function pinResults(state) {
37983
+ if (!openSearchResults) return;
37984
+ const query = state.input.value;
37985
+ closeSearchPalette();
37986
+ openSearchResults(query);
37987
+ }
37988
+ async function updateGrepFlag(state, flag, value, label) {
37989
+ if (state.settingsPending || value === state[flag]) return;
37990
+ const previous = state[flag];
37991
+ state[flag] = value;
37992
+ state.settingsPending = true;
37993
+ renderPaletteControls(state);
37994
+ try {
37995
+ await persistGrepSettings(
37996
+ flag === "grepCaseSensitive" ? { grepCaseSensitive: value } : { grepWholeWord: value }
37997
+ );
37998
+ if (PALETTE === state) updatePaletteResults(state);
37999
+ } catch (err) {
38000
+ console.error(`Failed to save grep ${label}`, err);
38001
+ state[flag] = previous;
38002
+ if (PALETTE === state) {
38003
+ state.status.textContent = text3().saveFailed(
38004
+ label,
38005
+ errorMessage2(err, text3().unknownError)
38006
+ );
38007
+ }
38008
+ } finally {
38009
+ state.settingsPending = false;
38010
+ if (PALETTE === state) {
38011
+ renderPaletteControls(state);
38012
+ state.input.focus();
38013
+ }
38014
+ }
37302
38015
  }
37303
38016
  function errorMessage2(err, fallback) {
37304
38017
  return err instanceof Error && err.message ? err.message : fallback;
@@ -37423,51 +38136,16 @@ code-viewer annotate add-db --db app.db --tab query \\
37423
38136
  if (cursor < path.length)
37424
38137
  parent.appendChild(document.createTextNode(path.slice(cursor)));
37425
38138
  }
37426
- function grepMatchRange(lineText, item) {
37427
- const matchText = item.matchText || (item.regex ? "" : item.query);
37428
- if (!matchText) return null;
37429
- const caseSensitive = item.regex || /[A-Z]/.test(item.query);
37430
- const haystack = caseSensitive ? lineText : lineText.toLowerCase();
37431
- const needle = caseSensitive ? matchText : matchText.toLowerCase();
37432
- const expectedStart = Math.max(0, item.column - 1);
37433
- let bestStart = -1;
37434
- let cursor = haystack.indexOf(needle);
37435
- while (cursor >= 0) {
37436
- if (bestStart < 0 || Math.abs(cursor - expectedStart) < Math.abs(bestStart - expectedStart))
37437
- bestStart = cursor;
37438
- cursor = haystack.indexOf(needle, cursor + Math.max(1, needle.length));
37439
- }
37440
- return bestStart < 0 ? null : { start: bestStart, end: bestStart + matchText.length };
38139
+ function grepHighlightText(item) {
38140
+ return item.matchText || (item.regex ? "" : item.term);
37441
38141
  }
37442
38142
  function highlightGrepMatch(cell, lineText, item) {
37443
- const match2 = grepMatchRange(lineText, item);
37444
- if (!match2) return;
37445
- const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
37446
- const parts = [];
37447
- let offset = 0;
37448
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
37449
- const textNode = node;
37450
- const nextOffset = offset + textNode.data.length;
37451
- const start = Math.max(match2.start, offset);
37452
- const end = Math.min(match2.end, nextOffset);
37453
- if (start < end)
37454
- parts.push({
37455
- node: textNode,
37456
- start: start - offset,
37457
- end: end - offset
37458
- });
37459
- offset = nextOffset;
37460
- }
37461
- for (const part of parts.reverse()) {
37462
- const selected = part.node.splitText(part.start);
37463
- selected.splitText(part.end - part.start);
37464
- const mark = document.createElement("mark");
37465
- mark.className = "gdp-grep-match";
37466
- const parent = selected.parentNode;
37467
- if (!parent) throw new Error("grep match text is detached");
37468
- parent.insertBefore(mark, selected);
37469
- mark.appendChild(selected);
37470
- }
38143
+ const needle = grepHighlightText(item);
38144
+ if (!needle) return;
38145
+ markNeedleInCell(cell, lineText, needle, "gdp-grep-match", {
38146
+ caseSensitive: item.caseSensitive,
38147
+ nearColumn: item.column
38148
+ });
37471
38149
  }
37472
38150
  function palettePreviewTarget(item) {
37473
38151
  return item.kind === "file" ? {
@@ -37769,27 +38447,73 @@ code-viewer annotate add-db --db app.db --tab query \\
37769
38447
  ).sort(
37770
38448
  (a2, b2) => b2.match.score - a2.match.score || a2.file.path.localeCompare(b2.file.path)
37771
38449
  );
37772
- return limitPaletteResults(
37773
- rankPaletteResultsByHistory(
37774
- candidates.map((candidate) => ({
37775
- kind: "file",
37776
- path: candidate.file.path,
37777
- old_path: candidate.file.old_path,
37778
- displayPath: candidate.displayPath,
37779
- ref: paletteRef("diff"),
37780
- targetPath: fileSourceTarget(candidate.file).path,
37781
- targetRef: fileSourceTarget(candidate.file).ref,
37782
- source: "diff",
37783
- ranges: candidate.match.ranges
37784
- })),
37785
- state.fileHistory
38450
+ return {
38451
+ total: candidates.length,
38452
+ items: limitPaletteResults(
38453
+ rankPaletteResultsByHistory(
38454
+ candidates.map((candidate) => ({
38455
+ kind: "file",
38456
+ path: candidate.file.path,
38457
+ old_path: candidate.file.old_path,
38458
+ displayPath: candidate.displayPath,
38459
+ ref: paletteRef("diff"),
38460
+ targetPath: fileSourceTarget(candidate.file).path,
38461
+ targetRef: fileSourceTarget(candidate.file).ref,
38462
+ source: "diff",
38463
+ ranges: candidate.match.ranges
38464
+ })),
38465
+ state.fileHistory
38466
+ )
37786
38467
  )
38468
+ };
38469
+ }
38470
+ async function updateRecentFilePalette(state, source) {
38471
+ const recent = [...state.fileHistory].reverse();
38472
+ if (recent.length === 0) {
38473
+ state.items = [];
38474
+ state.selected = -1;
38475
+ state.status.textContent = text3().typeToSearchFiles;
38476
+ renderPalette(state);
38477
+ return;
38478
+ }
38479
+ state.status.textContent = text3().loadingFiles;
38480
+ const ref = paletteRef(source);
38481
+ const requestGeneration = ++repoFileRequestGeneration;
38482
+ let response;
38483
+ try {
38484
+ response = await repoPaletteFiles(ref);
38485
+ } catch (err) {
38486
+ if (requestGeneration !== repoFileRequestGeneration) return;
38487
+ throw err;
38488
+ }
38489
+ if (PALETTE !== state || state.input.value.trim() !== "" || requestGeneration !== repoFileRequestGeneration)
38490
+ return;
38491
+ REPO_FILE_CACHE.set(repoFileCacheKey(ref), response);
38492
+ const existing = new Set(response.files.map((file) => file.path));
38493
+ state.items = limitPaletteResults(
38494
+ recent.filter(
38495
+ (path) => existing.has(path) && (!state.grepHideTests || !isTestFilePath(path))
38496
+ ).map((path) => ({
38497
+ kind: "file",
38498
+ path,
38499
+ displayPath: path,
38500
+ ref,
38501
+ source,
38502
+ ranges: []
38503
+ }))
37787
38504
  );
38505
+ state.selected = state.items.length ? 0 : -1;
38506
+ state.status.textContent = state.items.length ? text3().recentFiles(state.items.length) : text3().typeToSearchFiles;
38507
+ renderPalette(state);
37788
38508
  }
37789
38509
  async function updateFilePalette(state, query) {
37790
38510
  renderPaletteControls(state);
37791
38511
  const source = paletteSource();
37792
38512
  if (!query.trim()) {
38513
+ if (source === "repo") {
38514
+ await updateRecentFilePalette(state, source);
38515
+ return;
38516
+ }
37793
38517
  const base2 = source === "diff" ? state.diffSnapshot.filter(
37794
38518
  (file) => !state.grepHideTests || !isTestFilePath(file.path)
37795
38519
  ).map((file) => {
@@ -37814,8 +38538,12 @@ code-viewer annotate add-db --db app.db --tab query \\
37814
38538
  renderPalette(state);
37815
38539
  return;
37816
38540
  }
38541
+ let totalMatches = 0;
38542
+ let candidatesTruncated = false;
37817
38543
  if (source === "diff") {
37818
- state.items = diffFilePaletteItems(state, query);
38544
+ const ranked = diffFilePaletteItems(state, query);
38545
+ state.items = ranked.items;
38546
+ totalMatches = ranked.total;
37819
38547
  } else {
37820
38548
  state.status.textContent = text3().loadingFiles;
37821
38549
  const ref = paletteRef(source);
@@ -37833,9 +38561,10 @@ code-viewer annotate add-db --db app.db --tab query \\
37833
38561
  const visibleFiles = response.files.filter(
37834
38562
  (file) => !state.grepHideTests || !isTestFilePath(file.path)
37835
38563
  );
38564
+ const stats = { total: 0 };
37836
38565
  state.items = limitPaletteResults(
37837
38566
  rankPaletteResultsByHistory(
37838
- rankPathMatches(query, visibleFiles, PALETTE_RESULT_LIMIT).map(
38567
+ rankPathMatches(query, visibleFiles, PALETTE_RESULT_LIMIT, stats).map(
37839
38568
  (match2) => ({
37840
38569
  kind: "file",
37841
38570
  path: match2.item.path,
@@ -37848,23 +38577,37 @@ code-viewer annotate add-db --db app.db --tab query \\
37848
38577
  state.fileHistory
37849
38578
  )
37850
38579
  );
38580
+ totalMatches = stats.total;
38581
+ candidatesTruncated = response.truncated;
37851
38582
  }
37852
38583
  state.selected = state.items.length ? 0 : -1;
37853
- state.status.textContent = state.items.length ? text3().results(state.items.length) : text3().noResults;
38584
+ state.status.textContent = state.items.length ? text3().results(
38585
+ state.items.length,
38586
+ totalMatches > state.items.length ? totalMatches : void 0,
38587
+ candidatesTruncated
38588
+ ) : text3().noResults;
37854
38589
  renderPalette(state);
37855
38590
  }
38591
+ function diffPathInScope(path, scopes) {
38592
+ if (scopes.length === 0) return true;
38593
+ return scopes.some(
38594
+ (scope) => isGlobPathQuery(scope) ? !!globMatchPath(scope, path) : path === scope || path.startsWith(`${scope.replace(/\/+$/, "")}/`)
38595
+ );
38596
+ }
37856
38597
  function updateGrepPalette(state, query) {
37857
38598
  renderPaletteControls(state);
37858
38599
  state.controller?.abort();
37859
38600
  if (state.debounce) window.clearTimeout(state.debounce);
37860
- if (!query.trim()) {
38601
+ const parsed = parseGrepQuery(query);
38602
+ const term = parsed.term;
38603
+ if (!term) {
37861
38604
  state.items = [];
37862
38605
  state.selected = -1;
37863
38606
  state.status.textContent = text3().typeToGrep;
37864
38607
  renderPalette(state);
37865
38608
  return;
37866
38609
  }
37867
- if (state.grepRegex && !regexQueryIsValid(query)) {
38610
+ if (state.grepRegex && !regexQueryIsValid(term)) {
37868
38611
  state.controller?.abort();
37869
38612
  state.items = [];
37870
38613
  state.selected = -1;
@@ -37880,16 +38623,30 @@ code-viewer annotate add-db --db app.db --tab query \\
37880
38623
  const source = paletteSource();
37881
38624
  const ref = paletteRef(source);
37882
38625
  const regex = state.grepRegex;
38626
+ const caseSensitive = state.grepCaseSensitive;
38627
+ const wholeWord = state.grepWholeWord;
37883
38628
  const hideTests = state.grepHideTests;
37884
- const params = new URLSearchParams();
37885
- params.set("ref", ref);
37886
- params.set("q", query);
37887
- params.set("max", "200");
37888
- if (regex) params.set("regex", "1");
37889
- if (hideTests) params.set("exclude_tests", "1");
38629
+ const params = buildGrepRequestParams({
38630
+ term,
38631
+ ref,
38632
+ regex,
38633
+ caseSensitive,
38634
+ wholeWord,
38635
+ hideTests,
38636
+ max: 200
38637
+ });
37890
38638
  appendScopeParams(params);
37891
38639
  if (source === "diff") {
37892
- for (const file of state.diffSnapshot) params.append("path", file.path);
38640
+ const scoped = state.diffSnapshot.filter(
38641
+ (file) => diffPathInScope(file.path, parsed.paths)
38642
+ );
38643
+ if (scoped.length === 0) {
38644
+ state.status.textContent = text3().noResults;
38645
+ return;
38646
+ }
38647
+ for (const file of scoped) params.append("path", file.path);
38648
+ } else {
38649
+ for (const scope of parsed.paths) params.append("path", scope);
37893
38650
  }
37894
38651
  const controller = new AbortController();
37895
38652
  state.controller = controller;
@@ -37904,7 +38661,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37904
38661
  return r2.json();
37905
38662
  })
37906
38663
  ).then((response) => {
37907
- if (PALETTE !== state || controller.signal.aborted || state.input.value !== query || state.grepRegex !== regex || state.grepHideTests !== hideTests)
38664
+ if (PALETTE !== state || controller.signal.aborted || state.input.value !== query || state.grepRegex !== regex || state.grepCaseSensitive !== caseSensitive || state.grepWholeWord !== wholeWord || state.grepHideTests !== hideTests)
37908
38665
  return;
37909
38666
  if (responseGenerationIsStale(response.generation)) {
37910
38667
  state.items = [];
@@ -37925,7 +38682,9 @@ code-viewer annotate add-db --db app.db --tab query \\
37925
38682
  preview: match2.preview,
37926
38683
  matchText: match2.matchText,
37927
38684
  query,
38685
+ term,
37928
38686
  regex,
38687
+ caseSensitive,
37929
38688
  ref,
37930
38689
  source
37931
38690
  }))
@@ -37934,9 +38693,12 @@ code-viewer annotate add-db --db app.db --tab query \\
37934
38693
  state.status.textContent = text3().grepSummary({
37935
38694
  engine: response.engine,
37936
38695
  regex,
38696
+ caseSensitive,
38697
+ wholeWord,
37937
38698
  testsExcluded: hideTests,
37938
38699
  truncated: response.truncated,
37939
- count: state.items.length
38700
+ count: state.items.length,
38701
+ paths: parsed.paths
37940
38702
  });
37941
38703
  renderPalette(state);
37942
38704
  }).catch((err) => {
@@ -38027,12 +38789,14 @@ code-viewer annotate add-db --db app.db --tab query \\
38027
38789
  });
38028
38790
  scrollToFile(item.path, item.line);
38029
38791
  } else {
38792
+ const hl = grepHighlightText(item);
38030
38793
  setRoute({
38031
38794
  screen: "file",
38032
38795
  path: item.path,
38033
38796
  ref: item.ref,
38034
38797
  view: "blob",
38035
38798
  line: item.line,
38799
+ ...hl ? { hl } : {},
38036
38800
  range: currentRange()
38037
38801
  });
38038
38802
  void renderStandaloneSource({ path: item.path, ref: item.ref });
@@ -38048,6 +38812,10 @@ code-viewer annotate add-db --db app.db --tab query \\
38048
38812
  if (e2.key === "Enter") {
38049
38813
  if (state.composing) return;
38050
38814
  e2.preventDefault();
38815
+ if (state.mode === "grep" && (e2.ctrlKey || e2.metaKey) && openSearchResults) {
38816
+ pinResults(state);
38817
+ return;
38818
+ }
38051
38819
  void selectPaletteItem(state);
38052
38820
  return;
38053
38821
  }
@@ -38056,6 +38824,26 @@ code-viewer annotate add-db --db app.db --tab query \\
38056
38824
  void updateGrepRegex(state, !state.grepRegex);
38057
38825
  return;
38058
38826
  }
38827
+ if (state.mode === "grep" && e2.altKey && e2.key.toLowerCase() === "c") {
38828
+ e2.preventDefault();
38829
+ void updateGrepFlag(
38830
+ state,
38831
+ "grepCaseSensitive",
38832
+ !state.grepCaseSensitive,
38833
+ text3().caseSensitivity
38834
+ );
38835
+ return;
38836
+ }
38837
+ if (state.mode === "grep" && e2.altKey && e2.key.toLowerCase() === "w") {
38838
+ e2.preventDefault();
38839
+ void updateGrepFlag(
38840
+ state,
38841
+ "grepWholeWord",
38842
+ !state.grepWholeWord,
38843
+ text3().wordMatching
38844
+ );
38845
+ return;
38846
+ }
38059
38847
  const direction = e2.key === "ArrowDown" || e2.ctrlKey && e2.key.toLowerCase() === "n" ? 1 : e2.key === "ArrowUp" || e2.ctrlKey && e2.key.toLowerCase() === "p" ? -1 : 0;
38060
38848
  if (direction) {
38061
38849
  e2.preventDefault();
@@ -38088,6 +38876,317 @@ code-viewer annotate add-db --db app.db --tab query \\
38088
38876
  };
38089
38877
  }
38090
38878
 
38879
+ // web-src/views/search-results-view.ts
38880
+ var RESULTS_MAX = 500;
38881
+ function createSearchResultsView(deps) {
38882
+ let mounted = false;
38883
+ let input2 = null;
38884
+ let runButton = null;
38885
+ let controls = null;
38886
+ let status = null;
38887
+ let list2 = null;
38888
+ let query = "";
38889
+ let controller = null;
38890
+ let generation = 0;
38891
+ let settingsPending = false;
38892
+ let lastResponse = null;
38893
+ let activeKey = "";
38894
+ const text3 = () => searchPaletteText(deps.getLanguage());
38895
+ function host() {
38896
+ return deps.$("#search-sheet");
38897
+ }
38898
+ function toggleButton(label, title, pressed, onToggle) {
38899
+ const button = document.createElement("button");
38900
+ button.type = "button";
38901
+ button.className = "gdp-palette-mode-button";
38902
+ button.textContent = label;
38903
+ button.title = title;
38904
+ button.setAttribute("aria-pressed", String(pressed));
38905
+ button.disabled = settingsPending;
38906
+ button.addEventListener("click", (event) => {
38907
+ event.preventDefault();
38908
+ onToggle();
38909
+ });
38910
+ return button;
38911
+ }
38912
+ async function updateFlag(patch) {
38913
+ if (settingsPending) return;
38914
+ settingsPending = true;
38915
+ renderControls();
38916
+ try {
38917
+ await deps.persistGrepSettings(patch);
38918
+ } catch (err) {
38919
+ console.error("Failed to save search option", err);
38920
+ if (status)
38921
+ status.textContent = text3().saveFailed(
38922
+ text3().regexMode,
38923
+ err instanceof Error && err.message ? err.message : text3().unknownError
38924
+ );
38925
+ } finally {
38926
+ settingsPending = false;
38927
+ renderControls();
38928
+ if (query) run();
38929
+ }
38930
+ }
38931
+ function renderControls() {
38932
+ if (!controls) return;
38933
+ const current = text3();
38934
+ controls.replaceChildren(
38935
+ toggleButton(
38936
+ current.plain,
38937
+ current.plain,
38938
+ !deps.getGrepRegex(),
38939
+ () => void updateFlag({ grepRegex: false })
38940
+ ),
38941
+ toggleButton(
38942
+ current.regex,
38943
+ "Alt+R",
38944
+ deps.getGrepRegex(),
38945
+ () => void updateFlag({ grepRegex: true })
38946
+ ),
38947
+ toggleButton(
38948
+ current.matchCase,
38949
+ current.matchCaseTitle,
38950
+ deps.getGrepCaseSensitive(),
38951
+ () => void updateFlag({ grepCaseSensitive: !deps.getGrepCaseSensitive() })
38952
+ ),
38953
+ toggleButton(
38954
+ current.wholeWord,
38955
+ current.wholeWordTitle,
38956
+ deps.getGrepWholeWord(),
38957
+ () => void updateFlag({ grepWholeWord: !deps.getGrepWholeWord() })
38958
+ ),
38959
+ toggleButton(
38960
+ current.excludeTests,
38961
+ current.excludeTestsTitle,
38962
+ deps.getGrepHideTests(),
38963
+ () => void updateFlag({ hideTests: !deps.getGrepHideTests() })
38964
+ )
38965
+ );
38966
+ }
38967
+ function mount() {
38968
+ const el2 = host();
38969
+ if (!el2 || mounted) return;
38970
+ mounted = true;
38971
+ el2.replaceChildren();
38972
+ const head = document.createElement("div");
38973
+ head.className = "search-results-head";
38974
+ input2 = document.createElement("input");
38975
+ input2.type = "search";
38976
+ input2.className = "search-results-input";
38977
+ input2.autocomplete = "off";
38978
+ input2.spellcheck = false;
38979
+ input2.addEventListener("keydown", (event) => {
38980
+ if (event.key === "Enter") {
38981
+ event.preventDefault();
38982
+ run(input2?.value ?? "");
38983
+ }
38984
+ });
38985
+ runButton = document.createElement("button");
38986
+ runButton.type = "button";
38987
+ runButton.className = "gdp-btn gdp-btn-sm search-results-run";
38988
+ runButton.addEventListener("click", () => run(input2?.value ?? ""));
38989
+ controls = document.createElement("div");
38990
+ controls.className = "gdp-palette-controls search-results-controls";
38991
+ status = document.createElement("div");
38992
+ status.className = "gdp-palette-status search-results-status";
38993
+ head.append(input2, runButton);
38994
+ list2 = document.createElement("div");
38995
+ list2.className = "gdp-palette-list search-results-list";
38996
+ list2.setAttribute("role", "listbox");
38997
+ el2.append(head, controls, status, list2);
38998
+ localize();
38999
+ }
39000
+ function renderResults() {
39001
+ if (!list2 || !status) return;
39002
+ list2.replaceChildren();
39003
+ if (!lastResponse) {
39004
+ status.textContent = text3().resultsIdle;
39005
+ return;
39006
+ }
39007
+ const { response, term } = lastResponse;
39008
+ const grouped = /* @__PURE__ */ new Map();
39009
+ for (const match2 of response.matches) {
39010
+ const bucket = grouped.get(match2.path) ?? [];
39011
+ bucket.push(match2);
39012
+ grouped.set(match2.path, bucket);
39013
+ }
39014
+ for (const [path, matches2] of grouped) {
39015
+ const group = document.createElement("section");
39016
+ group.className = "gdp-palette-file-group";
39017
+ group.setAttribute("role", "group");
39018
+ group.setAttribute("aria-label", path);
39019
+ const heading2 = document.createElement("div");
39020
+ heading2.className = "gdp-palette-file-heading";
39021
+ const name = document.createElement("span");
39022
+ name.className = "gdp-palette-file-heading-name";
39023
+ name.textContent = path;
39024
+ name.title = path;
39025
+ const count = document.createElement("span");
39026
+ count.className = "gdp-palette-file-heading-count";
39027
+ count.textContent = String(matches2.length);
39028
+ heading2.append(name, count);
39029
+ const rows = document.createElement("div");
39030
+ rows.className = "gdp-palette-file-matches";
39031
+ for (const match2 of matches2) {
39032
+ const key = `${match2.path}\0${match2.line}`;
39033
+ const row = document.createElement("button");
39034
+ row.type = "button";
39035
+ row.className = "gdp-palette-row";
39036
+ row.setAttribute("role", "option");
39037
+ row.dataset.resultKey = key;
39038
+ row.setAttribute("aria-selected", String(key === activeKey));
39039
+ const title = document.createElement("span");
39040
+ title.className = "gdp-palette-row-title";
39041
+ title.textContent = text3().line(match2.line, match2.column);
39042
+ const detail = document.createElement("span");
39043
+ detail.className = "gdp-palette-row-detail";
39044
+ detail.textContent = match2.preview;
39045
+ row.append(title, detail);
39046
+ row.addEventListener("click", () => {
39047
+ setActiveRow(key);
39048
+ const hl = match2.matchText || (deps.getGrepRegex() ? "" : term);
39049
+ deps.openMatch({
39050
+ path: match2.path,
39051
+ line: match2.line,
39052
+ ...hl ? { hl } : {}
39053
+ });
39054
+ });
39055
+ rows.appendChild(row);
39056
+ }
39057
+ group.append(heading2, rows);
39058
+ list2.appendChild(group);
39059
+ }
39060
+ }
39061
+ function setActiveRow(key) {
39062
+ activeKey = key;
39063
+ list2?.querySelectorAll(".gdp-palette-row").forEach((row) => {
39064
+ row.setAttribute("aria-selected", String(row.dataset.resultKey === key));
39065
+ });
39066
+ }
39067
+ function run(nextQuery) {
39068
+ if (!mounted) return;
39069
+ if (nextQuery !== void 0) query = nextQuery.trim();
39070
+ if (input2 && input2.value !== query) input2.value = query;
39071
+ controller?.abort();
39072
+ controller = null;
39073
+ const parsed = parseGrepQuery(query);
39074
+ deps.onQueryChange?.(query);
39075
+ if (!parsed.term) {
39076
+ lastResponse = null;
39077
+ renderResults();
39078
+ return;
39079
+ }
39080
+ const myGeneration = ++generation;
39081
+ if (status) status.textContent = text3().searching;
39082
+ const ref = deps.getRef();
39083
+ const regex = deps.getGrepRegex();
39084
+ const caseSensitive = deps.getGrepCaseSensitive();
39085
+ const wholeWord = deps.getGrepWholeWord();
39086
+ const hideTests = deps.getGrepHideTests();
39087
+ const params = buildGrepRequestParams({
39088
+ term: parsed.term,
39089
+ ref,
39090
+ regex,
39091
+ caseSensitive,
39092
+ wholeWord,
39093
+ hideTests,
39094
+ max: RESULTS_MAX
39095
+ });
39096
+ deps.appendScopeParams(params);
39097
+ for (const scope of parsed.paths) params.append("path", scope);
39098
+ const abort = new AbortController();
39099
+ controller = abort;
39100
+ void deps.trackLoad(
39101
+ fetch(`/_grep?${params.toString()}`, { signal: abort.signal }).then(
39102
+ async (r2) => {
39103
+ if (!r2.ok)
39104
+ throw new Error(
39105
+ `grep request failed (${r2.status}): ${await r2.text()}`
39106
+ );
39107
+ return r2.json();
39108
+ }
39109
+ )
39110
+ ).then((response) => {
39111
+ if (myGeneration !== generation || abort.signal.aborted) return;
39112
+ const current = deps.getServerGeneration();
39113
+ if (response.generation !== void 0 && current > 0 && response.generation < current) {
39114
+ lastResponse = null;
39115
+ renderResults();
39116
+ if (status) status.textContent = text3().repositoryChanged;
39117
+ return;
39118
+ }
39119
+ lastResponse = { response, term: parsed.term };
39120
+ renderResults();
39121
+ if (status)
39122
+ status.textContent = `${text3().grepSummary({
39123
+ engine: response.engine,
39124
+ regex,
39125
+ caseSensitive,
39126
+ wholeWord,
39127
+ testsExcluded: hideTests,
39128
+ truncated: response.truncated,
39129
+ count: response.matches.length,
39130
+ paths: parsed.paths
39131
+ })} · ${text3().resultsScope(ref)}`;
39132
+ }).catch((err) => {
39133
+ if (deps.isAbortError(err) || abort.signal.aborted) return;
39134
+ if (myGeneration !== generation) return;
39135
+ console.error("Search results request failed", err);
39136
+ lastResponse = null;
39137
+ renderResults();
39138
+ if (status)
39139
+ status.textContent = text3().searchFailed(
39140
+ err instanceof Error && err.message ? err.message : text3().unknownError
39141
+ );
39142
+ });
39143
+ }
39144
+ function localize() {
39145
+ if (!mounted) return;
39146
+ const current = text3();
39147
+ if (input2) input2.placeholder = current.resultsPlaceholder;
39148
+ if (runButton) {
39149
+ runButton.textContent = current.resultsRun;
39150
+ runButton.title = current.resultsRun;
39151
+ }
39152
+ host()?.setAttribute("aria-label", current.resultsTitle);
39153
+ renderControls();
39154
+ renderResults();
39155
+ }
39156
+ function isOpen() {
39157
+ const el2 = host();
39158
+ return !!el2 && !el2.hidden;
39159
+ }
39160
+ return {
39161
+ open(nextQuery) {
39162
+ const el2 = host();
39163
+ if (!el2) return;
39164
+ mount();
39165
+ el2.hidden = false;
39166
+ el2.setAttribute("aria-hidden", "false");
39167
+ el2.removeAttribute("inert");
39168
+ document.body.classList.add("search-sheet-open");
39169
+ if (nextQuery !== void 0 && nextQuery !== query) run(nextQuery);
39170
+ else if (nextQuery === void 0 && !lastResponse && query) run(query);
39171
+ else renderResults();
39172
+ input2?.focus();
39173
+ },
39174
+ close() {
39175
+ const el2 = host();
39176
+ if (!el2) return;
39177
+ el2.hidden = true;
39178
+ el2.setAttribute("aria-hidden", "true");
39179
+ el2.setAttribute("inert", "");
39180
+ document.body.classList.remove("search-sheet-open");
39181
+ controller?.abort();
39182
+ controller = null;
39183
+ },
39184
+ isOpen,
39185
+ getQuery: () => query,
39186
+ localize
39187
+ };
39188
+ }
39189
+
38091
39190
  // web-src/views/source-view.ts
38092
39191
  function createSourceView(deps) {
38093
39192
  const {
@@ -38106,6 +39205,7 @@ code-viewer annotate add-db --db app.db --tab query \\
38106
39205
  placeSidebarToggle,
38107
39206
  createFileBreadcrumb,
38108
39207
  createRepositoryWebLink: createRepositoryWebLink2,
39208
+ createRevisionNav,
38109
39209
  createFileDetailMeta,
38110
39210
  createOpenPathButton,
38111
39211
  createMoveToTrashButton,
@@ -38396,10 +39496,17 @@ code-viewer annotate add-db --db app.db --tab query \\
38396
39496
  const cells = table2.querySelectorAll(
38397
39497
  ".gdp-source-line-code"
38398
39498
  );
39499
+ const sourceLines = textValue.split("\n");
38399
39500
  cells.forEach((cell, index) => {
38400
39501
  if (highlightedLines[index] == null) return;
38401
39502
  cell.innerHTML = highlightedLines[index] || " ";
38402
39503
  cell.classList.add("shiki");
39504
+ markSourceHighlightTerm(
39505
+ cell,
39506
+ sourceLines[index] ?? "",
39507
+ index + 1,
39508
+ target
39509
+ );
38403
39510
  });
38404
39511
  }).catch((err) => {
38405
39512
  console.error("Failed to apply source syntax highlighting", err);
@@ -38814,6 +39921,7 @@ code-viewer annotate add-db --db app.db --tab query \\
38814
39921
  const code2 = document.createElement("td");
38815
39922
  code2.className = "gdp-source-line-code";
38816
39923
  code2.textContent = line || " ";
39924
+ markSourceHighlightTerm(code2, line, index + 1, target);
38817
39925
  tr.appendChild(num);
38818
39926
  tr.appendChild(code2);
38819
39927
  tbody.appendChild(tr);
@@ -38959,6 +40067,45 @@ code-viewer annotate add-db --db app.db --tab query \\
38959
40067
  const routeTarget = sourceTargetFromRoute();
38960
40068
  return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : void 0;
38961
40069
  }
40070
+ function currentSourceHighlightTerm(target) {
40071
+ const routeTarget = sourceTargetFromRoute();
40072
+ return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" && STATE.route.line && STATE.route.hl ? STATE.route.hl : void 0;
40073
+ }
40074
+ function syncSourceHighlightMarks(card, target) {
40075
+ card.querySelectorAll("mark.gdp-grep-match").forEach((mark) => {
40076
+ const parent = mark.parentElement;
40077
+ if (!parent) return;
40078
+ while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
40079
+ parent.removeChild(mark);
40080
+ parent.normalize();
40081
+ });
40082
+ const term = currentSourceHighlightTerm(target);
40083
+ const virtual = card.querySelector(
40084
+ ".gdp-source-virtual"
40085
+ );
40086
+ if (virtual?.__gdpVirtualSourceSearch) {
40087
+ if (term) virtual.__gdpVirtualSourceSearch.open(term);
40088
+ return;
40089
+ }
40090
+ if (!term) return;
40091
+ card.querySelectorAll(".gdp-source-table tr[data-line]").forEach((row) => {
40092
+ const cell = row.querySelector(".gdp-source-line-code");
40093
+ if (!cell) return;
40094
+ markSourceHighlightTerm(
40095
+ cell,
40096
+ cell.textContent ?? "",
40097
+ Number(row.dataset.line || "0"),
40098
+ target
40099
+ );
40100
+ });
40101
+ }
40102
+ function markSourceHighlightTerm(cell, lineText, lineNumber, target) {
40103
+ const term = currentSourceHighlightTerm(target);
40104
+ if (!term) return;
40105
+ if (!lineInSourceTarget(lineNumber, currentSourceLineTarget(target)))
40106
+ return;
40107
+ markNeedleInCell(cell, lineText, term, "gdp-grep-match");
40108
+ }
38962
40109
  function lineTargetStart(line) {
38963
40110
  if (!line) return void 0;
38964
40111
  return typeof line === "number" ? line : line.start;
@@ -39166,8 +40313,9 @@ code-viewer annotate add-db --db app.db --tab query \\
39166
40313
  next.addEventListener("click", () => move(1));
39167
40314
  close.addEventListener("click", hide);
39168
40315
  return {
39169
- open: () => {
40316
+ open: (query) => {
39170
40317
  bar.hidden = false;
40318
+ if (query !== void 0) input2.value = query;
39171
40319
  input2.focus();
39172
40320
  input2.select();
39173
40321
  sync();
@@ -39335,6 +40483,13 @@ code-viewer annotate add-db --db app.db --tab query \\
39335
40483
  render
39336
40484
  );
39337
40485
  wrap.__gdpVirtualSourceSearch = search;
40486
+ const initialHighlightTerm = currentSourceHighlightTerm(target);
40487
+ if (initialHighlightTerm) {
40488
+ const searchHandle = search;
40489
+ setTimeout(() => {
40490
+ if (wrap.isConnected) searchHandle.open(initialHighlightTerm);
40491
+ }, 0);
40492
+ }
39338
40493
  let resizeObserver = null;
39339
40494
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
39340
40495
  if (!scroller.isConnected) {
@@ -39588,6 +40743,13 @@ code-viewer annotate add-db --db app.db --tab query \\
39588
40743
  render
39589
40744
  );
39590
40745
  wrap.__gdpVirtualSourceSearch = search;
40746
+ const initialHighlightTerm = currentSourceHighlightTerm(target);
40747
+ if (initialHighlightTerm) {
40748
+ const searchHandle = search;
40749
+ setTimeout(() => {
40750
+ if (wrap.isConnected) searchHandle.open(initialHighlightTerm);
40751
+ }, 0);
40752
+ }
39591
40753
  let resizeObserver = null;
39592
40754
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
39593
40755
  if (!scroller.isConnected) {
@@ -39737,6 +40899,8 @@ code-viewer annotate add-db --db app.db --tab query \\
39737
40899
  const mounted = mountedStandaloneSourceCard(target);
39738
40900
  const state = mounted?.dataset.sourceState;
39739
40901
  if (mounted && state === "done") {
40902
+ syncRenderedSourceLineHighlights(mounted, target);
40903
+ syncSourceHighlightMarks(mounted, target);
39740
40904
  scrollStandaloneSourceLine(
39741
40905
  mounted,
39742
40906
  lineTargetStart(
@@ -39772,7 +40936,8 @@ code-viewer annotate add-db --db app.db --tab query \\
39772
40936
  setRoute,
39773
40937
  setPreferredSourceTab,
39774
40938
  createFileBreadcrumb,
39775
- createRepositoryWebLink: createRepositoryWebLink2
40939
+ createRepositoryWebLink: createRepositoryWebLink2,
40940
+ createRevisionNav
39776
40941
  },
39777
40942
  target,
39778
40943
  activeTab,
@@ -45455,6 +46620,7 @@ ${t2.files.overlapTitle(others)}`;
45455
46620
  if (name) name.textContent = branch;
45456
46621
  el2.title = branch ? `Current branch: ${branch}` : "";
45457
46622
  }
46623
+ const MAX_RECENT_REFS = 8;
45458
46624
  function mergeLocalSettings(patch) {
45459
46625
  const next = { ...APP_SETTINGS };
45460
46626
  for (const [key, value] of Object.entries(patch)) {
@@ -46021,7 +47187,20 @@ ${error2.stack}` : ""}`
46021
47187
  copyReferenceLabel: () => uiText().global.copyLineReference,
46022
47188
  lineCountLabel: (count) => uiText().global.selectedLineCount(count),
46023
47189
  githubOpenTitle: () => uiText().global.githubSelectionOpen,
46024
- githubCopyTitle: () => uiText().global.githubSelectionCopy
47190
+ githubCopyTitle: () => uiText().global.githubSelectionCopy,
47191
+ lineHistoryTitle: () => uiText().global.lineHistory,
47192
+ openLineHistory: (path, start, end) => {
47193
+ const route = STATE.route;
47194
+ const ref = route.screen === "file" ? route.commit || route.ref || "worktree" : route.screen === "diff" && route.range.to ? route.range.to : "worktree";
47195
+ navigateToRoute({
47196
+ screen: "file",
47197
+ path,
47198
+ ref,
47199
+ view: "history",
47200
+ lines: { start, end },
47201
+ range: currentRange()
47202
+ });
47203
+ }
46025
47204
  });
46026
47205
  const DIFF_LINE_SELECT = createDiffLineSelect({ pill: LINE_REF_PILL });
46027
47206
  function clearRenderedSourceLineTargets() {
@@ -46091,6 +47270,7 @@ ${error2.stack}` : ""}`
46091
47270
  REPO_SIDEBAR_REF = ref;
46092
47271
  },
46093
47272
  isTestPath: isTestFilePath,
47273
+ filterCountTitle: (visible, total) => uiText().sidebar.filterCountTitle(visible, total),
46094
47274
  sidebarToggleTitle: (hidden) => hidden ? uiText().sidebar.show : uiText().sidebar.hide,
46095
47275
  openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
46096
47276
  omittedDirectoryBadge: (reason) => {
@@ -46169,6 +47349,7 @@ ${error2.stack}` : ""}`
46169
47349
  placeSidebarToggle,
46170
47350
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
46171
47351
  createRepositoryWebLink: createFileRepositoryWebLink,
47352
+ createRevisionNav: createFileRevisionNav,
46172
47353
  createFileDetailMeta: (target, meta) => REPO_VIEW.createFileDetailMeta(target, meta),
46173
47354
  createOpenPathButton,
46174
47355
  createMoveToTrashButton: (path, onDeleted) => REPO_VIEW.createMoveToTrashButton(path, onDeleted),
@@ -46212,6 +47393,7 @@ ${error2.stack}` : ""}`
46212
47393
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
46213
47394
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
46214
47395
  createRepositoryWebLink: createFileRepositoryWebLink,
47396
+ createRevisionNav: createFileRevisionNav,
46215
47397
  removeStandaloneSource,
46216
47398
  placeSidebarToggle,
46217
47399
  escapeHtml: escapeHtml3,
@@ -46295,6 +47477,17 @@ ${error2.stack}` : ""}`
46295
47477
  }),
46296
47478
  openGithubLabel: () => uiText().repo.openGithub,
46297
47479
  openRepositoryWebLabel: () => uiText().repo.openRepositoryWeb,
47480
+ folderHistoryLabel: () => uiText().repo.folderHistory,
47481
+ folderHistoryTitle: () => uiText().repo.folderHistoryTitle,
47482
+ openFolderHistory: (ref, path) => {
47483
+ const dir = path.replace(/\/+$/, "");
47484
+ navigateToRoute({
47485
+ screen: "history",
47486
+ ref: ref && ref !== "worktree" ? ref : "HEAD",
47487
+ ...dir ? { path: `${dir}/` } : {},
47488
+ range: currentRange()
47489
+ });
47490
+ },
46298
47491
  fileBadge: (status) => DIFF_VIEW.fileBadge(status)
46299
47492
  });
46300
47493
  const {
@@ -46328,6 +47521,8 @@ ${error2.stack}` : ""}`
46328
47521
  getFileSelectionHistory: () => APP_SETTINGS.fileSelectionHistory || [],
46329
47522
  getGrepSelectionHistory: () => APP_SETTINGS.grepSelectionHistory || [],
46330
47523
  getGrepRegex: () => APP_SETTINGS.grepRegex === true,
47524
+ getGrepCaseSensitive: () => APP_SETTINGS.grepCaseSensitive === true,
47525
+ getGrepWholeWord: () => APP_SETTINGS.grepWholeWord === true,
46331
47526
  getGrepHideTests: () => STATE.hideTests,
46332
47527
  getGrepGroupByFile: () => APP_SETTINGS.grepGroupByFile === true,
46333
47528
  getGrepPaletteWidth: () => APP_SETTINGS.grepPaletteWidth,
@@ -46336,7 +47531,8 @@ ${error2.stack}` : ""}`
46336
47531
  applyGrepHideTests: (hidden) => {
46337
47532
  STATE.hideTests = hidden;
46338
47533
  applyHideTests();
46339
- }
47534
+ },
47535
+ openSearchResults: (query) => openSearchSheet(query)
46340
47536
  });
46341
47537
  const { openSearchPalette, isPaletteOpen, paletteMode, clearRepoFileCache } = SEARCH_PALETTE;
46342
47538
  const UI_TEXT = {
@@ -46366,6 +47562,11 @@ ${error2.stack}` : ""}`
46366
47562
  queryHistory: "query history",
46367
47563
  settings: "viewer settings",
46368
47564
  theme: "toggle theme",
47565
+ search: "Search files (Ctrl+K) · Shift+click: grep (Ctrl+G)",
47566
+ lineHistory: "Line history",
47567
+ recentRef: "Recently used ref",
47568
+ olderRevision: "Older revision of this file",
47569
+ newerRevision: "Newer revision of this file",
46369
47570
  copyAiContext: "Copy AI context (Shift+Click to include code)",
46370
47571
  copyAiContextCopied: "Copied AI context",
46371
47572
  copyAiContextCopiedWithCode: (lines) => `Copied AI context + code (${lines} line${lines === 1 ? "" : "s"})`,
@@ -46454,7 +47655,8 @@ ${error2.stack}` : ""}`
46454
47655
  treeTitle: "tree view",
46455
47656
  flatTitle: "flat list",
46456
47657
  filter: "Filter files… / ⌘K",
46457
- filterTitle: "Filter files. Use /pattern/ for regex. Press / to focus this field, Cmd/Ctrl+K for the full-file palette, Ctrl+G for grep, ? for help.",
47658
+ filterTitle: "Filter files. Plain text matches anywhere in the path; /pattern/ is a regex, ~text is a fuzzy match, *.ts or src/** is a glob. Press / to focus this field, Cmd/Ctrl+K for the full-file palette, Ctrl+G for grep, ? for help.",
47659
+ filterCountTitle: (visible, total) => `${visible} of ${total} files match the filter`,
46458
47660
  filterClear: "Clear",
46459
47661
  filterClearTitle: "Clear file filter",
46460
47662
  hide: "hide sidebar",
@@ -46490,7 +47692,9 @@ ${error2.stack}` : ""}`
46490
47692
  submoduleLabel: "submodule",
46491
47693
  submoduleTitle: "Git submodule pinned to a commit",
46492
47694
  openGithub: "Open on GitHub",
46493
- openRepositoryWeb: "Open repository web page"
47695
+ openRepositoryWeb: "Open repository web page",
47696
+ folderHistory: "History",
47697
+ folderHistoryTitle: "Commits that touched this folder"
46494
47698
  },
46495
47699
  history: {
46496
47700
  title: "Commits",
@@ -46702,6 +47906,11 @@ ${error2.stack}` : ""}`
46702
47906
  queryHistory: "クエリ履歴",
46703
47907
  settings: "ビューア設定",
46704
47908
  theme: "テーマ切り替え",
47909
+ search: "ファイルを検索 (Ctrl+K)・Shift+クリックで grep (Ctrl+G)",
47910
+ lineHistory: "この行の履歴",
47911
+ recentRef: "最近使った ref",
47912
+ olderRevision: "このファイルの 1 つ前のリビジョン",
47913
+ newerRevision: "このファイルの 1 つ後のリビジョン",
46705
47914
  copyAiContext: "AI 用コンテキストをコピー(Shift+Click でコードも添付)",
46706
47915
  copyAiContextCopied: "コピーしました",
46707
47916
  copyAiContextCopiedWithCode: (lines) => `コピーしました(コード付き・${lines}行)`,
@@ -46790,7 +47999,8 @@ ${error2.stack}` : ""}`
46790
47999
  treeTitle: "ツリー表示",
46791
48000
  flatTitle: "一覧表示",
46792
48001
  filter: "ファイル絞り込み… / ⌘K",
46793
- filterTitle: "ファイルを絞り込みます。/pattern/ は正規表現。/ でこの欄にフォーカス、Cmd/Ctrl+K で全ファイルパレット、Ctrl+G で grep、? でヘルプ。",
48002
+ filterTitle: "ファイルを絞り込みます。文字列はパスの部分一致、/pattern/ は正規表現、~text はあいまい一致、*.ts や src/** は glob。/ でこの欄にフォーカス、Cmd/Ctrl+K で全ファイルパレット、Ctrl+G で grep、? でヘルプ。",
48003
+ filterCountTitle: (visible, total) => `${total} ファイル中 ${visible} 件が一致`,
46794
48004
  filterClear: "解除",
46795
48005
  filterClearTitle: "ファイル絞り込みを解除",
46796
48006
  hide: "サイドバーを隠す",
@@ -46826,7 +48036,9 @@ ${error2.stack}` : ""}`
46826
48036
  submoduleLabel: "サブモジュール",
46827
48037
  submoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。直接は開けません。",
46828
48038
  openGithub: "GitHubで開く",
46829
- openRepositoryWeb: "リポジトリのウェブページを開く"
48039
+ openRepositoryWeb: "リポジトリのウェブページを開く",
48040
+ folderHistory: "履歴",
48041
+ folderHistoryTitle: "このフォルダを変更したコミット"
46830
48042
  },
46831
48043
  history: {
46832
48044
  title: "コミット",
@@ -47054,6 +48266,11 @@ ${error2.stack}` : ""}`
47054
48266
  quickHelpBtn.title = text3.quickHelp.buttonTitle;
47055
48267
  quickHelpBtn.setAttribute("aria-label", text3.quickHelp.buttonTitle);
47056
48268
  }
48269
+ const searchBtn = document.querySelector("#search-btn");
48270
+ if (searchBtn) {
48271
+ searchBtn.title = text3.global.search;
48272
+ searchBtn.setAttribute("aria-label", text3.global.search);
48273
+ }
47057
48274
  QUICK_HELP?.localize();
47058
48275
  const doctorTitle = doctorText(STATE.language).title;
47059
48276
  const doctorBtn = document.querySelector("#doctor-btn");
@@ -47080,6 +48297,13 @@ ${error2.stack}` : ""}`
47080
48297
  }
47081
48298
  document.querySelector("#terminal-sheet")?.setAttribute("aria-label", terminalChrome.title);
47082
48299
  relocalizeTerminal?.();
48300
+ const searchChrome = searchPaletteText(STATE.language);
48301
+ const searchTabBtn = document.querySelector("#panel-tab-search");
48302
+ if (searchTabBtn) {
48303
+ searchTabBtn.textContent = searchChrome.resultsTitle;
48304
+ searchTabBtn.title = searchChrome.resultsOpen;
48305
+ }
48306
+ relocalizeSearchResults?.();
47083
48307
  document.querySelector(".app-panel-tabs")?.setAttribute("aria-label", text3.appPanel.tabs);
47084
48308
  const panelLayout = document.querySelector(
47085
48309
  ".app-panel-layout-switch"
@@ -47242,6 +48466,7 @@ ${error2.stack}` : ""}`
47242
48466
  let relocalizeTools = null;
47243
48467
  let relocalizeViewerSettings = null;
47244
48468
  let relocalizeTerminal = null;
48469
+ let relocalizeSearchResults = null;
47245
48470
  let relocalizeDatabase = null;
47246
48471
  let QUICK_HELP = null;
47247
48472
  function setViewerLanguage(language, persist = true) {
@@ -47592,6 +48817,18 @@ ${error2.stack}` : ""}`
47592
48817
  function isHistoryPanelRoute(route) {
47593
48818
  return route.screen === "history" || isFileHistoryRoute(route);
47594
48819
  }
48820
+ function historyRefForCurrentView() {
48821
+ const route = STATE.route;
48822
+ if (route.screen === "history") return route.ref || "HEAD";
48823
+ if (route.screen === "repo" || route.screen === "file") {
48824
+ return route.ref && route.ref !== "worktree" ? route.ref : "HEAD";
48825
+ }
48826
+ if (route.screen === "diff") {
48827
+ const to = route.range.to;
48828
+ return to && to !== "worktree" ? to : "HEAD";
48829
+ }
48830
+ return "HEAD";
48831
+ }
47595
48832
  function normalizeInternalFileRoute(route) {
47596
48833
  if (route.screen !== "file") return route;
47597
48834
  if (sourceInternalPathKind(route.path) === null) return route;
@@ -47640,6 +48877,7 @@ ${error2.stack}` : ""}`
47640
48877
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
47641
48878
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
47642
48879
  createRepositoryWebLink: createFileRepositoryWebLink,
48880
+ createRevisionNav: createFileRevisionNav,
47643
48881
  emptyText: () => uiText().diff
47644
48882
  },
47645
48883
  historyRoute,
@@ -47659,15 +48897,21 @@ ${error2.stack}` : ""}`
47659
48897
  return ANNOTATIONS_UI ? ANNOTATIONS_UI.withSessionParam(rawUrl) : rawUrl;
47660
48898
  }
47661
48899
  function withOverlayState(url) {
47662
- return withTerminalOverlay(
47663
- withToolsOverlay(
47664
- withDoctorOverlay(
47665
- url,
47666
- parseDoctorOverlay(window.location.pathname, window.location.search)
48900
+ return withSearchResultsOverlay(
48901
+ withTerminalOverlay(
48902
+ withToolsOverlay(
48903
+ withDoctorOverlay(
48904
+ url,
48905
+ parseDoctorOverlay(
48906
+ window.location.pathname,
48907
+ window.location.search
48908
+ )
48909
+ ),
48910
+ parseToolsOverlay(window.location.search)
47667
48911
  ),
47668
- parseToolsOverlay(window.location.search)
48912
+ parseTerminalOverlay(window.location.search)
47669
48913
  ),
47670
- parseTerminalOverlay(window.location.search)
48914
+ parseSearchResultsOverlay(window.location.search)
47671
48915
  );
47672
48916
  }
47673
48917
  function urlForRoute(route) {
@@ -48058,6 +49302,61 @@ ${error2.stack}` : ""}`
48058
49302
  });
48059
49303
  return button;
48060
49304
  }
49305
+ function createFileRevisionNav(target, activeTab) {
49306
+ if (activeTab === "history") return null;
49307
+ const nav = document.createElement("span");
49308
+ nav.className = "gdp-file-revision-nav";
49309
+ const text3 = uiText().global;
49310
+ const make = (title, paths) => {
49311
+ const button = document.createElement("button");
49312
+ button.type = "button";
49313
+ button.className = "gdp-file-header-icon gdp-file-revision-btn";
49314
+ button.title = title;
49315
+ button.setAttribute("aria-label", title);
49316
+ button.disabled = true;
49317
+ button.innerHTML = iconSvg("octicon-revision", paths);
49318
+ return button;
49319
+ };
49320
+ const older = make(text3.olderRevision, PREVIOUS_16_PATHS);
49321
+ const newer = make(text3.newerRevision, NEXT_16_PATHS);
49322
+ nav.append(older, newer);
49323
+ const goTo = (sha) => {
49324
+ const view = STATE.route.screen === "file" && STATE.route.view === "blame" ? "blame" : "blob";
49325
+ navigateToRoute({
49326
+ screen: "file",
49327
+ path: target.path,
49328
+ ref: sha,
49329
+ view,
49330
+ range: currentRange()
49331
+ });
49332
+ };
49333
+ const params = new URLSearchParams({
49334
+ path: target.path,
49335
+ ref: target.ref || "worktree"
49336
+ });
49337
+ void trackLoad(
49338
+ fetch(`/_file_revisions?${params.toString()}`).then(async (r2) => {
49339
+ if (!r2.ok) throw new Error(await r2.text());
49340
+ return r2.json();
49341
+ })
49342
+ ).then((neighbors) => {
49343
+ if (!nav.isConnected) return;
49344
+ if (neighbors.previous) {
49345
+ const sha = neighbors.previous;
49346
+ older.disabled = false;
49347
+ older.addEventListener("click", () => goTo(sha));
49348
+ }
49349
+ if (neighbors.next) {
49350
+ const sha = neighbors.next;
49351
+ newer.disabled = false;
49352
+ newer.addEventListener("click", () => goTo(sha));
49353
+ }
49354
+ }).catch((err) => {
49355
+ if (isAbortError3(err)) return;
49356
+ console.error("Failed to load file revision neighbours", err);
49357
+ });
49358
+ return nav;
49359
+ }
48061
49360
  function createFileRepositoryWebLink(target) {
48062
49361
  const webTarget = buildRepositoryWebTarget(REPO_WEB_URL, {
48063
49362
  ref: target.ref,
@@ -48259,6 +49558,12 @@ ${error2.stack}` : ""}`
48259
49558
  if (quickHelpIcon) {
48260
49559
  quickHelpIcon.innerHTML = iconSvg("octicon-question", QUESTION_16_PATH);
48261
49560
  }
49561
+ const searchIcon = document.querySelector(
49562
+ "#search-btn .goi-icon"
49563
+ );
49564
+ if (searchIcon) {
49565
+ searchIcon.innerHTML = iconSvg("octicon-search", SEARCH_16_PATH);
49566
+ }
48262
49567
  const branchIcon = document.querySelector(
48263
49568
  "#project-branch .goi-icon"
48264
49569
  );
@@ -48313,6 +49618,10 @@ ${error2.stack}` : ""}`
48313
49618
  event.preventDefault();
48314
49619
  openTerminalSheet();
48315
49620
  });
49621
+ $("#panel-tab-search")?.addEventListener("click", (event) => {
49622
+ event.preventDefault();
49623
+ openSearchSheet();
49624
+ });
48316
49625
  $("#app-panel-close")?.addEventListener("click", (event) => {
48317
49626
  event.preventDefault();
48318
49627
  closeAppPanel();
@@ -48548,6 +49857,9 @@ ${error2.stack}` : ""}`
48548
49857
  syncSidebarFilterClearButton();
48549
49858
  sbFilterClear.addEventListener("click", clearSidebarFilter);
48550
49859
  }
49860
+ document.querySelector("#search-btn")?.addEventListener("click", (event) => {
49861
+ openSearchPalette(event.shiftKey ? "grep" : "file");
49862
+ });
48551
49863
  function focusFileFilter() {
48552
49864
  const input2 = $("#sb-filter");
48553
49865
  input2.focus();
@@ -48738,11 +50050,18 @@ ${error2.stack}` : ""}`
48738
50050
  if (action === "goto-history") {
48739
50051
  navigateToRoute({
48740
50052
  screen: "history",
48741
- ref: "HEAD",
50053
+ ref: historyRefForCurrentView(),
48742
50054
  range: currentRange()
48743
50055
  });
48744
50056
  return true;
48745
50057
  }
50058
+ if (action === "history-next-commit" || action === "history-previous-commit") {
50059
+ if (!isHistoryPanelRoute(STATE.route)) return false;
50060
+ void HISTORY_VIEW.moveCommitSelection(
50061
+ action === "history-next-commit" ? 1 : -1
50062
+ );
50063
+ return true;
50064
+ }
48746
50065
  if (action === "goto-repo") {
48747
50066
  navigateToRoute({
48748
50067
  screen: "repo",
@@ -49060,6 +50379,7 @@ ${error2.stack}` : ""}`
49060
50379
  syncDoctorSheetFromUrl();
49061
50380
  syncToolsSheetFromUrl();
49062
50381
  syncTerminalSheetFromUrl();
50382
+ syncSearchSheetFromUrl();
49063
50383
  });
49064
50384
  function syncRefInputs() {
49065
50385
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -49151,7 +50471,19 @@ ${error2.stack}` : ""}`
49151
50471
  },
49152
50472
  getSyntaxHighlight: () => STATE.syntaxHighlight,
49153
50473
  getLanguage: () => STATE.language,
49154
- trackLoad
50474
+ trackLoad,
50475
+ commitWebLink: (sha) => {
50476
+ const target = buildRepositoryWebTarget(REPO_WEB_URL, {
50477
+ ref: sha,
50478
+ kind: "commit"
50479
+ });
50480
+ if (!target) return null;
50481
+ return createRepositoryWebLink(
50482
+ target,
50483
+ target.provider === "github" ? uiText().repo.openGithub : uiText().repo.openRepositoryWeb
50484
+ );
50485
+ },
50486
+ copyText: (text3) => navigator.clipboard.writeText(text3)
49155
50487
  });
49156
50488
  relocalizeHistory = () => HISTORY_VIEW.localize();
49157
50489
  function helpSectionDeps() {
@@ -49252,14 +50584,16 @@ ${error2.stack}` : ""}`
49252
50584
  function syncAppPanel() {
49253
50585
  const tools = parseToolsOverlay(window.location.search) !== null;
49254
50586
  const terminal = parseTerminalOverlay(window.location.search) !== null;
49255
- const open2 = tools || terminal;
50587
+ const search = parseSearchResultsOverlay(window.location.search) !== null;
50588
+ const open2 = tools || terminal || search;
49256
50589
  const panel = document.getElementById("app-panel");
49257
50590
  if (panel) {
49258
50591
  panel.classList.toggle("app-panel-open", open2);
49259
50592
  }
49260
50593
  for (const [id, selected] of [
49261
50594
  ["#panel-tab-tools", tools],
49262
- ["#panel-tab-terminal", terminal]
50595
+ ["#panel-tab-terminal", terminal],
50596
+ ["#panel-tab-search", search]
49263
50597
  ]) {
49264
50598
  const tab = document.querySelector(id);
49265
50599
  if (!tab) continue;
@@ -49284,6 +50618,8 @@ ${error2.stack}` : ""}`
49284
50618
  syncAppPanelLayout();
49285
50619
  }
49286
50620
  function closeAppPanel() {
50621
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50622
+ closeSearchSheet();
49287
50623
  if (parseToolsOverlay(window.location.search) !== null || TOOLS_VIEW.isOpen())
49288
50624
  closeToolsSheet();
49289
50625
  if (parseTerminalOverlay(window.location.search) !== null || TERMINAL_VIEW.isOpen())
@@ -49332,6 +50668,8 @@ ${error2.stack}` : ""}`
49332
50668
  function openToolsSheet(tool) {
49333
50669
  if (parseTerminalOverlay(window.location.search) !== null || TERMINAL_VIEW.isOpen())
49334
50670
  closeTerminalSheet();
50671
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50672
+ closeSearchSheet();
49335
50673
  updateUrlForToolsOverlay(tool ?? TOOLS_VIEW.getActiveTool());
49336
50674
  void TOOLS_VIEW.open(tool);
49337
50675
  syncAppPanel();
@@ -49349,6 +50687,77 @@ ${error2.stack}` : ""}`
49349
50687
  else if (!tool && open2) TOOLS_VIEW.close();
49350
50688
  syncAppPanel();
49351
50689
  }
50690
+ const SEARCH_RESULTS_VIEW = createSearchResultsView({
50691
+ $: (sel) => document.querySelector(sel),
50692
+ trackLoad,
50693
+ getLanguage: () => STATE.language,
50694
+ appendScopeParams,
50695
+ getRef: () => {
50696
+ const route = STATE.route;
50697
+ if (route.screen === "repo" || route.screen === "file")
50698
+ return route.ref || "worktree";
50699
+ return STATE.repoRef || "worktree";
50700
+ },
50701
+ getServerGeneration: () => SERVER_GENERATION,
50702
+ isAbortError: isAbortError3,
50703
+ getGrepRegex: () => APP_SETTINGS.grepRegex === true,
50704
+ getGrepCaseSensitive: () => APP_SETTINGS.grepCaseSensitive === true,
50705
+ getGrepWholeWord: () => APP_SETTINGS.grepWholeWord === true,
50706
+ getGrepHideTests: () => STATE.hideTests,
50707
+ persistGrepSettings: async (patch) => {
50708
+ await persistSettingsPatch(patch);
50709
+ if (patch.hideTests !== void 0) {
50710
+ STATE.hideTests = patch.hideTests;
50711
+ applyHideTests();
50712
+ }
50713
+ },
50714
+ openMatch: ({ path, line, hl }) => {
50715
+ const route = STATE.route;
50716
+ const ref = route.screen === "repo" || route.screen === "file" ? route.ref || "worktree" : STATE.repoRef || "worktree";
50717
+ setRoute({
50718
+ screen: "file",
50719
+ path,
50720
+ ref,
50721
+ view: "blob",
50722
+ line,
50723
+ ...hl ? { hl } : {},
50724
+ range: currentRange()
50725
+ });
50726
+ void renderStandaloneSource({ path, ref });
50727
+ },
50728
+ onQueryChange: (query) => updateUrlForSearchResultsOverlay(query)
50729
+ });
50730
+ relocalizeSearchResults = () => SEARCH_RESULTS_VIEW.localize();
50731
+ function updateUrlForSearchResultsOverlay(query) {
50732
+ const current = window.location.pathname + window.location.search;
50733
+ const next = withSearchResultsOverlay(current, query);
50734
+ if (next !== current) {
50735
+ history.replaceState(history.state, "", next + window.location.hash);
50736
+ }
50737
+ syncHeaderMenu();
50738
+ }
50739
+ function openSearchSheet(query) {
50740
+ if (parseTerminalOverlay(window.location.search) !== null || TERMINAL_VIEW.isOpen())
50741
+ closeTerminalSheet();
50742
+ if (parseToolsOverlay(window.location.search) !== null || TOOLS_VIEW.isOpen())
50743
+ closeToolsSheet();
50744
+ updateUrlForSearchResultsOverlay(query ?? SEARCH_RESULTS_VIEW.getQuery());
50745
+ SEARCH_RESULTS_VIEW.open(query);
50746
+ syncAppPanel();
50747
+ }
50748
+ function closeSearchSheet() {
50749
+ SEARCH_RESULTS_VIEW.close();
50750
+ updateUrlForSearchResultsOverlay(null);
50751
+ syncAppPanel();
50752
+ }
50753
+ function syncSearchSheetFromUrl() {
50754
+ const query = parseSearchResultsOverlay(window.location.search);
50755
+ const open2 = SEARCH_RESULTS_VIEW.isOpen();
50756
+ if (query !== null && (!open2 || SEARCH_RESULTS_VIEW.getQuery() !== query))
50757
+ SEARCH_RESULTS_VIEW.open(query);
50758
+ else if (query === null && open2) SEARCH_RESULTS_VIEW.close();
50759
+ syncAppPanel();
50760
+ }
49352
50761
  const TERMINAL_VIEW = createTerminalView({
49353
50762
  $: (sel) => document.querySelector(sel),
49354
50763
  trackLoad,
@@ -49377,6 +50786,8 @@ ${error2.stack}` : ""}`
49377
50786
  function openTerminalSheet(id) {
49378
50787
  if (parseToolsOverlay(window.location.search) !== null || TOOLS_VIEW.isOpen())
49379
50788
  closeToolsSheet();
50789
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50790
+ closeSearchSheet();
49380
50791
  const target = id ?? TERMINAL_VIEW.getActiveTarget();
49381
50792
  updateUrlForTerminalOverlay(target ?? "open");
49382
50793
  void TERMINAL_VIEW.open(target);
@@ -49454,7 +50865,17 @@ ${error2.stack}` : ""}`
49454
50865
  getFrom: () => STATE.from,
49455
50866
  getTo: () => STATE.to,
49456
50867
  getRepoRef: () => STATE.repoRef,
49457
- getRoute: () => STATE.route
50868
+ getRoute: () => STATE.route,
50869
+ getRecentRefs: () => APP_SETTINGS.recentRefs || [],
50870
+ rememberRecentRef: (ref) => {
50871
+ const current = APP_SETTINGS.recentRefs || [];
50872
+ const next = rememberPaletteSelection(current, ref).slice(
50873
+ -MAX_RECENT_REFS
50874
+ );
50875
+ if (next.join("\0") === current.join("\0")) return;
50876
+ patchSettings({ recentRefs: next });
50877
+ },
50878
+ recentRefTitle: () => uiText().global.recentRef
49458
50879
  });
49459
50880
  if (REF_PICKER) {
49460
50881
  const historyRefInput = document.querySelector("#history-ref");
@@ -49511,6 +50932,7 @@ ${error2.stack}` : ""}`
49511
50932
  syncDoctorSheetFromUrl();
49512
50933
  syncToolsSheetFromUrl();
49513
50934
  syncTerminalSheetFromUrl();
50935
+ syncSearchSheetFromUrl();
49514
50936
  if (isSameBlobFileRoute(previousRoute, STATE.route) && routeBlobPreview(previousRoute) !== routeBlobPreview(STATE.route) && switchSourceTab(routeBlobPreview(STATE.route) ? "preview" : "code", {
49515
50937
  updateRoute: false
49516
50938
  })) {