@youtyan/code-viewer 0.13.1 → 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"
@@ -12478,6 +12917,7 @@ ${frontmatter.yaml}
12478
12917
  exportAction: "Export",
12479
12918
  foreignKeyHint: "Foreign key — click to view related rows",
12480
12919
  relatedEmpty: "No matching row in the referenced table",
12920
+ relatedListResize: "Resize the related-reference list",
12481
12921
  filteredEmptyTitle: (count) => `No rows match ${count} active filter${count === 1 ? "" : "s"}`,
12482
12922
  filteredEmptyHint: "The table was loaded, but the current search or column filters hide every row.",
12483
12923
  filteredEmptyAction: "Clear filters",
@@ -12835,6 +13275,7 @@ ${frontmatter.yaml}
12835
13275
  exportAction: "エクスポート",
12836
13276
  foreignKeyHint: "外部キー: クリックして関連データを表示",
12837
13277
  relatedEmpty: "参照先に該当する行がありません",
13278
+ relatedListResize: "関連参照リストの幅を変える",
12838
13279
  filteredEmptyTitle: (count) => `フィルタ ${count} 件に一致する行がありません`,
12839
13280
  filteredEmptyHint: "表は読み込めていますが、現在の検索/列フィルタですべての行が隠れています。",
12840
13281
  filteredEmptyAction: "フィルタ解除",
@@ -20087,17 +20528,29 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20087
20528
 
20088
20529
  // web-src/views/database/table-grid.ts
20089
20530
  var ROW_HEIGHT = 28;
20531
+ var ROWNUM_WIDTH = 50;
20090
20532
  var OVERSCAN = 20;
20091
20533
  var PAGE_SIZE = 200;
20092
20534
  var MAX_PAGE_CACHE_PAGES = 32;
20093
20535
  var FILTER_DEBOUNCE_MS = 300;
20094
20536
  var DEFAULT_COL_WIDTH = 180;
20095
20537
  var CELL_PREVIEW_MAX_CHARS = 4e3;
20538
+ var DETAIL_JSON_HIGHLIGHT_MAX_CHARS = 1e5;
20096
20539
  var RELATED_PANEL_DEFAULT_HEIGHT = 320;
20097
20540
  var RELATED_PANEL_MIN_HEIGHT = 60;
20098
20541
  var DETAIL_PANEL_DEFAULT_HEIGHT = 200;
20099
20542
  var DETAIL_PANEL_MIN_HEIGHT = 40;
20100
20543
  var PANEL_MAX_RESERVE = 20;
20544
+ var RELATED_LIST_DEFAULT_WIDTH = 200;
20545
+ var RELATED_LIST_MIN_WIDTH = 120;
20546
+ var RELATED_LIST_MAX_WIDTH = 480;
20547
+ var RELATED_LIST_WIDTH_KEY = "code-viewer:db-related-list-width";
20548
+ var ARROW_STEP = {
20549
+ ArrowUp: { row: -1, col: 0 },
20550
+ ArrowDown: { row: 1, col: 0 },
20551
+ ArrowLeft: { row: 0, col: -1 },
20552
+ ArrowRight: { row: 0, col: 1 }
20553
+ };
20101
20554
  function createTableGrid(callbacks, options = {}) {
20102
20555
  const embedded = options.embedded === true;
20103
20556
  const text3 = () => callbacks.getText?.() ?? dbText("en");
@@ -20222,6 +20675,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20222
20675
  filterRowWrap.appendChild(filterRow);
20223
20676
  const viewport = document.createElement("div");
20224
20677
  viewport.className = "db-grid-viewport";
20678
+ viewport.tabIndex = 0;
20225
20679
  const spacer = document.createElement("div");
20226
20680
  spacer.className = "db-grid-spacer";
20227
20681
  const body = document.createElement("div");
@@ -20325,6 +20779,156 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20325
20779
  function clearActiveCell() {
20326
20780
  setActiveCell(-1, -1);
20327
20781
  }
20782
+ function syncHorizontalScroll() {
20783
+ headerWrap.scrollLeft = viewport.scrollLeft;
20784
+ filterRowWrap.scrollLeft = viewport.scrollLeft;
20785
+ }
20786
+ function focusCell(rowIndex, row, colIndex) {
20787
+ setSelectedRow(rowIndex, row);
20788
+ setActiveCell(rowIndex, colIndex);
20789
+ viewport.focus({ preventScroll: true });
20790
+ }
20791
+ function moveActiveCell(rowIndex, colIndex) {
20792
+ const rendered = body.children[rowIndex - renderStartRow];
20793
+ focusCell(rowIndex, rendered ?? null, colIndex);
20794
+ scrollCellIntoView(rowIndex, colIndex);
20795
+ showDetailForActiveCell();
20796
+ }
20797
+ function scrollCellIntoView(rowIndex, colIndex) {
20798
+ const viewHeight = viewport.clientHeight;
20799
+ if (viewHeight > 0) {
20800
+ const top = rowIndex * ROW_HEIGHT;
20801
+ const bottom = top + ROW_HEIGHT;
20802
+ if (top < viewport.scrollTop) viewport.scrollTop = top;
20803
+ else if (bottom > viewport.scrollTop + viewHeight) {
20804
+ viewport.scrollTop = bottom - viewHeight;
20805
+ }
20806
+ }
20807
+ const viewWidth = viewport.clientWidth;
20808
+ if (viewWidth <= 0) return;
20809
+ let left = ROWNUM_WIDTH;
20810
+ for (let c2 = 0; c2 < colIndex; c2++) left += getColWidth(columnNames[c2]);
20811
+ const right = left + getColWidth(columnNames[colIndex]);
20812
+ if (left < viewport.scrollLeft) {
20813
+ viewport.scrollLeft = colIndex === 0 ? 0 : left;
20814
+ } else if (right > viewport.scrollLeft + viewWidth) {
20815
+ viewport.scrollLeft = right - viewWidth;
20816
+ }
20817
+ }
20818
+ function cachedRow(rowIndex) {
20819
+ const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
20820
+ return getCachedPage(pageStart)?.[rowIndex - pageStart];
20821
+ }
20822
+ function cachedCellValue(rowIndex, colIndex) {
20823
+ const row = cachedRow(rowIndex);
20824
+ return row ? row[colIndex] : void 0;
20825
+ }
20826
+ function showDetailForActiveCell() {
20827
+ const rowIndex = activeCellRowIndex;
20828
+ const colIndex = activeCellColIndex;
20829
+ if (rowIndex < 0 || colIndex < 0) return;
20830
+ const value = cachedCellValue(rowIndex, colIndex);
20831
+ if (value !== void 0) {
20832
+ showCellDetail(colIndex, value);
20833
+ return;
20834
+ }
20835
+ const pageStart = Math.floor(rowIndex / PAGE_SIZE) * PAGE_SIZE;
20836
+ void ensurePage(pageStart).then(() => {
20837
+ if (activeCellRowIndex !== rowIndex || activeCellColIndex !== colIndex) {
20838
+ return;
20839
+ }
20840
+ const loaded = cachedCellValue(rowIndex, colIndex);
20841
+ if (loaded !== void 0) showCellDetail(colIndex, loaded);
20842
+ });
20843
+ }
20844
+ function moveGridFocus(back) {
20845
+ if (back) {
20846
+ if (!embedded || !callbacks.onFocusParentGrid) return false;
20847
+ callbacks.onFocusParentGrid();
20848
+ return true;
20849
+ }
20850
+ if (embedded || !relatedPanel || relatedPanel.hidden || !embeddedGrid) {
20851
+ return false;
20852
+ }
20853
+ embeddedGrid.focusGrid();
20854
+ return true;
20855
+ }
20856
+ function closeOpenPanel() {
20857
+ if (relatedPanel && !relatedPanel.hidden) {
20858
+ hideRelatedPanel();
20859
+ clearActiveCell();
20860
+ return true;
20861
+ }
20862
+ if (!detailPanel.hidden) {
20863
+ detailPanel.hidden = true;
20864
+ clearDetailContent();
20865
+ clearActiveCell();
20866
+ return true;
20867
+ }
20868
+ return false;
20869
+ }
20870
+ function activateActiveCell() {
20871
+ const rowIndex = activeCellRowIndex;
20872
+ const colIndex = activeCellColIndex;
20873
+ if (rowIndex < 0 || colIndex < 0) return false;
20874
+ const rowValues = cachedRow(rowIndex);
20875
+ if (!rowValues) return false;
20876
+ const colName = columnNames[colIndex];
20877
+ const fkClickable = fkColumns.has(colName) && (!embedded || !!callbacks.onForeignKeyCellClick);
20878
+ if (!fkClickable) {
20879
+ showDetailForActiveCell();
20880
+ return true;
20881
+ }
20882
+ if (embedded) {
20883
+ callbacks.onForeignKeyCellClick?.(
20884
+ currentTable,
20885
+ columnNames,
20886
+ rowValues,
20887
+ colName
20888
+ );
20889
+ } else {
20890
+ openRelatedForRow(currentTable, columnNames, rowValues, colName);
20891
+ }
20892
+ return true;
20893
+ }
20894
+ function onViewportKeydown(e2) {
20895
+ if (isImeComposing(e2)) return;
20896
+ if (isEditableKeyTarget(e2.target)) return;
20897
+ if (e2.key === "Tab" && !e2.ctrlKey && !e2.metaKey && !e2.altKey) {
20898
+ if (moveGridFocus(e2.shiftKey)) e2.preventDefault();
20899
+ return;
20900
+ }
20901
+ if (e2.ctrlKey || e2.metaKey || e2.altKey || e2.shiftKey) return;
20902
+ if (e2.key === "Escape") {
20903
+ if (closeOpenPanel()) e2.preventDefault();
20904
+ return;
20905
+ }
20906
+ if (e2.key === "Enter") {
20907
+ if (activateActiveCell()) e2.preventDefault();
20908
+ return;
20909
+ }
20910
+ const step = ARROW_STEP[e2.key];
20911
+ if (!step) return;
20912
+ const lastRow = totalRows - 1;
20913
+ const lastCol = columnNames.length - 1;
20914
+ if (lastRow < 0 || lastCol < 0) return;
20915
+ e2.preventDefault();
20916
+ if (activeCellRowIndex < 0 || activeCellColIndex < 0) {
20917
+ moveActiveCell(Math.min(Math.max(selectedRowIndex, 0), lastRow), 0);
20918
+ return;
20919
+ }
20920
+ const nextRow = Math.min(
20921
+ Math.max(activeCellRowIndex + step.row, 0),
20922
+ lastRow
20923
+ );
20924
+ const nextCol = Math.min(
20925
+ Math.max(activeCellColIndex + step.col, 0),
20926
+ lastCol
20927
+ );
20928
+ if (nextRow === activeCellRowIndex && nextCol === activeCellColIndex)
20929
+ return;
20930
+ moveActiveCell(nextRow, nextCol);
20931
+ }
20328
20932
  function clearDetailContent() {
20329
20933
  for (const child of Array.from(detailPanel.children)) {
20330
20934
  if (child !== detailResize) child.remove();
@@ -20399,6 +21003,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20399
21003
  let editingCellCol = -1;
20400
21004
  let relatedPanel = null;
20401
21005
  let relatedListEl = null;
21006
+ let relatedListResizeEl = null;
20402
21007
  let relatedGridHost = null;
20403
21008
  let relatedEmptyEl = null;
20404
21009
  let relatedCrumbEl = null;
@@ -20412,6 +21017,20 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20412
21017
  RELATED_PANEL_MIN_HEIGHT,
20413
21018
  Math.min(panelMaxHeight(), savedRelatedHeight)
20414
21019
  ) : RELATED_PANEL_DEFAULT_HEIGHT;
21020
+ let relatedListWidth = clampRelatedListWidth(
21021
+ readStoredSize(RELATED_LIST_WIDTH_KEY, RELATED_LIST_DEFAULT_WIDTH)
21022
+ );
21023
+ let relatedListResizeDetach = null;
21024
+ function clampRelatedListWidth(width) {
21025
+ return Math.max(
21026
+ RELATED_LIST_MIN_WIDTH,
21027
+ Math.min(RELATED_LIST_MAX_WIDTH, Math.round(width))
21028
+ );
21029
+ }
21030
+ function applyRelatedListWidth(width) {
21031
+ relatedListWidth = clampRelatedListWidth(width);
21032
+ el2.style.setProperty("--db-related-list-w", `${relatedListWidth}px`);
21033
+ }
20415
21034
  if (!embedded) {
20416
21035
  relatedPanel = document.createElement("div");
20417
21036
  relatedPanel.className = "db-related-panel";
@@ -20591,6 +21210,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20591
21210
  function resetSelectionAndDetail() {
20592
21211
  selectedRowIndex = -1;
20593
21212
  selectedRowElement = null;
21213
+ clearActiveCell();
20594
21214
  detailPanel.hidden = true;
20595
21215
  clearDetailContent();
20596
21216
  }
@@ -20797,6 +21417,24 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20797
21417
  bodyRow.className = "db-related-body";
20798
21418
  relatedListEl = document.createElement("div");
20799
21419
  relatedListEl.className = "db-related-list";
21420
+ const listResize = document.createElement("div");
21421
+ listResize.className = "db-related-list-resize";
21422
+ listResize.tabIndex = 0;
21423
+ listResize.setAttribute("role", "separator");
21424
+ listResize.setAttribute("aria-orientation", "vertical");
21425
+ listResize.setAttribute("aria-label", text3().grid.relatedListResize);
21426
+ relatedListResizeEl = listResize;
21427
+ applyRelatedListWidth(relatedListWidth);
21428
+ relatedListResizeDetach = attachDragResizer({
21429
+ handle: listResize,
21430
+ getSize: () => relatedListWidth,
21431
+ applySize: applyRelatedListWidth,
21432
+ direction: 1,
21433
+ axis: "x",
21434
+ onEnd: () => writeStoredSize(RELATED_LIST_WIDTH_KEY, relatedListWidth),
21435
+ activeClassTarget: relatedPanel,
21436
+ activeClassName: "db-related-list-resizing"
21437
+ });
20800
21438
  relatedGridHost = document.createElement("div");
20801
21439
  relatedGridHost.className = "db-related-grid-host";
20802
21440
  embeddedGrid = createTableGrid(
@@ -20817,7 +21455,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20817
21455
  getBaseEq: () => relatedEq,
20818
21456
  getText: callbacks.getText,
20819
21457
  // 埋め込みグリッドの FK クリックで 1 段潜る。
20820
- onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked)
21458
+ onForeignKeyCellClick: (sourceTable, colNames, rowData, clicked) => drillIntoRelated(sourceTable, colNames, rowData, clicked),
21459
+ // 埋め込み側で Shift+Tab を押したらメイングリッドへ戻す。
21460
+ onFocusParentGrid: () => viewport.focus({ preventScroll: true })
20821
21461
  },
20822
21462
  { embedded: true }
20823
21463
  );
@@ -20827,7 +21467,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20827
21467
  relatedEmptyEl.textContent = text3().grid.relatedEmpty;
20828
21468
  relatedEmptyEl.hidden = true;
20829
21469
  relatedGridHost.appendChild(relatedEmptyEl);
20830
- bodyRow.append(relatedListEl, relatedGridHost);
21470
+ bodyRow.append(relatedListEl, listResize, relatedGridHost);
20831
21471
  relatedPanel.append(resizer, header, bodyRow);
20832
21472
  }
20833
21473
  function startRelatedResize(e2) {
@@ -20945,7 +21585,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20945
21585
  if (i2 === level.selectedIndex) item.classList.add("active");
20946
21586
  const name = document.createElement("span");
20947
21587
  name.className = "db-related-list-name";
20948
- name.textContent = relatedDrillTable(target);
21588
+ const tableName = document.createElement("span");
21589
+ tableName.className = "db-related-list-table";
21590
+ tableName.textContent = relatedDrillTable(target);
21591
+ tableName.title = relatedDrillTable(target);
21592
+ name.appendChild(tableName);
20949
21593
  if (target.fk.inferred) {
20950
21594
  const badge = document.createElement("span");
20951
21595
  badge.className = "db-related-list-inferred-badge";
@@ -20955,7 +21599,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20955
21599
  }
20956
21600
  const via = document.createElement("span");
20957
21601
  via.className = "db-related-list-via";
20958
- via.textContent = target.direction === "outgoing" ? `${target.fk.fromColumn} = ${target.value}` : `${target.fk.fromTable}.${target.fk.fromColumn} = ${target.value}`;
21602
+ const condition = target.direction === "outgoing" ? `${target.fk.fromColumn} = ${target.value}` : `${target.fk.fromTable}.${target.fk.fromColumn} = ${target.value}`;
21603
+ via.textContent = condition;
21604
+ via.title = condition;
20959
21605
  item.append(name, via);
20960
21606
  item.addEventListener("click", () => selectRelatedTarget(i2));
20961
21607
  relatedListEl?.appendChild(item);
@@ -20992,9 +21638,42 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
20992
21638
  grid.showError(err instanceof Error ? err.message : String(err));
20993
21639
  }
20994
21640
  }
21641
+ let jsonHighlighter = null;
21642
+ let jsonHighlighterRequested = false;
21643
+ let detailJsonPre = null;
21644
+ let detailJsonText = "";
21645
+ function paintJsonHighlight(pre, json) {
21646
+ if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return false;
21647
+ const inner = highlightToInnerHtml(json, "json", jsonHighlighter);
21648
+ if (!inner) return false;
21649
+ pre.innerHTML = inner;
21650
+ return true;
21651
+ }
21652
+ function showJsonDetail(pre, json) {
21653
+ detailJsonPre = pre;
21654
+ detailJsonText = json;
21655
+ if (paintJsonHighlight(pre, json)) return;
21656
+ pre.textContent = json;
21657
+ if (json.length > DETAIL_JSON_HIGHLIGHT_MAX_CHARS) return;
21658
+ ensureJsonHighlighter();
21659
+ }
21660
+ function ensureJsonHighlighter() {
21661
+ if (jsonHighlighterRequested) return;
21662
+ jsonHighlighterRequested = true;
21663
+ void loadShikiHighlighter({
21664
+ themes: ["github-light", "github-dark"],
21665
+ langs: ["json"]
21666
+ }).then((highlighter) => {
21667
+ jsonHighlighter = highlighter;
21668
+ if (!highlighter || !detailJsonPre?.isConnected) return;
21669
+ paintJsonHighlight(detailJsonPre, detailJsonText);
21670
+ });
21671
+ }
20995
21672
  function showCellDetail(colIndex, value) {
20996
21673
  const colName = columnNames[colIndex];
20997
21674
  const colType = columns[colIndex]?.type || "";
21675
+ detailJsonPre = null;
21676
+ detailJsonText = "";
20998
21677
  hideRelatedPanel();
20999
21678
  detailPanel.hidden = false;
21000
21679
  clearDetailContent();
@@ -21046,7 +21725,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21046
21725
  const parsed = JSON.parse(str);
21047
21726
  const pre = document.createElement("pre");
21048
21727
  pre.className = "db-grid-detail-json";
21049
- pre.textContent = JSON.stringify(parsed, null, 2);
21728
+ showJsonDetail(pre, JSON.stringify(parsed, null, 2));
21050
21729
  content.appendChild(pre);
21051
21730
  } catch {
21052
21731
  content.textContent = str;
@@ -21111,6 +21790,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21111
21790
  }
21112
21791
  renderFilterRow();
21113
21792
  syncContentWidth();
21793
+ syncHorizontalScroll();
21114
21794
  }
21115
21795
  function startResize(colIndex, startEvent) {
21116
21796
  cleanupResize();
@@ -21191,9 +21871,27 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21191
21871
  invalidateData();
21192
21872
  }, FILTER_DEBOUNCE_MS);
21193
21873
  }
21874
+ let scrollbarGutterPx = -1;
21875
+ function syncScrollbarGutter() {
21876
+ const outer = viewport.offsetWidth;
21877
+ const inner = viewport.clientWidth;
21878
+ if (!(outer > 0) || !(inner > 0)) return;
21879
+ const gutter = outer - inner;
21880
+ if (gutter === scrollbarGutterPx) return;
21881
+ scrollbarGutterPx = gutter;
21882
+ el2.style.setProperty("--db-grid-scrollbar-w", `${gutter}px`);
21883
+ }
21884
+ function measureContentWidth() {
21885
+ let total = 0;
21886
+ for (const cell of headerRow.children) {
21887
+ total += cell.getBoundingClientRect().width;
21888
+ }
21889
+ return total;
21890
+ }
21194
21891
  function syncContentWidth() {
21195
21892
  requestAnimationFrame(() => {
21196
- const w = headerRow.scrollWidth;
21893
+ syncScrollbarGutter();
21894
+ const w = measureContentWidth();
21197
21895
  if (w > 0) {
21198
21896
  spacer.style.minWidth = `${w}px`;
21199
21897
  body.style.minWidth = `${w}px`;
@@ -21393,8 +22091,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21393
22091
  const fkClickable = fkColumns.has(cellColName) && (!embedded || !!callbacks.onForeignKeyCellClick);
21394
22092
  const isEditingThisCell = !ro && i2 === editingCellRow && c2 === editingCellCol;
21395
22093
  const activate = () => {
21396
- setSelectedRow(rowIndex, row);
21397
- setActiveCell(rowIndex, cellColIndex);
22094
+ focusCell(rowIndex, row, cellColIndex);
21398
22095
  if (fkClickable) {
21399
22096
  if (embedded) {
21400
22097
  callbacks.onForeignKeyCellClick?.(
@@ -21486,8 +22183,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21486
22183
  }
21487
22184
  cell.addEventListener("click", (e2) => {
21488
22185
  e2.stopPropagation();
21489
- setSelectedRow(rowIndex, row);
21490
- setActiveCell(rowIndex, cellColIndex);
22186
+ focusCell(rowIndex, row, cellColIndex);
21491
22187
  if (fkClickable) {
21492
22188
  if (embedded) {
21493
22189
  callbacks.onForeignKeyCellClick?.(
@@ -21597,6 +22293,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21597
22293
  );
21598
22294
  }
21599
22295
  syncFilteredEmptyState();
22296
+ syncHorizontalScroll();
21600
22297
  if (focusRestore) {
21601
22298
  const next = body.querySelector(
21602
22299
  `.db-grid-cell-input[data-edit-row="${focusRestore.row}"][data-edit-col="${focusRestore.col}"]`
@@ -21762,11 +22459,22 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21762
22459
  void refreshCurrentTable();
21763
22460
  });
21764
22461
  const onViewportScroll = () => {
21765
- headerWrap.scrollLeft = viewport.scrollLeft;
21766
- filterRowWrap.scrollLeft = viewport.scrollLeft;
22462
+ syncHorizontalScroll();
21767
22463
  renderViewport();
21768
22464
  };
22465
+ const onWrapScroll = (wrap) => () => {
22466
+ if (wrap.scrollLeft === viewport.scrollLeft) return;
22467
+ viewport.scrollLeft = wrap.scrollLeft;
22468
+ syncHorizontalScroll();
22469
+ };
22470
+ const onHeaderWrapScroll = onWrapScroll(headerWrap);
22471
+ const onFilterWrapScroll = onWrapScroll(filterRowWrap);
21769
22472
  viewport.addEventListener("scroll", onViewportScroll, { passive: true });
22473
+ viewport.addEventListener("keydown", onViewportKeydown);
22474
+ headerWrap.addEventListener("scroll", onHeaderWrapScroll, { passive: true });
22475
+ filterRowWrap.addEventListener("scroll", onFilterWrapScroll, {
22476
+ passive: true
22477
+ });
21770
22478
  const onCompositionStart = () => {
21771
22479
  isComposing = true;
21772
22480
  };
@@ -21780,7 +22488,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21780
22488
  clear();
21781
22489
  embeddedGrid?.destroy();
21782
22490
  embeddedGrid = null;
22491
+ relatedListResizeDetach?.();
22492
+ relatedListResizeDetach = null;
21783
22493
  viewport.removeEventListener("scroll", onViewportScroll);
22494
+ viewport.removeEventListener("keydown", onViewportKeydown);
22495
+ headerWrap.removeEventListener("scroll", onHeaderWrapScroll);
22496
+ filterRowWrap.removeEventListener("scroll", onFilterWrapScroll);
21784
22497
  body.removeEventListener("compositionstart", onCompositionStart);
21785
22498
  body.removeEventListener("compositionend", onCompositionEnd);
21786
22499
  detailResizeCleanup?.();
@@ -21804,6 +22517,10 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
21804
22517
  updateStatus();
21805
22518
  }
21806
22519
  renderRelatedCrumbs();
22520
+ renderRelatedList();
22521
+ if (relatedListResizeEl) {
22522
+ relatedListResizeEl.setAttribute("aria-label", t2.grid.relatedListResize);
22523
+ }
21807
22524
  embeddedGrid?.localize();
21808
22525
  }
21809
22526
  function rebuildFkColumnsForCurrentTable() {
@@ -22053,6 +22770,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
22053
22770
  }
22054
22771
  return {
22055
22772
  el: el2,
22773
+ focusGrid: () => viewport.focus({ preventScroll: true }),
22056
22774
  load,
22057
22775
  refresh: refreshCurrentTable,
22058
22776
  showError,
@@ -27022,50 +27740,6 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27022
27740
  deps.setStatus("live");
27023
27741
  }
27024
27742
 
27025
- // web-src/core/history.ts
27026
- var EMPTY_TREE_SHA = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
27027
- var HISTORY_PAGE_SIZE = 50;
27028
- var HISTORY_AUTO_LOAD_MAX_PAGES = 20;
27029
- function commitDiffRange(commit) {
27030
- return { from: commit.parents[0] || EMPTY_TREE_SHA, to: commit.sha };
27031
- }
27032
- function shouldContinueAutoLoad(state) {
27033
- if (state.found) return false;
27034
- if (!state.hasMore) return false;
27035
- return state.pagesLoaded < HISTORY_AUTO_LOAD_MAX_PAGES;
27036
- }
27037
- var MONTH_NAMES = [
27038
- "January",
27039
- "February",
27040
- "March",
27041
- "April",
27042
- "May",
27043
- "June",
27044
- "July",
27045
- "August",
27046
- "September",
27047
- "October",
27048
- "November",
27049
- "December"
27050
- ];
27051
- var DAY_MS = 24 * 60 * 60 * 1e3;
27052
- function historyGroupLabel(whenIso, now) {
27053
- const t2 = Date.parse(whenIso);
27054
- if (!Number.isFinite(t2)) return "Unknown date";
27055
- const dayStart = new Date(
27056
- now.getFullYear(),
27057
- now.getMonth(),
27058
- now.getDate()
27059
- ).getTime();
27060
- if (t2 >= dayStart) return "Today";
27061
- if (t2 >= dayStart - DAY_MS) return "Yesterday";
27062
- if (t2 >= dayStart - 6 * DAY_MS) return "This week";
27063
- const d2 = new Date(t2);
27064
- if (d2.getFullYear() === now.getFullYear() && d2.getMonth() === now.getMonth())
27065
- return "This month";
27066
- return `${MONTH_NAMES[d2.getMonth()]} ${d2.getFullYear()}`;
27067
- }
27068
-
27069
27743
  // web-src/views/history-view.ts
27070
27744
  var HISTORY_BODY_COLLAPSE_LINES = 10;
27071
27745
  var HISTORY_WORKTREE_COMMIT = "worktree";
@@ -27078,7 +27752,21 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27078
27752
  refreshTitle: "Refresh commit history",
27079
27753
  refreshTitlePending: "History may have changed. Refresh",
27080
27754
  filterClearLabel: "Clear",
27081
- 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}`
27082
27770
  },
27083
27771
  ja: {
27084
27772
  worktreeLabel: "未コミット変更 (Working tree)",
@@ -27088,7 +27776,21 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27088
27776
  refreshTitle: "コミット履歴を更新",
27089
27777
  refreshTitlePending: "新しい履歴がある可能性があります。更新",
27090
27778
  filterClearLabel: "解除",
27091
- 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}`
27092
27794
  }
27093
27795
  };
27094
27796
  function historyText(lang) {
@@ -27108,6 +27810,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27108
27810
  panel.className = "gdp-file-history-panel";
27109
27811
  }
27110
27812
  panel.setAttribute("aria-label", "Commit history");
27813
+ panel.tabIndex = -1;
27111
27814
  const panelHead = document.createElement("div");
27112
27815
  panelHead.className = "history-head";
27113
27816
  const title = document.createElement("span");
@@ -27143,8 +27846,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27143
27846
  if (page) filterInput.id = "history-filter";
27144
27847
  else filterInput.className = "history-filter";
27145
27848
  filterInput.type = "search";
27146
- 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;
27147
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);
27148
27855
  const filterClearButton = document.createElement("button");
27149
27856
  filterClearButton.type = "button";
27150
27857
  if (page) filterClearButton.id = "history-filter-clear";
@@ -27156,7 +27863,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27156
27863
  "aria-label",
27157
27864
  HISTORY_TEXT.en.filterClearTitle
27158
27865
  );
27159
- filterWrap.append(filterInput, filterClearButton);
27866
+ filterWrap.append(filterInput, filterClearButton, authorList);
27160
27867
  const banner = document.createElement("div");
27161
27868
  if (page) banner.id = "history-banner";
27162
27869
  banner.className = "history-banner";
@@ -27183,6 +27890,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27183
27890
  sentinel,
27184
27891
  filterInput,
27185
27892
  filterClearButton,
27893
+ authorList,
27186
27894
  refreshButton,
27187
27895
  refreshResult
27188
27896
  };
@@ -27206,7 +27914,9 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27206
27914
  const date = document.createElement("span");
27207
27915
  if (page) date.id = "hci-date";
27208
27916
  date.className = "hci-date";
27209
- head.append(sha, author, date);
27917
+ const actions = document.createElement("span");
27918
+ actions.className = "hci-actions";
27919
+ head.append(sha, author, date, actions);
27210
27920
  const subject = document.createElement("h2");
27211
27921
  if (page) subject.id = "hci-subject";
27212
27922
  subject.className = "hci-subject";
@@ -27308,6 +28018,12 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27308
28018
  let pathFilter = "";
27309
28019
  let refreshStatus = { type: "none" };
27310
28020
  let freshSha = "";
28021
+ let compareSha = "";
28022
+ let authorsLoadedFor = "";
28023
+ let lineRange2;
28024
+ function lineRangeKey(range) {
28025
+ return range ? formatHistoryLineRange(range) : "";
28026
+ }
27311
28027
  function historyScopeFromRoute(route = deps.getRoute()) {
27312
28028
  if (route.screen === "history") {
27313
28029
  const nextRef = route.ref || "HEAD";
@@ -27315,8 +28031,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27315
28031
  mode: "history",
27316
28032
  logRef: nextRef,
27317
28033
  routeRef: nextRef,
27318
- pathFilter: "",
27319
- 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
27320
28039
  };
27321
28040
  }
27322
28041
  if (route.screen === "file" && route.view === "history") {
@@ -27326,11 +28045,51 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27326
28045
  logRef: nextRouteRef === "worktree" ? "HEAD" : nextRouteRef,
27327
28046
  routeRef: nextRouteRef,
27328
28047
  pathFilter: route.path,
27329
- commit: route.commit
28048
+ query: route.q || "",
28049
+ lines: route.lines,
28050
+ commit: route.commit,
28051
+ compare: route.compare
27330
28052
  };
27331
28053
  }
27332
28054
  return null;
27333
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
+ }
27334
28093
  function currentRefreshScopeKey() {
27335
28094
  const scope = historyScopeFromRoute();
27336
28095
  if (!scope) return "";
@@ -27339,6 +28098,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27339
28098
  scope.logRef,
27340
28099
  scope.routeRef,
27341
28100
  scope.pathFilter,
28101
+ lineRangeKey(scope.lines),
27342
28102
  query
27343
28103
  ].join("\0");
27344
28104
  }
@@ -27420,6 +28180,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27420
28180
  if (query) params.set("q", query);
27421
28181
  if (pathFilter) {
27422
28182
  params.set("path", pathFilter);
28183
+ if (lineRange2) params.set("lines", formatHistoryLineRange(lineRange2));
27423
28184
  if (routeRef === "worktree") params.set("worktree", "1");
27424
28185
  }
27425
28186
  const url = `/_log?${params.toString()}`;
@@ -27436,10 +28197,43 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27436
28197
  return null;
27437
28198
  });
27438
28199
  }
27439
- 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) {
27440
28221
  const active = commit.sha === selectedSha ? " active" : "";
27441
28222
  const fresh = commit.sha === freshSha ? " history-item-fresh" : "";
27442
- 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;
27443
28237
  }
27444
28238
  function worktreeRow() {
27445
28239
  const active = selectedSha === HISTORY_WORKTREE_COMMIT ? " active" : "";
@@ -27450,6 +28244,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27450
28244
  syncRefreshResult(activeMount.refreshResult);
27451
28245
  const now = /* @__PURE__ */ new Date();
27452
28246
  const html = mode === "history" ? [worktreeRow()] : [];
28247
+ const inRange = rangeShas();
27453
28248
  let lastGroup = "";
27454
28249
  for (const commit of commits) {
27455
28250
  if (commit.sha === HISTORY_WORKTREE_COMMIT) {
@@ -27463,7 +28258,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27463
28258
  );
27464
28259
  lastGroup = group;
27465
28260
  }
27466
- html.push(commitRow(commit));
28261
+ html.push(commitRow(commit, inRange.has(commit.sha)));
27467
28262
  }
27468
28263
  list2.innerHTML = html.join("");
27469
28264
  activeHistoryRow = list2.querySelector(".history-item.active");
@@ -27513,6 +28308,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27513
28308
  info.querySelector(".hci-head")?.removeAttribute("hidden");
27514
28309
  set2(".hci-sha", commit.sha);
27515
28310
  set2(".hci-author", commit.author);
28311
+ renderCommitActions(info, commit);
27516
28312
  const t2 = Date.parse(commit.when);
27517
28313
  set2(
27518
28314
  ".hci-date",
@@ -27539,6 +28335,80 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27539
28335
  }
27540
28336
  info.hidden = false;
27541
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
+ }
27542
28412
  function updateWorktreeInfo() {
27543
28413
  const info = commitInfoElement();
27544
28414
  if (!info) return;
@@ -27564,11 +28434,16 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27564
28434
  }
27565
28435
  activeHistoryRow = selectedSha ? list2.querySelector(historyItemSelector(selectedSha)) : null;
27566
28436
  activeHistoryRow?.classList.add("active");
28437
+ updateRangeRows();
27567
28438
  }
27568
- function isEditableTarget(target) {
27569
- return typeof Element === "function" && target instanceof Element && !!target.closest(
27570
- 'input, textarea, select, button, [contenteditable="true"]'
27571
- );
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
+ });
27572
28447
  }
27573
28448
  function selectableShas() {
27574
28449
  const shas = mode === "history" ? [HISTORY_WORKTREE_COMMIT] : [];
@@ -27630,63 +28505,46 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27630
28505
  const selectionGen = ++selectionGeneration;
27631
28506
  const gen = generation;
27632
28507
  selectedSha = commit.sha;
28508
+ compareSha = options.compare ?? "";
28509
+ if (compareSha === commit.parents[0]) compareSha = "";
27633
28510
  updateActiveRow();
27634
28511
  await updateCommitInfo(commit);
27635
28512
  if (selectionGen !== selectionGeneration || gen !== generation) return;
28513
+ const range = compareSha ? { from: compareSha, to: commit.sha } : commitDiffRange(commit);
27636
28514
  if (options.updateUrl !== false) {
27637
- const range = commitDiffRange(commit);
27638
- if (mode === "file") {
27639
- deps.setRoute(
27640
- {
27641
- screen: "file",
27642
- path: pathFilter,
27643
- ref: routeRef,
27644
- view: "history",
27645
- commit: commit.sha,
27646
- range
27647
- },
27648
- true
27649
- );
27650
- } else {
27651
- deps.setRoute(
27652
- { screen: "history", ref, commit: commit.sha, range },
27653
- true
27654
- );
27655
- }
28515
+ deps.setRoute(
28516
+ routeFor({ commit: commit.sha, compare: compareSha }),
28517
+ true
28518
+ );
27656
28519
  }
27657
28520
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27658
- await deps.applyCommitRange(
27659
- commitDiffRange(commit),
27660
- pathFilter || void 0
27661
- );
28521
+ await deps.applyCommitRange(range, pathFilter || void 0);
27662
28522
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27663
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
+ }
27664
28538
  async function selectWorktree(options = {}) {
27665
28539
  const selectionGen = ++selectionGeneration;
27666
28540
  const gen = generation;
27667
28541
  selectedSha = HISTORY_WORKTREE_COMMIT;
28542
+ compareSha = "";
27668
28543
  updateActiveRow();
27669
28544
  updateWorktreeInfo();
27670
28545
  const range = worktreeDiffRange();
27671
28546
  if (options.updateUrl !== false) {
27672
- if (mode === "file") {
27673
- deps.setRoute(
27674
- {
27675
- screen: "file",
27676
- path: pathFilter,
27677
- ref: routeRef,
27678
- view: "history",
27679
- commit: selectedSha,
27680
- range
27681
- },
27682
- true
27683
- );
27684
- } else {
27685
- deps.setRoute(
27686
- { screen: "history", ref, commit: selectedSha, range },
27687
- true
27688
- );
27689
- }
28547
+ deps.setRoute(routeFor({ commit: selectedSha }), true);
27690
28548
  }
27691
28549
  if (selectionGen !== selectionGeneration || gen !== generation) return;
27692
28550
  await deps.applyCommitRange(range, pathFilter || void 0);
@@ -27724,10 +28582,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27724
28582
  } else {
27725
28583
  pagesLoaded = 1;
27726
28584
  }
28585
+ const compare = historyScopeFromRoute()?.compare || "";
27727
28586
  for (; ; ) {
27728
28587
  const found = commits.find((c2) => c2.sha.startsWith(sha));
27729
28588
  if (found) {
27730
- await selectCommit(found, { updateUrl: false });
28589
+ await selectCommit(found, { updateUrl: false, compare });
27731
28590
  scrollToSelected();
27732
28591
  return;
27733
28592
  }
@@ -27751,7 +28610,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27751
28610
  setBanner(`showing commit outside the loaded ${ref} log`);
27752
28611
  commits = [single, ...commits];
27753
28612
  renderList();
27754
- await selectCommit(single, { updateUrl: false });
28613
+ await selectCommit(single, { updateUrl: false, compare });
27755
28614
  scrollToSelected();
27756
28615
  }
27757
28616
  function scrollToSelected() {
@@ -27768,14 +28627,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27768
28627
  async function doEnterHistory(force = false) {
27769
28628
  const scope = historyScopeFromRoute();
27770
28629
  if (!scope) return;
27771
- 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;
27772
28631
  if (scopeChanged || force || commits.length === 0) {
27773
28632
  generation++;
27774
28633
  const gen = generation;
27775
28634
  ref = scope.logRef;
27776
28635
  routeRef = scope.routeRef;
27777
28636
  pathFilter = scope.pathFilter;
28637
+ lineRange2 = scope.lines;
28638
+ query = scope.query;
27778
28639
  mode = scope.mode;
28640
+ compareSha = "";
28641
+ syncFilterInputValue();
28642
+ syncPanelTitle();
27779
28643
  commits = [];
27780
28644
  hasMore = false;
27781
28645
  loading = false;
@@ -27805,33 +28669,19 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27805
28669
  }
27806
28670
  function onRefPicked(nextRef) {
27807
28671
  const value = nextRef && nextRef !== "worktree" ? nextRef : "HEAD";
27808
- if (mode === "file" && pathFilter) {
27809
- deps.setRoute(
27810
- {
27811
- screen: "file",
27812
- path: pathFilter,
27813
- ref: nextRef || "worktree",
27814
- view: "history",
27815
- range: { from: "HEAD", to: "worktree" }
27816
- },
27817
- false
27818
- );
27819
- } else {
27820
- deps.setRoute(
27821
- {
27822
- screen: "history",
27823
- ref: value,
27824
- range: { from: "HEAD", to: "worktree" }
27825
- },
27826
- false
27827
- );
27828
- }
28672
+ deps.setRoute(
28673
+ routeFor({
28674
+ ref: mode === "file" ? nextRef || "worktree" : value
28675
+ }),
28676
+ false
28677
+ );
27829
28678
  void enterHistory({ force: true });
27830
28679
  }
27831
28680
  function leaveHistory() {
27832
28681
  generation++;
27833
28682
  loading = false;
27834
28683
  inFlight = null;
28684
+ compareSha = "";
27835
28685
  if (filterTimer) {
27836
28686
  clearTimeout(filterTimer);
27837
28687
  filterTimer = void 0;
@@ -27846,17 +28696,33 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27846
28696
  function handleListClick(e2) {
27847
28697
  const row = e2.target.closest(".history-item");
27848
28698
  if (!row?.dataset.sha) return;
28699
+ panel.focus?.();
27849
28700
  if (row.dataset.sha === HISTORY_WORKTREE_COMMIT) {
27850
28701
  void selectWorktree();
27851
28702
  return;
27852
28703
  }
27853
28704
  const commit = commits.find((c2) => c2.sha === row.dataset.sha);
27854
- 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: "" });
27855
28711
  }
27856
28712
  function applyFilter(next) {
27857
28713
  const value = next.trim();
27858
28714
  if (value === query) return;
27859
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
+ }
27860
28726
  generation++;
27861
28727
  selectionGeneration++;
27862
28728
  clearRefreshResult();
@@ -27945,6 +28811,7 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27945
28811
  attachedFilterInput.removeEventListener("input", handleFilterInput);
27946
28812
  attachedFilterInput.removeEventListener("change", handleFilterInput);
27947
28813
  attachedFilterInput.removeEventListener("keydown", handleFilterKeydown);
28814
+ attachedFilterInput.removeEventListener("focus", handleFilterFocus);
27948
28815
  attachedFilterInput = null;
27949
28816
  }
27950
28817
  if (attachedFilterClearButton) {
@@ -27977,8 +28844,11 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
27977
28844
  input2.addEventListener("input", handleFilterInput);
27978
28845
  input2.addEventListener("change", handleFilterInput);
27979
28846
  input2.addEventListener("keydown", handleFilterKeydown);
28847
+ input2.addEventListener("focus", handleFilterFocus);
27980
28848
  attachedFilterInput = input2;
27981
28849
  }
28850
+ syncPanelTitle();
28851
+ syncFilterChrome();
27982
28852
  const filterClearButton = mount.filterClearButton ?? null;
27983
28853
  if (filterClearButton) {
27984
28854
  syncFilterClearButton(filterClearButton);
@@ -28002,14 +28872,50 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28002
28872
  );
28003
28873
  observer.observe(sentinel);
28004
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
+ }
28005
28918
  const docWithEvents = document;
28006
- docWithEvents.addEventListener?.("keydown", (e2) => {
28007
- if (isImeComposing(e2) || isEditableTarget(e2.target)) return;
28008
- if (e2.key !== "ArrowDown" && e2.key !== "ArrowUp") return;
28009
- if (!historyScopeFromRoute()) return;
28010
- e2.preventDefault();
28011
- void moveSelection(e2.key === "ArrowDown" ? 1 : -1);
28012
- });
28013
28919
  docWithEvents.addEventListener?.("input", (e2) => {
28014
28920
  if (e2.target !== activeMount.filterInput) return;
28015
28921
  handleFilterInput(e2);
@@ -28027,8 +28933,13 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28027
28933
  syncRefreshButton(activeMount.refreshButton);
28028
28934
  syncRefreshResult(activeMount.refreshResult);
28029
28935
  syncFilterClearButton(activeMount.filterClearButton);
28936
+ syncFilterChrome();
28937
+ syncPanelTitle();
28030
28938
  renderList();
28031
28939
  },
28940
+ // Keyboard stepping through the commit list (keymap actions
28941
+ // history-next-commit / history-previous-commit).
28942
+ moveCommitSelection: (delta) => moveSelection(delta),
28032
28943
  // Called from the SSE "update" listener when a history panel is on
28033
28944
  // screen. Only updates existing local UI hints — no fetch, no list redraw,
28034
28945
  // no generation bump, so it can't race with trackLoad/cancelInFlightRequests.
@@ -28284,8 +29195,28 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28284
29195
  {
28285
29196
  selectors: [{ action: "goto-history" }],
28286
29197
  description: {
28287
- en: "Go to the history screen",
28288
- 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: "同上(コミット一覧にフォーカスがあるとき)"
28289
29220
  }
28290
29221
  },
28291
29222
  {
@@ -28968,6 +29899,23 @@ ${entry.message}` : entry.detail ?? entry.message ?? "";
28968
29899
  }
28969
29900
  ]
28970
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
+ },
28971
29919
  {
28972
29920
  title: "Scratch on pasted text",
28973
29921
  blocks: [
@@ -29342,7 +30290,7 @@ code-viewer annotate add-db --db app.db --tab query \\
29342
30290
  ],
29343
30291
  [
29344
30292
  "Detail footer & related panel",
29345
- "Click any cell to open a resizable detail footer. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation."
30293
+ "Click any cell to open a resizable detail footer; JSON values are pretty-printed and syntax-highlighted there. Once the grid has focus, arrow keys move the active cell from data cell to data cell and the footer follows the value under it, scrolling only as far as needed; Enter follows a foreign key (arrow keys alone never fire a related-table query), Escape closes whichever panel is open, and Tab / Shift+Tab move between the main grid and the related grid. Foreign-key cells open a related-rows panel with multi-step drill-down breadcrumbs, supporting both outgoing (FK → PK) and incoming (PK ← FK) navigation. Its reference list keeps each entry on one line with the full table name and condition in a tooltip, and the list width can be dragged and is remembered."
29346
30294
  ],
29347
30295
  [
29348
30296
  "Schema tab",
@@ -29735,6 +30683,23 @@ code-viewer annotate add-db --db app.db --tab query \\
29735
30683
  }
29736
30684
  ]
29737
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
+ },
29738
30703
  {
29739
30704
  title: "貼り付けたテキストを扱う",
29740
30705
  blocks: [
@@ -30109,7 +31074,7 @@ code-viewer annotate add-db --db app.db --tab query \\
30109
31074
  ],
30110
31075
  [
30111
31076
  "詳細フッタ・関連パネル",
30112
- "セルをクリックするとリサイズ可能な詳細フッタが開きます。外部キー値からは関連行パネルが開き、ブレッドクラム付きで多段ドリルダウン可能。outgoing (FK→PK) と incoming (PK←FK) の両方向に対応。"
31077
+ "セルをクリックするとリサイズ可能な詳細フッタが開きます。JSON 値は整形してシンタックスハイライト付きで表示されます。グリッドにフォーカスがある間は矢印キーでデータセル間を移動でき、詳細フッタもその値に追従します(スクロールは見える位置まで必要なぶんだけ)。Enter で外部キーを辿り(矢印キーだけでは関連テーブルへのクエリは飛びません)、Escape で開いているパネルを閉じ、Tab / Shift+Tab でメイングリッドと関連グリッドを行き来できます。外部キー値からは関連行パネルが開き、ブレッドクラム付きで多段ドリルダウン可能。outgoing (FK→PK) と incoming (PK←FK) の両方向に対応。左の参照リストは各項目を1行に保ち、テーブル名と条件の全文は tooltip で確認できます。リスト幅はドラッグで変更でき、次回も保持されます。"
30113
31078
  ],
30114
31079
  [
30115
31080
  "Schema タブ",
@@ -33094,7 +34059,12 @@ code-viewer annotate add-db --db app.db --tab query \\
33094
34059
  githubCopy.type = "button";
33095
34060
  githubCopy.innerHTML = `<span class="lrp-github-copy-icon">${iconSvg("octicon-copy", COPY_16_PATHS)}</span><span class="lrp-github-copy-label"></span>`;
33096
34061
  githubActions.append(githubOpen, githubCopy);
33097
- 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);
33098
34068
  document.body.appendChild(pill);
33099
34069
  let refText = "";
33100
34070
  let currentPath = "";
@@ -33103,6 +34073,14 @@ code-viewer annotate add-db --db app.db --tab query \\
33103
34073
  let githubUrl = "";
33104
34074
  let feedbackTimer = null;
33105
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
+ }
33106
34084
  function renderGithubActions() {
33107
34085
  const openTitle = deps.githubOpenTitle();
33108
34086
  const copyTitle = deps.githubCopyTitle();
@@ -33194,6 +34172,10 @@ code-viewer annotate add-db --db app.db --tab query \\
33194
34172
  if (icon) icon.innerHTML = iconSvg("octicon-copy", COPY_16_PATHS);
33195
34173
  }, 1200);
33196
34174
  });
34175
+ historyButton.addEventListener("click", () => {
34176
+ if (!currentPath || !deps.openLineHistory) return;
34177
+ deps.openLineHistory(currentPath, currentStart, currentEnd);
34178
+ });
33197
34179
  closeButton.addEventListener("click", () => {
33198
34180
  deps.onClose();
33199
34181
  });
@@ -33208,6 +34190,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33208
34190
  currentEnd = Math.max(1, Math.floor(Math.max(start, end)));
33209
34191
  githubUrl = deps.githubUrlForSelection(currentPath, currentStart, currentEnd) || "";
33210
34192
  renderGithubActions();
34193
+ renderHistoryAction();
33211
34194
  if (feedbackTimer) {
33212
34195
  clearTimeout(feedbackTimer);
33213
34196
  feedbackTimer = null;
@@ -33315,6 +34298,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33315
34298
  }
33316
34299
 
33317
34300
  // web-src/views/ref-picker.ts
34301
+ var QUICK_REF_VALUES = /* @__PURE__ */ new Set(["worktree", "HEAD", "--staged"]);
33318
34302
  function createRefPicker(deps) {
33319
34303
  function wireRefSelectorInput(input2, onPick) {
33320
34304
  const wrap = input2.closest("[data-ref-selector]");
@@ -33334,6 +34318,37 @@ code-viewer annotate add-db --db app.db --tab query \\
33334
34318
  });
33335
34319
  if (onPick)
33336
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
+ }
33337
34352
  }
33338
34353
  const REFS = {
33339
34354
  branches: [],
@@ -33558,6 +34573,7 @@ code-viewer annotate add-db --db app.db --tab query \\
33558
34573
  popover.querySelectorAll(".rp-chip").forEach((c2) => {
33559
34574
  c2.classList.toggle("current", c2.dataset.val === cur);
33560
34575
  });
34576
+ renderRecentChips(cur);
33561
34577
  popover.hidden = false;
33562
34578
  const r2 = input2.getBoundingClientRect();
33563
34579
  const popWidth = Math.min(560, Math.floor(window.innerWidth * 0.9));
@@ -34441,6 +35457,8 @@ code-viewer annotate add-db --db app.db --tab query \\
34441
35457
  input2.title = invalid ? filter.error || "invalid regular expression" : "";
34442
35458
  const filterActive = filter.kind !== "empty" && !invalid;
34443
35459
  const matches2 = invalid ? () => true : filter.match;
35460
+ let totalFiles = 0;
35461
+ let visibleFiles = 0;
34444
35462
  const walk = (node, depth) => {
34445
35463
  let subtreeVisible = false;
34446
35464
  const rows = [];
@@ -34465,7 +35483,9 @@ code-viewer annotate add-db --db app.db --tab query \\
34465
35483
  } else {
34466
35484
  const testHidden = STATE.hideTests && !isRepositorySidebarMode() && isTestPath(item.file.path || "");
34467
35485
  const visible = !testHidden && matches2(item.file.path || "");
35486
+ if (!testHidden) totalFiles++;
34468
35487
  if (visible) {
35488
+ visibleFiles++;
34469
35489
  rows.push({
34470
35490
  kind: "file",
34471
35491
  path: item.file.path,
@@ -34480,6 +35500,31 @@ code-viewer annotate add-db --db app.db --tab query \\
34480
35500
  return { visible: subtreeVisible, rows };
34481
35501
  };
34482
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);
34483
35528
  }
34484
35529
  function sidebarVirtualRange() {
34485
35530
  const sidebar = document.querySelector("#sidebar");
@@ -34749,7 +35794,9 @@ code-viewer annotate add-db --db app.db --tab query \\
34749
35794
  } else {
34750
35795
  renderFlat(files, ul, onFileClick);
34751
35796
  }
34752
- $("#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
+ );
34753
35800
  const effectiveView = treeMode ? "tree" : STATE.sbView;
34754
35801
  $$(".sb-view-seg button").forEach((b2) => {
34755
35802
  b2.classList.toggle("active", b2.dataset.view === effectiveView);
@@ -34864,10 +35911,16 @@ code-viewer annotate add-db --db app.db --tab query \\
34864
35911
  input2.title = invalid ? filter.error || "invalid regular expression" : "";
34865
35912
  const matches2 = invalid ? () => true : filter.match;
34866
35913
  const filterActive = filter.kind !== "empty" && !invalid;
35914
+ let totalFiles = 0;
35915
+ let visibleFiles = 0;
34867
35916
  $$("#filelist li[data-path]").forEach((li) => {
34868
35917
  const match2 = matches2(li.dataset.path || "");
34869
35918
  li.classList.toggle("hidden", !match2);
35919
+ if (li.classList.contains("hidden-by-tests")) return;
35920
+ totalFiles++;
35921
+ if (match2) visibleFiles++;
34870
35922
  });
35923
+ syncSidebarFilterCount(filterActive, visibleFiles, totalFiles);
34871
35924
  if (!isRepositorySidebarMode()) {
34872
35925
  document.querySelectorAll(".gdp-file-shell").forEach((card) => {
34873
35926
  const match2 = matches2(card.dataset.path || "");
@@ -35373,6 +36426,9 @@ code-viewer annotate add-db --db app.db --tab query \\
35373
36426
  repositoryWebTarget,
35374
36427
  openGithubLabel,
35375
36428
  openRepositoryWebLabel,
36429
+ folderHistoryLabel,
36430
+ folderHistoryTitle,
36431
+ openFolderHistory,
35376
36432
  fileBadge
35377
36433
  } = deps;
35378
36434
  let REPO_SORT = {
@@ -35789,6 +36845,15 @@ code-viewer annotate add-db --db app.db --tab query \\
35789
36845
  meta.ref
35790
36846
  );
35791
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);
35792
36857
  if (canTrashWorktreeRef(meta.ref)) {
35793
36858
  toolbar.appendChild(
35794
36859
  createNewFolderButton(meta.path || "", () => loadRepo())
@@ -36354,294 +37419,12 @@ code-viewer annotate add-db --db app.db --tab query \\
36354
37419
  };
36355
37420
  }
36356
37421
 
36357
- // web-src/core/fuzzy-search.ts
36358
- function basenameStart(path) {
36359
- const slash = path.lastIndexOf("/");
36360
- return slash < 0 ? 0 : slash + 1;
36361
- }
36362
- function isBoundary(path, index) {
36363
- if (index <= 0) return true;
36364
- const prev = path[index - 1];
36365
- return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
36366
- }
36367
- function toRanges(indices) {
36368
- const ranges = [];
36369
- for (const index of indices) {
36370
- const last = ranges[ranges.length - 1];
36371
- if (last && last.end === index) {
36372
- last.end = index + 1;
36373
- } else {
36374
- ranges.push({ start: index, end: index + 1 });
36375
- }
36376
- }
36377
- return ranges;
36378
- }
36379
- function basenameMatchTier(loweredQuery, loweredBasename) {
36380
- if (loweredBasename === loweredQuery) return 4;
36381
- if (loweredBasename.startsWith(`${loweredQuery}.`)) return 3;
36382
- if (loweredBasename.startsWith(loweredQuery)) return 2;
36383
- if (loweredBasename.includes(loweredQuery)) return 1;
36384
- return 0;
36385
- }
36386
- function pathMatchTier(loweredQuery, loweredPath, loweredBasename) {
36387
- if (loweredQuery.includes("/") && (loweredPath === loweredQuery || loweredPath.endsWith(`/${loweredQuery}`)))
36388
- return 4;
36389
- return basenameMatchTier(loweredQuery, loweredBasename);
36390
- }
36391
- function contiguousPathRange(loweredQuery, loweredPath, baseStart) {
36392
- const loweredBasename = loweredPath.slice(baseStart);
36393
- const basenameMatchStart = loweredBasename.indexOf(loweredQuery);
36394
- if (basenameMatchStart >= 0) {
36395
- const start = baseStart + basenameMatchStart;
36396
- return { start, end: start + loweredQuery.length };
36397
- }
36398
- if (loweredQuery.includes("/")) {
36399
- const pathMatchStart = loweredPath.endsWith(`/${loweredQuery}`) ? loweredPath.length - loweredQuery.length : loweredPath === loweredQuery ? 0 : -1;
36400
- if (pathMatchStart >= 0)
36401
- return {
36402
- start: pathMatchStart,
36403
- end: pathMatchStart + loweredQuery.length
36404
- };
36405
- }
36406
- return null;
36407
- }
36408
- function computeFuzzyMatch(query, path) {
36409
- const q = query.trim().toLowerCase();
36410
- if (!q) return { score: 0, ranges: [], tier: 0 };
36411
- const lowerPath = path.toLowerCase();
36412
- const baseStart = basenameStart(path);
36413
- const indices = [];
36414
- let from = 0;
36415
- let score = 0;
36416
- for (const ch of q) {
36417
- const index = lowerPath.indexOf(ch, from);
36418
- if (index < 0) return null;
36419
- indices.push(index);
36420
- score += 10;
36421
- if (index >= baseStart) score += 8;
36422
- if (isBoundary(path, index)) score += 6;
36423
- const prev = indices[indices.length - 2];
36424
- if (prev != null && prev + 1 === index) score += 12;
36425
- from = index + 1;
36426
- }
36427
- const first = indices[0];
36428
- score -= Math.min(first, 40);
36429
- if (indices[0] >= baseStart) score += 20;
36430
- const basename = lowerPath.slice(baseStart);
36431
- const tier = pathMatchTier(q, lowerPath, basename);
36432
- const contiguousRange = contiguousPathRange(q, lowerPath, baseStart);
36433
- return {
36434
- score,
36435
- ranges: contiguousRange ? [contiguousRange] : toRanges(indices),
36436
- tier
36437
- };
36438
- }
36439
- function fuzzyMatchPath(query, path) {
36440
- const match2 = computeFuzzyMatch(query, path);
36441
- return match2 ? { score: match2.score, ranges: match2.ranges } : null;
36442
- }
36443
- function rankFuzzyPaths(query, items, limit) {
36444
- const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
36445
- const compare = (a2, b2) => b2.tier - a2.tier || b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
36446
- if (!bounded) {
36447
- return items.map((item) => {
36448
- const match2 = computeFuzzyMatch(query, item.path);
36449
- return match2 ? { item, score: match2.score, ranges: match2.ranges, tier: match2.tier } : null;
36450
- }).filter(
36451
- (item) => item !== null
36452
- ).sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
36453
- }
36454
- const top = [];
36455
- for (const item of items) {
36456
- const match2 = computeFuzzyMatch(query, item.path);
36457
- if (!match2) continue;
36458
- const ranked = {
36459
- item,
36460
- score: match2.score,
36461
- ranges: match2.ranges,
36462
- tier: match2.tier
36463
- };
36464
- pushBoundedTop(top, ranked, bounded, compare);
36465
- }
36466
- return top.sort(compare).map(({ item, score, ranges }) => ({ item, score, ranges }));
36467
- }
36468
- function pushBoundedTop(heap, item, limit, compareBestFirst) {
36469
- const isWorse = (a2, b2) => compareBestFirst(a2, b2) > 0;
36470
- const siftUp = (index) => {
36471
- while (index > 0) {
36472
- const parent = Math.floor((index - 1) / 2);
36473
- if (!isWorse(heap[index], heap[parent])) break;
36474
- [heap[index], heap[parent]] = [heap[parent], heap[index]];
36475
- index = parent;
36476
- }
36477
- };
36478
- const siftDown = (index) => {
36479
- while (true) {
36480
- const left = index * 2 + 1;
36481
- const right = left + 1;
36482
- let worst = index;
36483
- if (left < heap.length && isWorse(heap[left], heap[worst])) worst = left;
36484
- if (right < heap.length && isWorse(heap[right], heap[worst]))
36485
- worst = right;
36486
- if (worst === index) break;
36487
- [heap[index], heap[worst]] = [heap[worst], heap[index]];
36488
- index = worst;
36489
- }
36490
- };
36491
- if (heap.length < limit) {
36492
- heap.push(item);
36493
- siftUp(heap.length - 1);
36494
- return;
36495
- }
36496
- if (compareBestFirst(item, heap[0]) >= 0) return;
36497
- heap[0] = item;
36498
- siftDown(0);
36499
- }
36500
- function rankGlobPathMatches(query, items, limit) {
36501
- const matchPath = createGlobPathMatcher(query);
36502
- if (!matchPath) return [];
36503
- const bounded = Number.isInteger(limit) && limit !== void 0 && limit > 0 ? Math.floor(limit) : 0;
36504
- const compare = (a2, b2) => b2.score - a2.score || a2.item.path.localeCompare(b2.item.path);
36505
- if (!bounded) {
36506
- return items.map((item) => {
36507
- const match2 = matchPath(item.path);
36508
- return match2 ? {
36509
- item,
36510
- score: match2.score,
36511
- ranges: match2.ranges,
36512
- mode: "glob"
36513
- } : null;
36514
- }).filter((item) => item !== null).sort(compare);
36515
- }
36516
- const top = [];
36517
- for (const item of items) {
36518
- const match2 = matchPath(item.path);
36519
- if (!match2) continue;
36520
- const ranked = {
36521
- item,
36522
- score: match2.score,
36523
- ranges: match2.ranges,
36524
- mode: "glob"
36525
- };
36526
- pushBoundedTop(top, ranked, bounded, compare);
36527
- }
36528
- return top.sort(compare);
36529
- }
36530
- function rankPathMatches(query, items, limit) {
36531
- if (isGlobPathQuery(query)) {
36532
- return rankGlobPathMatches(query, items, limit);
36533
- }
36534
- return rankFuzzyPaths(query, items, limit).map((item) => ({
36535
- ...item,
36536
- mode: "fuzzy"
36537
- }));
36538
- }
36539
- function isGlobPathQuery(query) {
36540
- return /[*?]/.test(query.trim());
36541
- }
36542
- function escapeRegexChar(ch) {
36543
- return /[\\^$+?.()|{}]/.test(ch) ? `\\${ch}` : ch;
36544
- }
36545
- function globToRegExp(query) {
36546
- const pattern = query.trim();
36547
- if (!pattern) return null;
36548
- let source = "^";
36549
- for (let i2 = 0; i2 < pattern.length; i2++) {
36550
- const ch = pattern[i2];
36551
- if (ch === "*") {
36552
- if (pattern[i2 + 1] === "*") {
36553
- source += ".*";
36554
- i2++;
36555
- } else {
36556
- source += "[^/]*";
36557
- }
36558
- } else if (ch === "?") {
36559
- source += "[^/]";
36560
- } else if (ch === "[") {
36561
- const close = pattern.indexOf("]", i2 + 1);
36562
- if (close < 0) {
36563
- source += "\\[";
36564
- } else {
36565
- const body = pattern.slice(i2 + 1, close).replace(/\\/g, "\\\\");
36566
- source += `[${body}]`;
36567
- i2 = close;
36568
- }
36569
- } else {
36570
- source += escapeRegexChar(ch);
36571
- }
36572
- }
36573
- source += "$";
36574
- try {
36575
- return new RegExp(source, "i");
36576
- } catch {
36577
- return null;
36578
- }
36579
- }
36580
- function globMatchPath(query, path) {
36581
- return createGlobPathMatcher(query)?.(path) ?? null;
36582
- }
36583
- function createGlobPathMatcher(query) {
36584
- const regex = globToRegExp(query);
36585
- if (!regex) return null;
36586
- const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
36587
- const suffix = query.replace(/^\*+/, "").toLowerCase();
36588
- return (path) => {
36589
- const baseStart = basenameStart(path);
36590
- const basename = path.slice(baseStart);
36591
- if (!regex.test(path) && (query.includes("/") || !regex.test(basename)))
36592
- return null;
36593
- const ranges = [];
36594
- const lowerPath = path.toLowerCase();
36595
- for (const part of literal) {
36596
- const start = lowerPath.indexOf(part.toLowerCase());
36597
- if (start >= 0) ranges.push({ start, end: start + part.length });
36598
- }
36599
- ranges.sort((a2, b2) => a2.start - b2.start || a2.end - b2.end);
36600
- const mergedRanges = [];
36601
- for (const range of ranges) {
36602
- const last = mergedRanges[mergedRanges.length - 1];
36603
- if (last && last.end >= range.start) {
36604
- last.end = Math.max(last.end, range.end);
36605
- } else {
36606
- mergedRanges.push({ ...range });
36607
- }
36608
- }
36609
- const score = 1e3 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(suffix) ? 50 : 0);
36610
- return { score, ranges: mergedRanges };
36611
- };
36612
- }
36613
-
36614
- // web-src/core/search-palette.ts
36615
- var PALETTE_RESULT_LIMIT = 50;
36616
- var GREP_SELECTION_HISTORY_LIMIT = 100;
36617
- var MIN_GREP_PALETTE_WIDTH = 720;
36618
- var MAX_GREP_PALETTE_WIDTH = 2400;
36619
- var MIN_GREP_PALETTE_HEIGHT = 420;
36620
- var MAX_GREP_PALETTE_HEIGHT = 1400;
36621
- function limitPaletteResults(items) {
36622
- return items.slice(0, PALETTE_RESULT_LIMIT);
36623
- }
36624
- function movePaletteSelection(index, count, direction) {
36625
- if (count <= 0) return -1;
36626
- if (index < 0) return direction > 0 ? 0 : count - 1;
36627
- return Math.max(0, Math.min(count - 1, index + direction));
36628
- }
36629
- function rememberPaletteSelection(history2, path) {
36630
- const next = history2.filter((entry) => entry !== path);
36631
- next.push(path);
36632
- return next.slice(-GREP_SELECTION_HISTORY_LIMIT);
36633
- }
36634
- function rankPaletteResultsByHistory(items, history2) {
36635
- const priority = new Map(history2.map((path, index) => [path, index + 1]));
36636
- return items.map((item, index) => ({ item, index })).sort(
36637
- (a2, b2) => (priority.get(b2.item.path) ?? 0) - (priority.get(a2.item.path) ?? 0) || a2.index - b2.index
36638
- ).map(({ item }) => item);
36639
- }
36640
-
36641
37422
  // web-src/views/search-palette-i18n.ts
36642
37423
  var EN2 = {
36643
37424
  files: "Files",
36644
37425
  grep: "Grep",
37426
+ switchToFiles: "Switch to file search (Ctrl+K)",
37427
+ switchToGrep: "Switch to text search (Ctrl+G)",
36645
37428
  searchFiles: "Search files",
36646
37429
  searchText: "Search text",
36647
37430
  fileCodePreview: "File code preview",
@@ -36652,14 +37435,20 @@ code-viewer annotate add-db --db app.db --tab query \\
36652
37435
  fuzzyHint: "Fuzzy path search",
36653
37436
  plain: "Plain",
36654
37437
  regex: ".* Regex",
37438
+ matchCase: "Aa",
37439
+ matchCaseTitle: "Match case (Alt+C)",
37440
+ wholeWord: "Word",
37441
+ wholeWordTitle: "Match whole words only (Alt+W)",
36655
37442
  excludeTests: "No test",
36656
37443
  excludeTestsTitle: "Exclude test/spec files",
36657
37444
  groupFiles: "Group files",
36658
37445
  groupFilesTitle: "Group matching lines by file",
36659
- regexHint: "Alt+R regex",
37446
+ grepHint: "path:<dir or glob> narrows · Alt+R regex",
36660
37447
  windowWidth: "window width",
36661
37448
  windowHeight: "window height",
36662
37449
  regexMode: "regex mode",
37450
+ caseSensitivity: "case sensitivity",
37451
+ wordMatching: "whole-word matching",
36663
37452
  fileGrouping: "file grouping",
36664
37453
  testExclusion: "test exclusion",
36665
37454
  saveFailed: (label, error2) => `Failed to save ${label}: ${error2}`,
@@ -36673,22 +37462,42 @@ code-viewer annotate add-db --db app.db --tab query \\
36673
37462
  line: (line, column) => `Line ${line}:${column}`,
36674
37463
  diffFiles: (count) => `${count} diff files`,
36675
37464
  typeToSearchFiles: "Type to search repository files",
37465
+ recentFiles: (count) => `Recent files - ${count}`,
36676
37466
  loadingFiles: "Loading files...",
36677
- results: (count) => `${count} results`,
37467
+ results: (count, total, candidatesTruncated) => (total === void 0 ? `${count} results` : `${count} of ${total} results`) + (candidatesTruncated ? " · file list truncated" : ""),
36678
37468
  noResults: "No results",
36679
37469
  typeToGrep: "Type to grep",
36680
37470
  invalidRegex: "Invalid regular expression",
36681
37471
  searching: "Searching...",
36682
37472
  repositoryChanged: "Repository changed; search again",
36683
- 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`,
36684
37483
  searchFailed: (error2) => `Search failed: ${error2}`,
36685
37484
  savingSelection: "Saving selection...",
36686
37485
  selectionSaveFailed: (error2) => `Failed to save selection: ${error2}`,
36687
- 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}`
36688
37495
  };
36689
37496
  var JA2 = {
36690
37497
  files: "ファイル",
36691
37498
  grep: "GREP",
37499
+ switchToFiles: "ファイル名検索に切り替え (Ctrl+K)",
37500
+ switchToGrep: "コード検索に切り替え (Ctrl+G)",
36692
37501
  searchFiles: "ファイルを検索",
36693
37502
  searchText: "コードを検索",
36694
37503
  fileCodePreview: "ファイルのコード表示",
@@ -36699,14 +37508,20 @@ code-viewer annotate add-db --db app.db --tab query \\
36699
37508
  fuzzyHint: "パスのあいまい検索",
36700
37509
  plain: "通常",
36701
37510
  regex: ".* 正規表現",
37511
+ matchCase: "Aa",
37512
+ matchCaseTitle: "大文字と小文字を区別 (Alt+C)",
37513
+ wholeWord: "単語",
37514
+ wholeWordTitle: "単語単位で一致 (Alt+W)",
36702
37515
  excludeTests: "テスト除外",
36703
37516
  excludeTestsTitle: "test/spec ファイルを除外",
36704
37517
  groupFiles: "ファイル別",
36705
37518
  groupFilesTitle: "一致した行をファイル別に表示",
36706
- regexHint: "Alt+R 正規表現",
37519
+ grepHint: "path:<ディレクトリ or glob> で絞り込み · Alt+R 正規表現",
36707
37520
  windowWidth: "ウィンドウの幅",
36708
37521
  windowHeight: "ウィンドウの高さ",
36709
37522
  regexMode: "正規表現モード",
37523
+ caseSensitivity: "大文字小文字の区別",
37524
+ wordMatching: "単語単位の一致",
36710
37525
  fileGrouping: "ファイル別表示",
36711
37526
  testExclusion: "テスト除外",
36712
37527
  saveFailed: (label, error2) => `${label}を保存できませんでした: ${error2}`,
@@ -36720,23 +37535,92 @@ code-viewer annotate add-db --db app.db --tab query \\
36720
37535
  line: (line, column) => `${line} 行:${column}`,
36721
37536
  diffFiles: (count) => `差分ファイル ${count} 件`,
36722
37537
  typeToSearchFiles: "リポジトリ内のファイル名を入力してください",
37538
+ recentFiles: (count) => `最近開いたファイル - ${count} 件`,
36723
37539
  loadingFiles: "ファイルを読み込み中...",
36724
- results: (count) => `${count} 件`,
37540
+ results: (count, total, candidatesTruncated) => (total === void 0 ? `${count} 件` : `${total} 件中 ${count} 件`) + (candidatesTruncated ? "・ファイル一覧は上限で打ち切り" : ""),
36725
37541
  noResults: "該当なし",
36726
37542
  typeToGrep: "検索するコードを入力してください",
36727
37543
  invalidRegex: "正規表現が正しくありません",
36728
37544
  searching: "検索中...",
36729
37545
  repositoryChanged: "リポジトリが変更されました。もう一度検索してください",
36730
- 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} 件`,
36731
37556
  searchFailed: (error2) => `検索に失敗しました: ${error2}`,
36732
37557
  savingSelection: "選択履歴を保存中...",
36733
37558
  selectionSaveFailed: (error2) => `選択履歴を保存できませんでした: ${error2}`,
36734
- 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} 内`
36735
37568
  };
36736
37569
  function searchPaletteText(language) {
36737
37570
  return language === "ja" ? JA2 : EN2;
36738
37571
  }
36739
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
+
36740
37624
  // web-src/views/search-palette-ui.ts
36741
37625
  function createSearchPalette(deps) {
36742
37626
  const {
@@ -36760,14 +37644,18 @@ code-viewer annotate add-db --db app.db --tab query \\
36760
37644
  getFileSelectionHistory,
36761
37645
  getGrepSelectionHistory,
36762
37646
  getGrepRegex,
37647
+ getGrepCaseSensitive,
37648
+ getGrepWholeWord,
36763
37649
  getGrepHideTests,
36764
37650
  getGrepGroupByFile,
36765
37651
  getGrepPaletteWidth,
36766
37652
  getGrepPaletteHeight,
36767
37653
  persistGrepSettings,
36768
- applyGrepHideTests
37654
+ applyGrepHideTests,
37655
+ openSearchResults
36769
37656
  } = deps;
36770
37657
  let PALETTE = null;
37658
+ const LAST_QUERY = { file: "", grep: "" };
36771
37659
  let repoFileRequestGeneration = 0;
36772
37660
  const GREP_CONTEXT_RADIUS = 5;
36773
37661
  const FILE_PREVIEW_LINE_LIMIT = 200;
@@ -36804,6 +37692,7 @@ code-viewer annotate add-db --db app.db --tab query \\
36804
37692
  }
36805
37693
  function closeSearchPalette() {
36806
37694
  if (!PALETTE) return;
37695
+ LAST_QUERY[PALETTE.mode] = PALETTE.input.value;
36807
37696
  const previousFocusScope = PALETTE.previousFocusScope;
36808
37697
  PALETTE.controller?.abort();
36809
37698
  PALETTE.previewController?.abort();
@@ -36815,6 +37704,7 @@ code-viewer annotate add-db --db app.db --tab query \\
36815
37704
  }
36816
37705
  function createPalette(mode) {
36817
37706
  const previousFocusScope = PALETTE ? PALETTE.previousFocusScope : getPanelFocusScope();
37707
+ const initialQuery = PALETTE ? PALETTE.input.value : LAST_QUERY[mode];
36818
37708
  closeSearchPalette();
36819
37709
  const root = document.createElement("div");
36820
37710
  root.className = "gdp-palette-backdrop";
@@ -36833,7 +37723,19 @@ code-viewer annotate add-db --db app.db --tab query \\
36833
37723
  dialog.style.height = `${clampGrepPaletteHeight(savedHeight)}px`;
36834
37724
  const label = document.createElement("div");
36835
37725
  label.className = "gdp-palette-label";
36836
- 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
+ }
36837
37739
  const input2 = document.createElement("input");
36838
37740
  input2.className = "gdp-palette-input";
36839
37741
  input2.type = "search";
@@ -36843,6 +37745,7 @@ code-viewer annotate add-db --db app.db --tab query \\
36843
37745
  input2.setAttribute("role", "combobox");
36844
37746
  input2.setAttribute("aria-expanded", "true");
36845
37747
  input2.setAttribute("aria-controls", "gdp-palette-list");
37748
+ input2.value = initialQuery;
36846
37749
  const status = document.createElement("div");
36847
37750
  status.className = "gdp-palette-status";
36848
37751
  const controls = document.createElement("div");
@@ -36886,6 +37789,8 @@ code-viewer annotate add-db --db app.db --tab query \\
36886
37789
  preview,
36887
37790
  mode,
36888
37791
  grepRegex: getGrepRegex(),
37792
+ grepCaseSensitive: getGrepCaseSensitive(),
37793
+ grepWholeWord: getGrepWholeWord(),
36889
37794
  grepHideTests: getGrepHideTests(),
36890
37795
  grepGroupByFile: getGrepGroupByFile(),
36891
37796
  grepPaletteWidth: clampGrepPaletteWidth(savedWidth ?? dialogRect.width),
@@ -36957,6 +37862,7 @@ code-viewer annotate add-db --db app.db --tab query \\
36957
37862
  input2.addEventListener("input", () => updatePaletteResults(state));
36958
37863
  input2.addEventListener("keydown", (e2) => handlePaletteKeydown(e2, state));
36959
37864
  input2.focus();
37865
+ if (input2.value) input2.select();
36960
37866
  updatePaletteResults(state);
36961
37867
  return state;
36962
37868
  }
@@ -37004,6 +37910,38 @@ code-viewer annotate add-db --db app.db --tab query \\
37004
37910
  e2.preventDefault();
37005
37911
  void updateGrepRegex(state, true);
37006
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
+ });
37007
37945
  const excludeTests = createExcludeTestsButton(state);
37008
37946
  const groupFiles = document.createElement("button");
37009
37947
  groupFiles.type = "button";
@@ -37018,8 +37956,62 @@ code-viewer annotate add-db --db app.db --tab query \\
37018
37956
  });
37019
37957
  const hint = document.createElement("span");
37020
37958
  hint.className = "gdp-palette-mode-hint";
37021
- hint.textContent = text3().regexHint;
37022
- 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
+ }
37023
38015
  }
37024
38016
  function errorMessage2(err, fallback) {
37025
38017
  return err instanceof Error && err.message ? err.message : fallback;
@@ -37144,51 +38136,16 @@ code-viewer annotate add-db --db app.db --tab query \\
37144
38136
  if (cursor < path.length)
37145
38137
  parent.appendChild(document.createTextNode(path.slice(cursor)));
37146
38138
  }
37147
- function grepMatchRange(lineText, item) {
37148
- const matchText = item.matchText || (item.regex ? "" : item.query);
37149
- if (!matchText) return null;
37150
- const caseSensitive = item.regex || /[A-Z]/.test(item.query);
37151
- const haystack = caseSensitive ? lineText : lineText.toLowerCase();
37152
- const needle = caseSensitive ? matchText : matchText.toLowerCase();
37153
- const expectedStart = Math.max(0, item.column - 1);
37154
- let bestStart = -1;
37155
- let cursor = haystack.indexOf(needle);
37156
- while (cursor >= 0) {
37157
- if (bestStart < 0 || Math.abs(cursor - expectedStart) < Math.abs(bestStart - expectedStart))
37158
- bestStart = cursor;
37159
- cursor = haystack.indexOf(needle, cursor + Math.max(1, needle.length));
37160
- }
37161
- return bestStart < 0 ? null : { start: bestStart, end: bestStart + matchText.length };
38139
+ function grepHighlightText(item) {
38140
+ return item.matchText || (item.regex ? "" : item.term);
37162
38141
  }
37163
38142
  function highlightGrepMatch(cell, lineText, item) {
37164
- const match2 = grepMatchRange(lineText, item);
37165
- if (!match2) return;
37166
- const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
37167
- const parts = [];
37168
- let offset = 0;
37169
- for (let node = walker.nextNode(); node; node = walker.nextNode()) {
37170
- const textNode = node;
37171
- const nextOffset = offset + textNode.data.length;
37172
- const start = Math.max(match2.start, offset);
37173
- const end = Math.min(match2.end, nextOffset);
37174
- if (start < end)
37175
- parts.push({
37176
- node: textNode,
37177
- start: start - offset,
37178
- end: end - offset
37179
- });
37180
- offset = nextOffset;
37181
- }
37182
- for (const part of parts.reverse()) {
37183
- const selected = part.node.splitText(part.start);
37184
- selected.splitText(part.end - part.start);
37185
- const mark = document.createElement("mark");
37186
- mark.className = "gdp-grep-match";
37187
- const parent = selected.parentNode;
37188
- if (!parent) throw new Error("grep match text is detached");
37189
- parent.insertBefore(mark, selected);
37190
- mark.appendChild(selected);
37191
- }
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
+ });
37192
38149
  }
37193
38150
  function palettePreviewTarget(item) {
37194
38151
  return item.kind === "file" ? {
@@ -37490,27 +38447,73 @@ code-viewer annotate add-db --db app.db --tab query \\
37490
38447
  ).sort(
37491
38448
  (a2, b2) => b2.match.score - a2.match.score || a2.file.path.localeCompare(b2.file.path)
37492
38449
  );
37493
- return limitPaletteResults(
37494
- rankPaletteResultsByHistory(
37495
- candidates.map((candidate) => ({
37496
- kind: "file",
37497
- path: candidate.file.path,
37498
- old_path: candidate.file.old_path,
37499
- displayPath: candidate.displayPath,
37500
- ref: paletteRef("diff"),
37501
- targetPath: fileSourceTarget(candidate.file).path,
37502
- targetRef: fileSourceTarget(candidate.file).ref,
37503
- source: "diff",
37504
- ranges: candidate.match.ranges
37505
- })),
37506
- 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
+ )
37507
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
+ }))
37508
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);
37509
38508
  }
37510
38509
  async function updateFilePalette(state, query) {
37511
38510
  renderPaletteControls(state);
37512
38511
  const source = paletteSource();
37513
38512
  if (!query.trim()) {
38513
+ if (source === "repo") {
38514
+ await updateRecentFilePalette(state, source);
38515
+ return;
38516
+ }
37514
38517
  const base2 = source === "diff" ? state.diffSnapshot.filter(
37515
38518
  (file) => !state.grepHideTests || !isTestFilePath(file.path)
37516
38519
  ).map((file) => {
@@ -37535,8 +38538,12 @@ code-viewer annotate add-db --db app.db --tab query \\
37535
38538
  renderPalette(state);
37536
38539
  return;
37537
38540
  }
38541
+ let totalMatches = 0;
38542
+ let candidatesTruncated = false;
37538
38543
  if (source === "diff") {
37539
- state.items = diffFilePaletteItems(state, query);
38544
+ const ranked = diffFilePaletteItems(state, query);
38545
+ state.items = ranked.items;
38546
+ totalMatches = ranked.total;
37540
38547
  } else {
37541
38548
  state.status.textContent = text3().loadingFiles;
37542
38549
  const ref = paletteRef(source);
@@ -37554,9 +38561,10 @@ code-viewer annotate add-db --db app.db --tab query \\
37554
38561
  const visibleFiles = response.files.filter(
37555
38562
  (file) => !state.grepHideTests || !isTestFilePath(file.path)
37556
38563
  );
38564
+ const stats = { total: 0 };
37557
38565
  state.items = limitPaletteResults(
37558
38566
  rankPaletteResultsByHistory(
37559
- rankPathMatches(query, visibleFiles, PALETTE_RESULT_LIMIT).map(
38567
+ rankPathMatches(query, visibleFiles, PALETTE_RESULT_LIMIT, stats).map(
37560
38568
  (match2) => ({
37561
38569
  kind: "file",
37562
38570
  path: match2.item.path,
@@ -37569,23 +38577,37 @@ code-viewer annotate add-db --db app.db --tab query \\
37569
38577
  state.fileHistory
37570
38578
  )
37571
38579
  );
38580
+ totalMatches = stats.total;
38581
+ candidatesTruncated = response.truncated;
37572
38582
  }
37573
38583
  state.selected = state.items.length ? 0 : -1;
37574
- 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;
37575
38589
  renderPalette(state);
37576
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
+ }
37577
38597
  function updateGrepPalette(state, query) {
37578
38598
  renderPaletteControls(state);
37579
38599
  state.controller?.abort();
37580
38600
  if (state.debounce) window.clearTimeout(state.debounce);
37581
- if (!query.trim()) {
38601
+ const parsed = parseGrepQuery(query);
38602
+ const term = parsed.term;
38603
+ if (!term) {
37582
38604
  state.items = [];
37583
38605
  state.selected = -1;
37584
38606
  state.status.textContent = text3().typeToGrep;
37585
38607
  renderPalette(state);
37586
38608
  return;
37587
38609
  }
37588
- if (state.grepRegex && !regexQueryIsValid(query)) {
38610
+ if (state.grepRegex && !regexQueryIsValid(term)) {
37589
38611
  state.controller?.abort();
37590
38612
  state.items = [];
37591
38613
  state.selected = -1;
@@ -37601,16 +38623,30 @@ code-viewer annotate add-db --db app.db --tab query \\
37601
38623
  const source = paletteSource();
37602
38624
  const ref = paletteRef(source);
37603
38625
  const regex = state.grepRegex;
38626
+ const caseSensitive = state.grepCaseSensitive;
38627
+ const wholeWord = state.grepWholeWord;
37604
38628
  const hideTests = state.grepHideTests;
37605
- const params = new URLSearchParams();
37606
- params.set("ref", ref);
37607
- params.set("q", query);
37608
- params.set("max", "200");
37609
- if (regex) params.set("regex", "1");
37610
- 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
+ });
37611
38638
  appendScopeParams(params);
37612
38639
  if (source === "diff") {
37613
- 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);
37614
38650
  }
37615
38651
  const controller = new AbortController();
37616
38652
  state.controller = controller;
@@ -37625,7 +38661,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37625
38661
  return r2.json();
37626
38662
  })
37627
38663
  ).then((response) => {
37628
- 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)
37629
38665
  return;
37630
38666
  if (responseGenerationIsStale(response.generation)) {
37631
38667
  state.items = [];
@@ -37646,7 +38682,9 @@ code-viewer annotate add-db --db app.db --tab query \\
37646
38682
  preview: match2.preview,
37647
38683
  matchText: match2.matchText,
37648
38684
  query,
38685
+ term,
37649
38686
  regex,
38687
+ caseSensitive,
37650
38688
  ref,
37651
38689
  source
37652
38690
  }))
@@ -37655,9 +38693,12 @@ code-viewer annotate add-db --db app.db --tab query \\
37655
38693
  state.status.textContent = text3().grepSummary({
37656
38694
  engine: response.engine,
37657
38695
  regex,
38696
+ caseSensitive,
38697
+ wholeWord,
37658
38698
  testsExcluded: hideTests,
37659
38699
  truncated: response.truncated,
37660
- count: state.items.length
38700
+ count: state.items.length,
38701
+ paths: parsed.paths
37661
38702
  });
37662
38703
  renderPalette(state);
37663
38704
  }).catch((err) => {
@@ -37748,12 +38789,14 @@ code-viewer annotate add-db --db app.db --tab query \\
37748
38789
  });
37749
38790
  scrollToFile(item.path, item.line);
37750
38791
  } else {
38792
+ const hl = grepHighlightText(item);
37751
38793
  setRoute({
37752
38794
  screen: "file",
37753
38795
  path: item.path,
37754
38796
  ref: item.ref,
37755
38797
  view: "blob",
37756
38798
  line: item.line,
38799
+ ...hl ? { hl } : {},
37757
38800
  range: currentRange()
37758
38801
  });
37759
38802
  void renderStandaloneSource({ path: item.path, ref: item.ref });
@@ -37769,6 +38812,10 @@ code-viewer annotate add-db --db app.db --tab query \\
37769
38812
  if (e2.key === "Enter") {
37770
38813
  if (state.composing) return;
37771
38814
  e2.preventDefault();
38815
+ if (state.mode === "grep" && (e2.ctrlKey || e2.metaKey) && openSearchResults) {
38816
+ pinResults(state);
38817
+ return;
38818
+ }
37772
38819
  void selectPaletteItem(state);
37773
38820
  return;
37774
38821
  }
@@ -37777,6 +38824,26 @@ code-viewer annotate add-db --db app.db --tab query \\
37777
38824
  void updateGrepRegex(state, !state.grepRegex);
37778
38825
  return;
37779
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
+ }
37780
38847
  const direction = e2.key === "ArrowDown" || e2.ctrlKey && e2.key.toLowerCase() === "n" ? 1 : e2.key === "ArrowUp" || e2.ctrlKey && e2.key.toLowerCase() === "p" ? -1 : 0;
37781
38848
  if (direction) {
37782
38849
  e2.preventDefault();
@@ -37809,6 +38876,317 @@ code-viewer annotate add-db --db app.db --tab query \\
37809
38876
  };
37810
38877
  }
37811
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
+
37812
39190
  // web-src/views/source-view.ts
37813
39191
  function createSourceView(deps) {
37814
39192
  const {
@@ -37827,6 +39205,7 @@ code-viewer annotate add-db --db app.db --tab query \\
37827
39205
  placeSidebarToggle,
37828
39206
  createFileBreadcrumb,
37829
39207
  createRepositoryWebLink: createRepositoryWebLink2,
39208
+ createRevisionNav,
37830
39209
  createFileDetailMeta,
37831
39210
  createOpenPathButton,
37832
39211
  createMoveToTrashButton,
@@ -38117,10 +39496,17 @@ code-viewer annotate add-db --db app.db --tab query \\
38117
39496
  const cells = table2.querySelectorAll(
38118
39497
  ".gdp-source-line-code"
38119
39498
  );
39499
+ const sourceLines = textValue.split("\n");
38120
39500
  cells.forEach((cell, index) => {
38121
39501
  if (highlightedLines[index] == null) return;
38122
39502
  cell.innerHTML = highlightedLines[index] || " ";
38123
39503
  cell.classList.add("shiki");
39504
+ markSourceHighlightTerm(
39505
+ cell,
39506
+ sourceLines[index] ?? "",
39507
+ index + 1,
39508
+ target
39509
+ );
38124
39510
  });
38125
39511
  }).catch((err) => {
38126
39512
  console.error("Failed to apply source syntax highlighting", err);
@@ -38535,6 +39921,7 @@ code-viewer annotate add-db --db app.db --tab query \\
38535
39921
  const code2 = document.createElement("td");
38536
39922
  code2.className = "gdp-source-line-code";
38537
39923
  code2.textContent = line || " ";
39924
+ markSourceHighlightTerm(code2, line, index + 1, target);
38538
39925
  tr.appendChild(num);
38539
39926
  tr.appendChild(code2);
38540
39927
  tbody.appendChild(tr);
@@ -38680,6 +40067,45 @@ code-viewer annotate add-db --db app.db --tab query \\
38680
40067
  const routeTarget = sourceTargetFromRoute();
38681
40068
  return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : void 0;
38682
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
+ }
38683
40109
  function lineTargetStart(line) {
38684
40110
  if (!line) return void 0;
38685
40111
  return typeof line === "number" ? line : line.start;
@@ -38887,8 +40313,9 @@ code-viewer annotate add-db --db app.db --tab query \\
38887
40313
  next.addEventListener("click", () => move(1));
38888
40314
  close.addEventListener("click", hide);
38889
40315
  return {
38890
- open: () => {
40316
+ open: (query) => {
38891
40317
  bar.hidden = false;
40318
+ if (query !== void 0) input2.value = query;
38892
40319
  input2.focus();
38893
40320
  input2.select();
38894
40321
  sync();
@@ -39056,6 +40483,13 @@ code-viewer annotate add-db --db app.db --tab query \\
39056
40483
  render
39057
40484
  );
39058
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
+ }
39059
40493
  let resizeObserver = null;
39060
40494
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
39061
40495
  if (!scroller.isConnected) {
@@ -39309,6 +40743,13 @@ code-viewer annotate add-db --db app.db --tab query \\
39309
40743
  render
39310
40744
  );
39311
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
+ }
39312
40753
  let resizeObserver = null;
39313
40754
  resizeObserver = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
39314
40755
  if (!scroller.isConnected) {
@@ -39458,6 +40899,8 @@ code-viewer annotate add-db --db app.db --tab query \\
39458
40899
  const mounted = mountedStandaloneSourceCard(target);
39459
40900
  const state = mounted?.dataset.sourceState;
39460
40901
  if (mounted && state === "done") {
40902
+ syncRenderedSourceLineHighlights(mounted, target);
40903
+ syncSourceHighlightMarks(mounted, target);
39461
40904
  scrollStandaloneSourceLine(
39462
40905
  mounted,
39463
40906
  lineTargetStart(
@@ -39493,7 +40936,8 @@ code-viewer annotate add-db --db app.db --tab query \\
39493
40936
  setRoute,
39494
40937
  setPreferredSourceTab,
39495
40938
  createFileBreadcrumb,
39496
- createRepositoryWebLink: createRepositoryWebLink2
40939
+ createRepositoryWebLink: createRepositoryWebLink2,
40940
+ createRevisionNav
39497
40941
  },
39498
40942
  target,
39499
40943
  activeTab,
@@ -45176,6 +46620,7 @@ ${t2.files.overlapTitle(others)}`;
45176
46620
  if (name) name.textContent = branch;
45177
46621
  el2.title = branch ? `Current branch: ${branch}` : "";
45178
46622
  }
46623
+ const MAX_RECENT_REFS = 8;
45179
46624
  function mergeLocalSettings(patch) {
45180
46625
  const next = { ...APP_SETTINGS };
45181
46626
  for (const [key, value] of Object.entries(patch)) {
@@ -45742,7 +47187,20 @@ ${error2.stack}` : ""}`
45742
47187
  copyReferenceLabel: () => uiText().global.copyLineReference,
45743
47188
  lineCountLabel: (count) => uiText().global.selectedLineCount(count),
45744
47189
  githubOpenTitle: () => uiText().global.githubSelectionOpen,
45745
- 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
+ }
45746
47204
  });
45747
47205
  const DIFF_LINE_SELECT = createDiffLineSelect({ pill: LINE_REF_PILL });
45748
47206
  function clearRenderedSourceLineTargets() {
@@ -45812,6 +47270,7 @@ ${error2.stack}` : ""}`
45812
47270
  REPO_SIDEBAR_REF = ref;
45813
47271
  },
45814
47272
  isTestPath: isTestFilePath,
47273
+ filterCountTitle: (visible, total) => uiText().sidebar.filterCountTitle(visible, total),
45815
47274
  sidebarToggleTitle: (hidden) => hidden ? uiText().sidebar.show : uiText().sidebar.hide,
45816
47275
  openDirectoryInOsTitle: () => uiText().sidebar.openDirectoryInOs,
45817
47276
  omittedDirectoryBadge: (reason) => {
@@ -45890,6 +47349,7 @@ ${error2.stack}` : ""}`
45890
47349
  placeSidebarToggle,
45891
47350
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
45892
47351
  createRepositoryWebLink: createFileRepositoryWebLink,
47352
+ createRevisionNav: createFileRevisionNav,
45893
47353
  createFileDetailMeta: (target, meta) => REPO_VIEW.createFileDetailMeta(target, meta),
45894
47354
  createOpenPathButton,
45895
47355
  createMoveToTrashButton: (path, onDeleted) => REPO_VIEW.createMoveToTrashButton(path, onDeleted),
@@ -45933,6 +47393,7 @@ ${error2.stack}` : ""}`
45933
47393
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
45934
47394
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
45935
47395
  createRepositoryWebLink: createFileRepositoryWebLink,
47396
+ createRevisionNav: createFileRevisionNav,
45936
47397
  removeStandaloneSource,
45937
47398
  placeSidebarToggle,
45938
47399
  escapeHtml: escapeHtml3,
@@ -46016,6 +47477,17 @@ ${error2.stack}` : ""}`
46016
47477
  }),
46017
47478
  openGithubLabel: () => uiText().repo.openGithub,
46018
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
+ },
46019
47491
  fileBadge: (status) => DIFF_VIEW.fileBadge(status)
46020
47492
  });
46021
47493
  const {
@@ -46049,6 +47521,8 @@ ${error2.stack}` : ""}`
46049
47521
  getFileSelectionHistory: () => APP_SETTINGS.fileSelectionHistory || [],
46050
47522
  getGrepSelectionHistory: () => APP_SETTINGS.grepSelectionHistory || [],
46051
47523
  getGrepRegex: () => APP_SETTINGS.grepRegex === true,
47524
+ getGrepCaseSensitive: () => APP_SETTINGS.grepCaseSensitive === true,
47525
+ getGrepWholeWord: () => APP_SETTINGS.grepWholeWord === true,
46052
47526
  getGrepHideTests: () => STATE.hideTests,
46053
47527
  getGrepGroupByFile: () => APP_SETTINGS.grepGroupByFile === true,
46054
47528
  getGrepPaletteWidth: () => APP_SETTINGS.grepPaletteWidth,
@@ -46057,7 +47531,8 @@ ${error2.stack}` : ""}`
46057
47531
  applyGrepHideTests: (hidden) => {
46058
47532
  STATE.hideTests = hidden;
46059
47533
  applyHideTests();
46060
- }
47534
+ },
47535
+ openSearchResults: (query) => openSearchSheet(query)
46061
47536
  });
46062
47537
  const { openSearchPalette, isPaletteOpen, paletteMode, clearRepoFileCache } = SEARCH_PALETTE;
46063
47538
  const UI_TEXT = {
@@ -46087,6 +47562,11 @@ ${error2.stack}` : ""}`
46087
47562
  queryHistory: "query history",
46088
47563
  settings: "viewer settings",
46089
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",
46090
47570
  copyAiContext: "Copy AI context (Shift+Click to include code)",
46091
47571
  copyAiContextCopied: "Copied AI context",
46092
47572
  copyAiContextCopiedWithCode: (lines) => `Copied AI context + code (${lines} line${lines === 1 ? "" : "s"})`,
@@ -46175,7 +47655,8 @@ ${error2.stack}` : ""}`
46175
47655
  treeTitle: "tree view",
46176
47656
  flatTitle: "flat list",
46177
47657
  filter: "Filter files… / ⌘K",
46178
- 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`,
46179
47660
  filterClear: "Clear",
46180
47661
  filterClearTitle: "Clear file filter",
46181
47662
  hide: "hide sidebar",
@@ -46211,7 +47692,9 @@ ${error2.stack}` : ""}`
46211
47692
  submoduleLabel: "submodule",
46212
47693
  submoduleTitle: "Git submodule pinned to a commit",
46213
47694
  openGithub: "Open on GitHub",
46214
- openRepositoryWeb: "Open repository web page"
47695
+ openRepositoryWeb: "Open repository web page",
47696
+ folderHistory: "History",
47697
+ folderHistoryTitle: "Commits that touched this folder"
46215
47698
  },
46216
47699
  history: {
46217
47700
  title: "Commits",
@@ -46423,6 +47906,11 @@ ${error2.stack}` : ""}`
46423
47906
  queryHistory: "クエリ履歴",
46424
47907
  settings: "ビューア設定",
46425
47908
  theme: "テーマ切り替え",
47909
+ search: "ファイルを検索 (Ctrl+K)・Shift+クリックで grep (Ctrl+G)",
47910
+ lineHistory: "この行の履歴",
47911
+ recentRef: "最近使った ref",
47912
+ olderRevision: "このファイルの 1 つ前のリビジョン",
47913
+ newerRevision: "このファイルの 1 つ後のリビジョン",
46426
47914
  copyAiContext: "AI 用コンテキストをコピー(Shift+Click でコードも添付)",
46427
47915
  copyAiContextCopied: "コピーしました",
46428
47916
  copyAiContextCopiedWithCode: (lines) => `コピーしました(コード付き・${lines}行)`,
@@ -46511,7 +47999,8 @@ ${error2.stack}` : ""}`
46511
47999
  treeTitle: "ツリー表示",
46512
48000
  flatTitle: "一覧表示",
46513
48001
  filter: "ファイル絞り込み… / ⌘K",
46514
- 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} 件が一致`,
46515
48004
  filterClear: "解除",
46516
48005
  filterClearTitle: "ファイル絞り込みを解除",
46517
48006
  hide: "サイドバーを隠す",
@@ -46547,7 +48036,9 @@ ${error2.stack}` : ""}`
46547
48036
  submoduleLabel: "サブモジュール",
46548
48037
  submoduleTitle: "Git サブモジュール: 特定のコミットに固定されています。直接は開けません。",
46549
48038
  openGithub: "GitHubで開く",
46550
- openRepositoryWeb: "リポジトリのウェブページを開く"
48039
+ openRepositoryWeb: "リポジトリのウェブページを開く",
48040
+ folderHistory: "履歴",
48041
+ folderHistoryTitle: "このフォルダを変更したコミット"
46551
48042
  },
46552
48043
  history: {
46553
48044
  title: "コミット",
@@ -46775,6 +48266,11 @@ ${error2.stack}` : ""}`
46775
48266
  quickHelpBtn.title = text3.quickHelp.buttonTitle;
46776
48267
  quickHelpBtn.setAttribute("aria-label", text3.quickHelp.buttonTitle);
46777
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
+ }
46778
48274
  QUICK_HELP?.localize();
46779
48275
  const doctorTitle = doctorText(STATE.language).title;
46780
48276
  const doctorBtn = document.querySelector("#doctor-btn");
@@ -46801,6 +48297,13 @@ ${error2.stack}` : ""}`
46801
48297
  }
46802
48298
  document.querySelector("#terminal-sheet")?.setAttribute("aria-label", terminalChrome.title);
46803
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?.();
46804
48307
  document.querySelector(".app-panel-tabs")?.setAttribute("aria-label", text3.appPanel.tabs);
46805
48308
  const panelLayout = document.querySelector(
46806
48309
  ".app-panel-layout-switch"
@@ -46963,6 +48466,7 @@ ${error2.stack}` : ""}`
46963
48466
  let relocalizeTools = null;
46964
48467
  let relocalizeViewerSettings = null;
46965
48468
  let relocalizeTerminal = null;
48469
+ let relocalizeSearchResults = null;
46966
48470
  let relocalizeDatabase = null;
46967
48471
  let QUICK_HELP = null;
46968
48472
  function setViewerLanguage(language, persist = true) {
@@ -47313,6 +48817,18 @@ ${error2.stack}` : ""}`
47313
48817
  function isHistoryPanelRoute(route) {
47314
48818
  return route.screen === "history" || isFileHistoryRoute(route);
47315
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
+ }
47316
48832
  function normalizeInternalFileRoute(route) {
47317
48833
  if (route.screen !== "file") return route;
47318
48834
  if (sourceInternalPathKind(route.path) === null) return route;
@@ -47361,6 +48877,7 @@ ${error2.stack}` : ""}`
47361
48877
  setPreferredSourceTab: (tab) => SOURCE_VIEW.setPreferredSourceTab(tab),
47362
48878
  createFileBreadcrumb: (path, ref) => DIFF_VIEW.createFileBreadcrumb(path, ref),
47363
48879
  createRepositoryWebLink: createFileRepositoryWebLink,
48880
+ createRevisionNav: createFileRevisionNav,
47364
48881
  emptyText: () => uiText().diff
47365
48882
  },
47366
48883
  historyRoute,
@@ -47380,15 +48897,21 @@ ${error2.stack}` : ""}`
47380
48897
  return ANNOTATIONS_UI ? ANNOTATIONS_UI.withSessionParam(rawUrl) : rawUrl;
47381
48898
  }
47382
48899
  function withOverlayState(url) {
47383
- return withTerminalOverlay(
47384
- withToolsOverlay(
47385
- withDoctorOverlay(
47386
- url,
47387
- 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)
47388
48911
  ),
47389
- parseToolsOverlay(window.location.search)
48912
+ parseTerminalOverlay(window.location.search)
47390
48913
  ),
47391
- parseTerminalOverlay(window.location.search)
48914
+ parseSearchResultsOverlay(window.location.search)
47392
48915
  );
47393
48916
  }
47394
48917
  function urlForRoute(route) {
@@ -47779,6 +49302,61 @@ ${error2.stack}` : ""}`
47779
49302
  });
47780
49303
  return button;
47781
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
+ }
47782
49360
  function createFileRepositoryWebLink(target) {
47783
49361
  const webTarget = buildRepositoryWebTarget(REPO_WEB_URL, {
47784
49362
  ref: target.ref,
@@ -47980,6 +49558,12 @@ ${error2.stack}` : ""}`
47980
49558
  if (quickHelpIcon) {
47981
49559
  quickHelpIcon.innerHTML = iconSvg("octicon-question", QUESTION_16_PATH);
47982
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
+ }
47983
49567
  const branchIcon = document.querySelector(
47984
49568
  "#project-branch .goi-icon"
47985
49569
  );
@@ -48034,6 +49618,10 @@ ${error2.stack}` : ""}`
48034
49618
  event.preventDefault();
48035
49619
  openTerminalSheet();
48036
49620
  });
49621
+ $("#panel-tab-search")?.addEventListener("click", (event) => {
49622
+ event.preventDefault();
49623
+ openSearchSheet();
49624
+ });
48037
49625
  $("#app-panel-close")?.addEventListener("click", (event) => {
48038
49626
  event.preventDefault();
48039
49627
  closeAppPanel();
@@ -48269,6 +49857,9 @@ ${error2.stack}` : ""}`
48269
49857
  syncSidebarFilterClearButton();
48270
49858
  sbFilterClear.addEventListener("click", clearSidebarFilter);
48271
49859
  }
49860
+ document.querySelector("#search-btn")?.addEventListener("click", (event) => {
49861
+ openSearchPalette(event.shiftKey ? "grep" : "file");
49862
+ });
48272
49863
  function focusFileFilter() {
48273
49864
  const input2 = $("#sb-filter");
48274
49865
  input2.focus();
@@ -48459,11 +50050,18 @@ ${error2.stack}` : ""}`
48459
50050
  if (action === "goto-history") {
48460
50051
  navigateToRoute({
48461
50052
  screen: "history",
48462
- ref: "HEAD",
50053
+ ref: historyRefForCurrentView(),
48463
50054
  range: currentRange()
48464
50055
  });
48465
50056
  return true;
48466
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
+ }
48467
50065
  if (action === "goto-repo") {
48468
50066
  navigateToRoute({
48469
50067
  screen: "repo",
@@ -48781,6 +50379,7 @@ ${error2.stack}` : ""}`
48781
50379
  syncDoctorSheetFromUrl();
48782
50380
  syncToolsSheetFromUrl();
48783
50381
  syncTerminalSheetFromUrl();
50382
+ syncSearchSheetFromUrl();
48784
50383
  });
48785
50384
  function syncRefInputs() {
48786
50385
  const fi = $("#ref-from"), ti = $("#ref-to");
@@ -48872,7 +50471,19 @@ ${error2.stack}` : ""}`
48872
50471
  },
48873
50472
  getSyntaxHighlight: () => STATE.syntaxHighlight,
48874
50473
  getLanguage: () => STATE.language,
48875
- 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)
48876
50487
  });
48877
50488
  relocalizeHistory = () => HISTORY_VIEW.localize();
48878
50489
  function helpSectionDeps() {
@@ -48973,14 +50584,16 @@ ${error2.stack}` : ""}`
48973
50584
  function syncAppPanel() {
48974
50585
  const tools = parseToolsOverlay(window.location.search) !== null;
48975
50586
  const terminal = parseTerminalOverlay(window.location.search) !== null;
48976
- const open2 = tools || terminal;
50587
+ const search = parseSearchResultsOverlay(window.location.search) !== null;
50588
+ const open2 = tools || terminal || search;
48977
50589
  const panel = document.getElementById("app-panel");
48978
50590
  if (panel) {
48979
50591
  panel.classList.toggle("app-panel-open", open2);
48980
50592
  }
48981
50593
  for (const [id, selected] of [
48982
50594
  ["#panel-tab-tools", tools],
48983
- ["#panel-tab-terminal", terminal]
50595
+ ["#panel-tab-terminal", terminal],
50596
+ ["#panel-tab-search", search]
48984
50597
  ]) {
48985
50598
  const tab = document.querySelector(id);
48986
50599
  if (!tab) continue;
@@ -49005,6 +50618,8 @@ ${error2.stack}` : ""}`
49005
50618
  syncAppPanelLayout();
49006
50619
  }
49007
50620
  function closeAppPanel() {
50621
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50622
+ closeSearchSheet();
49008
50623
  if (parseToolsOverlay(window.location.search) !== null || TOOLS_VIEW.isOpen())
49009
50624
  closeToolsSheet();
49010
50625
  if (parseTerminalOverlay(window.location.search) !== null || TERMINAL_VIEW.isOpen())
@@ -49053,6 +50668,8 @@ ${error2.stack}` : ""}`
49053
50668
  function openToolsSheet(tool) {
49054
50669
  if (parseTerminalOverlay(window.location.search) !== null || TERMINAL_VIEW.isOpen())
49055
50670
  closeTerminalSheet();
50671
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50672
+ closeSearchSheet();
49056
50673
  updateUrlForToolsOverlay(tool ?? TOOLS_VIEW.getActiveTool());
49057
50674
  void TOOLS_VIEW.open(tool);
49058
50675
  syncAppPanel();
@@ -49070,6 +50687,77 @@ ${error2.stack}` : ""}`
49070
50687
  else if (!tool && open2) TOOLS_VIEW.close();
49071
50688
  syncAppPanel();
49072
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
+ }
49073
50761
  const TERMINAL_VIEW = createTerminalView({
49074
50762
  $: (sel) => document.querySelector(sel),
49075
50763
  trackLoad,
@@ -49098,6 +50786,8 @@ ${error2.stack}` : ""}`
49098
50786
  function openTerminalSheet(id) {
49099
50787
  if (parseToolsOverlay(window.location.search) !== null || TOOLS_VIEW.isOpen())
49100
50788
  closeToolsSheet();
50789
+ if (parseSearchResultsOverlay(window.location.search) !== null || SEARCH_RESULTS_VIEW.isOpen())
50790
+ closeSearchSheet();
49101
50791
  const target = id ?? TERMINAL_VIEW.getActiveTarget();
49102
50792
  updateUrlForTerminalOverlay(target ?? "open");
49103
50793
  void TERMINAL_VIEW.open(target);
@@ -49175,7 +50865,17 @@ ${error2.stack}` : ""}`
49175
50865
  getFrom: () => STATE.from,
49176
50866
  getTo: () => STATE.to,
49177
50867
  getRepoRef: () => STATE.repoRef,
49178
- 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
49179
50879
  });
49180
50880
  if (REF_PICKER) {
49181
50881
  const historyRefInput = document.querySelector("#history-ref");
@@ -49232,6 +50932,7 @@ ${error2.stack}` : ""}`
49232
50932
  syncDoctorSheetFromUrl();
49233
50933
  syncToolsSheetFromUrl();
49234
50934
  syncTerminalSheetFromUrl();
50935
+ syncSearchSheetFromUrl();
49235
50936
  if (isSameBlobFileRoute(previousRoute, STATE.route) && routeBlobPreview(previousRoute) !== routeBlobPreview(STATE.route) && switchSourceTab(routeBlobPreview(STATE.route) ? "preview" : "code", {
49236
50937
  updateRoute: false
49237
50938
  })) {