@youtyan/code-viewer 0.1.11 → 0.1.12

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
@@ -131,6 +131,164 @@
131
131
  };
132
132
  }
133
133
 
134
+ // web-src/fuzzy-search.ts
135
+ function basenameStart(path) {
136
+ const slash = path.lastIndexOf("/");
137
+ return slash < 0 ? 0 : slash + 1;
138
+ }
139
+ function isBoundary(path, index) {
140
+ if (index <= 0)
141
+ return true;
142
+ const prev = path[index - 1];
143
+ return prev === "/" || prev === "-" || prev === "_" || prev === "." || prev === " ";
144
+ }
145
+ function toRanges(indices) {
146
+ const ranges = [];
147
+ for (const index of indices) {
148
+ const last = ranges[ranges.length - 1];
149
+ if (last && last.end === index) {
150
+ last.end = index + 1;
151
+ } else {
152
+ ranges.push({ start: index, end: index + 1 });
153
+ }
154
+ }
155
+ return ranges;
156
+ }
157
+ function fuzzyMatchPath(query, path) {
158
+ const q = query.trim().toLowerCase();
159
+ if (!q)
160
+ return { score: 0, ranges: [] };
161
+ const lowerPath = path.toLowerCase();
162
+ const baseStart = basenameStart(path);
163
+ const indices = [];
164
+ let from = 0;
165
+ let score = 0;
166
+ for (const ch of q) {
167
+ const index = lowerPath.indexOf(ch, from);
168
+ if (index < 0)
169
+ return null;
170
+ indices.push(index);
171
+ score += 10;
172
+ if (index >= baseStart)
173
+ score += 8;
174
+ if (isBoundary(path, index))
175
+ score += 6;
176
+ const prev = indices[indices.length - 2];
177
+ if (prev != null && prev + 1 === index)
178
+ score += 12;
179
+ from = index + 1;
180
+ }
181
+ const first = indices[0] || 0;
182
+ score -= Math.min(first, 40);
183
+ if (indices[0] >= baseStart)
184
+ score += 20;
185
+ const basename = lowerPath.slice(baseStart);
186
+ if (basename.startsWith(q))
187
+ score += 30;
188
+ if (basename === q || basename.startsWith(q + "."))
189
+ score += 25;
190
+ if (lowerPath.endsWith(q))
191
+ score += 15;
192
+ return { score, ranges: toRanges(indices) };
193
+ }
194
+ function rankFuzzyPaths(query, items) {
195
+ return items.map((item) => {
196
+ const match = fuzzyMatchPath(query, item.path);
197
+ return match ? { item, score: match.score, ranges: match.ranges } : null;
198
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
199
+ }
200
+ function isGlobPathQuery(query) {
201
+ return /[*?]/.test(query.trim());
202
+ }
203
+ function escapeRegexChar(ch) {
204
+ return /[\\^$+?.()|{}]/.test(ch) ? "\\" + ch : ch;
205
+ }
206
+ function globToRegExp(query) {
207
+ const pattern = query.trim();
208
+ if (!pattern)
209
+ return null;
210
+ let source = "^";
211
+ for (let i = 0;i < pattern.length; i++) {
212
+ const ch = pattern[i];
213
+ if (ch === "*") {
214
+ if (pattern[i + 1] === "*") {
215
+ source += ".*";
216
+ i++;
217
+ } else {
218
+ source += "[^/]*";
219
+ }
220
+ } else if (ch === "?") {
221
+ source += "[^/]";
222
+ } else if (ch === "[") {
223
+ const close = pattern.indexOf("]", i + 1);
224
+ if (close < 0) {
225
+ source += "\\[";
226
+ } else {
227
+ const body = pattern.slice(i + 1, close).replace(/\\/g, "\\\\");
228
+ source += "[" + body + "]";
229
+ i = close;
230
+ }
231
+ } else {
232
+ source += escapeRegexChar(ch);
233
+ }
234
+ }
235
+ source += "$";
236
+ try {
237
+ return new RegExp(source, "i");
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+ function globMatchPath(query, path) {
243
+ const regex = globToRegExp(query);
244
+ const baseStart = basenameStart(path);
245
+ const basename = path.slice(baseStart);
246
+ if (!regex || !regex.test(path) && (query.includes("/") || !regex.test(basename)))
247
+ return null;
248
+ const literal = query.replace(/[*?[\]]+/g, " ").trim().split(/\s+/).filter(Boolean);
249
+ const ranges = [];
250
+ const lowerPath = path.toLowerCase();
251
+ for (const part of literal) {
252
+ const start = lowerPath.indexOf(part.toLowerCase());
253
+ if (start >= 0)
254
+ ranges.push({ start, end: start + part.length });
255
+ }
256
+ ranges.sort((a, b) => a.start - b.start || a.end - b.end);
257
+ const mergedRanges = [];
258
+ for (const range of ranges) {
259
+ const last = mergedRanges[mergedRanges.length - 1];
260
+ if (last && last.end >= range.start) {
261
+ last.end = Math.max(last.end, range.end);
262
+ } else {
263
+ mergedRanges.push({ ...range });
264
+ }
265
+ }
266
+ const score = 1000 - Math.min(path.length, 200) + (path.slice(baseStart).toLowerCase().endsWith(query.replace(/^\*+/, "").toLowerCase()) ? 50 : 0);
267
+ return { score, ranges: mergedRanges };
268
+ }
269
+ function rankPathMatches(query, items) {
270
+ if (isGlobPathQuery(query)) {
271
+ return items.map((item) => {
272
+ const match = globMatchPath(query, item.path);
273
+ return match ? { item, score: match.score, ranges: match.ranges, mode: "glob" } : null;
274
+ }).filter((item) => item !== null).sort((a, b) => b.score - a.score || a.item.path.localeCompare(b.item.path));
275
+ }
276
+ return rankFuzzyPaths(query, items).map((item) => ({ ...item, mode: "fuzzy" }));
277
+ }
278
+
279
+ // web-src/search-palette.ts
280
+ var PALETTE_RESULT_LIMIT = 50;
281
+ function limitPaletteResults(items) {
282
+ return items.slice(0, PALETTE_RESULT_LIMIT);
283
+ }
284
+ function movePaletteSelection(index, count, direction) {
285
+ if (count <= 0)
286
+ return -1;
287
+ if (index < 0)
288
+ return direction > 0 ? 0 : count - 1;
289
+ return (index + direction + count) % count;
290
+ }
291
+
134
292
  // web-src/catch-up.ts
135
293
  function shouldCatchUpDiff(route) {
136
294
  return route.screen !== "repo" && !(route.screen === "file" && route.view === "blob");
@@ -160,6 +318,24 @@
160
318
  to: raw.slice(sep + 2) || fallback.to
161
319
  };
162
320
  }
321
+ function parseLineTarget(value) {
322
+ const raw = value || "";
323
+ const range = /^(\d+)-(\d+)$/.exec(raw);
324
+ if (range) {
325
+ const a = Number(range[1]);
326
+ const b = Number(range[2]);
327
+ const start = Math.min(a, b);
328
+ const end = Math.max(a, b);
329
+ if (start > 0)
330
+ return { start, end };
331
+ return;
332
+ }
333
+ const line = Number(raw);
334
+ return Number.isInteger(line) && line > 0 ? line : undefined;
335
+ }
336
+ function formatLineTarget(line) {
337
+ return typeof line === "number" ? String(line) : line.start + "-" + line.end;
338
+ }
163
339
  function parseRoute(pathname, search, fallbackRange) {
164
340
  const params = new URLSearchParams(search);
165
341
  const legacyRange = parseLegacyRange(params.get("range"), fallbackRange);
@@ -178,14 +354,20 @@
178
354
  };
179
355
  case "/todif":
180
356
  case "/todiff":
181
- return { screen: "diff", range };
357
+ return {
358
+ screen: "diff",
359
+ range,
360
+ ...params.get("path") ? { path: params.get("path") || "" } : {},
361
+ ...parseLineTarget(params.get("line")) ? { line: parseLineTarget(params.get("line")) } : {}
362
+ };
182
363
  case "/file": {
183
364
  const path = params.get("path") || "";
184
365
  const target = params.get("target") || "";
185
366
  const ref = target || params.get("ref") || "worktree";
367
+ const line = parseLineTarget(params.get("line"));
186
368
  if (!path)
187
369
  return { screen: "unknown", reason: "missing-path", rawPathname: pathname, rawSearch: search, range };
188
- return { screen: "file", path, ref, range, view: target ? "blob" : "detail" };
370
+ return { screen: "file", path, ref, range, view: target ? "blob" : "detail", ...line ? { line } : {} };
189
371
  }
190
372
  default:
191
373
  return { screen: "unknown", reason: "unknown-pathname", rawPathname: pathname, rawSearch: search, range };
@@ -204,11 +386,11 @@
204
386
  }
205
387
  case "file":
206
388
  if (route.view === "blob") {
207
- return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree");
389
+ return "/file?path=" + encodeURIComponent(route.path) + "&target=" + encodeURIComponent(route.ref || "worktree") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
208
390
  }
209
- return "/file?path=" + encodeURIComponent(route.path) + "&ref=" + encodeURIComponent(route.ref || "worktree") + "&from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
391
+ 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)) : "");
210
392
  case "diff":
211
- return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
393
+ return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree") + (route.path ? "&path=" + encodeURIComponent(route.path) : "") + (route.line ? "&line=" + encodeURIComponent(formatLineTarget(route.line)) : "");
212
394
  case "unknown":
213
395
  return "/todif?from=" + encodeURIComponent(route.range.from || "") + "&to=" + encodeURIComponent(route.range.to || "worktree");
214
396
  default:
@@ -6954,7 +7136,41 @@
6954
7136
  return;
6955
7137
  enqueueLoad(f2, card, 5);
6956
7138
  }
6957
- function scrollToFile(path) {
7139
+ function clearDiffLineFocus() {
7140
+ document.querySelectorAll(".gdp-diff-line-target").forEach((row) => {
7141
+ row.classList.remove("gdp-diff-line-target");
7142
+ });
7143
+ }
7144
+ function diffRowLineNumber(row) {
7145
+ const newLine = row.querySelector(".line-num2, td.d2h-code-side-linenumber");
7146
+ const raw = (newLine?.textContent || "").trim();
7147
+ const line = Number(raw);
7148
+ return Number.isInteger(line) && line > 0 ? line : null;
7149
+ }
7150
+ function focusDiffLine(card, line) {
7151
+ const start = lineTargetStart(line);
7152
+ if (!start)
7153
+ return false;
7154
+ const rows = Array.from(card.querySelectorAll("table.d2h-diff-table tr"));
7155
+ const row = rows.find((candidate) => diffRowLineNumber(candidate) === start);
7156
+ if (!row)
7157
+ return false;
7158
+ clearDiffLineFocus();
7159
+ row.classList.add("gdp-diff-line-target");
7160
+ row.scrollIntoView({ behavior: "smooth", block: "center" });
7161
+ return true;
7162
+ }
7163
+ function applyDiffRouteFocus(card) {
7164
+ if (STATE.route.screen !== "diff" || !STATE.route.path || !STATE.route.line)
7165
+ return false;
7166
+ if (card && card.dataset.path !== STATE.route.path)
7167
+ return false;
7168
+ const targetCard = card || document.querySelector(diffCardSelector(STATE.route.path));
7169
+ if (!targetCard)
7170
+ return false;
7171
+ return focusDiffLine(targetCard, STATE.route.line);
7172
+ }
7173
+ function scrollToFile(path, line) {
6958
7174
  const card = document.querySelector(diffCardSelector(path));
6959
7175
  if (!card)
6960
7176
  return;
@@ -6970,7 +7186,9 @@
6970
7186
  if (f2)
6971
7187
  enqueueLoad(f2, card, 10);
6972
7188
  }
6973
- card.scrollIntoView({ behavior: "smooth", block: "start" });
7189
+ if (!line || !focusDiffLine(card, line)) {
7190
+ card.scrollIntoView({ behavior: "smooth", block: "start" });
7191
+ }
6974
7192
  }
6975
7193
  function markActive(path) {
6976
7194
  STATE.activeFile = path;
@@ -8727,9 +8945,12 @@
8727
8945
  return false;
8728
8946
  const line = lines[index];
8729
8947
  const tr = document.createElement("tr");
8948
+ tr.dataset.line = String(index + 1);
8949
+ tr.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
8730
8950
  const num = document.createElement("td");
8731
8951
  num.className = "gdp-source-line-number";
8732
8952
  num.textContent = String(index + 1);
8953
+ bindSourceLineNumber(num, card, target, index + 1);
8733
8954
  const code2 = document.createElement("td");
8734
8955
  code2.className = "gdp-source-line-code";
8735
8956
  if (shikiLines && shikiLines[index] != null) {
@@ -8821,6 +9042,69 @@
8821
9042
  url.searchParams.delete("virtual");
8822
9043
  return url.pathname + url.search;
8823
9044
  }
9045
+ function currentSourceLineTarget(target) {
9046
+ const routeTarget = sourceTargetFromRoute();
9047
+ return sourceTargetsEqual(routeTarget, target) && STATE.route.screen === "file" ? STATE.route.line : undefined;
9048
+ }
9049
+ function lineTargetStart(line) {
9050
+ if (!line)
9051
+ return;
9052
+ return typeof line === "number" ? line : line.start;
9053
+ }
9054
+ function lineInSourceTarget(lineNumber, target) {
9055
+ if (!target)
9056
+ return false;
9057
+ if (typeof target === "number")
9058
+ return lineNumber === target;
9059
+ return lineNumber >= target.start && lineNumber <= target.end;
9060
+ }
9061
+ let SOURCE_LINE_DRAG = null;
9062
+ function normalizeSourceLineSelection(start, end) {
9063
+ const a2 = Math.max(1, Math.floor(start));
9064
+ const b2 = Math.max(1, Math.floor(end));
9065
+ const from = Math.min(a2, b2);
9066
+ const to = Math.max(a2, b2);
9067
+ return from === to ? from : { start: from, end: to };
9068
+ }
9069
+ function setSourceLineRoute(target, line) {
9070
+ if (STATE.route.screen !== "file")
9071
+ return;
9072
+ setRoute({
9073
+ screen: "file",
9074
+ path: target.path,
9075
+ ref: target.ref,
9076
+ view: STATE.route.view,
9077
+ range: currentRange(),
9078
+ line
9079
+ }, true);
9080
+ }
9081
+ function syncRenderedSourceLineHighlights(card, target) {
9082
+ const lineTarget = currentSourceLineTarget(target);
9083
+ card.querySelectorAll("[data-line]").forEach((row) => {
9084
+ const line = Number(row.dataset.line || "0");
9085
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(line, lineTarget));
9086
+ });
9087
+ }
9088
+ function updateSourceLineSelection(card, target, start, end) {
9089
+ setSourceLineRoute(target, normalizeSourceLineSelection(start, end));
9090
+ syncRenderedSourceLineHighlights(card, target);
9091
+ }
9092
+ function beginSourceLineSelection(event, card, target, line) {
9093
+ event.preventDefault();
9094
+ SOURCE_LINE_DRAG = { target, start: line };
9095
+ updateSourceLineSelection(card, target, line, line);
9096
+ }
9097
+ function bindSourceLineNumber(num, card, target, line) {
9098
+ num.addEventListener("mousedown", (e2) => beginSourceLineSelection(e2, card, target, line));
9099
+ num.addEventListener("mouseenter", () => {
9100
+ if (!SOURCE_LINE_DRAG || !sourceTargetsEqual(SOURCE_LINE_DRAG.target, target))
9101
+ return;
9102
+ updateSourceLineSelection(card, target, SOURCE_LINE_DRAG.start, line);
9103
+ });
9104
+ }
9105
+ document.addEventListener("mouseup", () => {
9106
+ SOURCE_LINE_DRAG = null;
9107
+ });
8824
9108
  function renderVirtualSource(target, textValue, lines, hljsRef, lang) {
8825
9109
  const wrap = document.createElement("div");
8826
9110
  wrap.className = "gdp-source-virtual";
@@ -8859,7 +9143,8 @@
8859
9143
  full.title = "Render every line without virtualization. This can be slow for large files.";
8860
9144
  full.addEventListener("click", (e2) => {
8861
9145
  e2.preventDefault();
8862
- history.pushState(null, "", full.href);
9146
+ const url = new URL(full.href, window.location.origin);
9147
+ setRoute(parseRoute(url.pathname, url.search, currentRange()), true);
8863
9148
  renderStandaloneSource(target);
8864
9149
  });
8865
9150
  actions.append(copy, full);
@@ -8893,9 +9178,12 @@
8893
9178
  for (let index = start;index < end; index++) {
8894
9179
  const row = document.createElement("div");
8895
9180
  row.className = "gdp-source-virtual-row";
9181
+ row.dataset.line = String(index + 1);
9182
+ row.classList.toggle("gdp-source-line-target", lineInSourceTarget(index + 1, currentSourceLineTarget(target)));
8896
9183
  const num = document.createElement("span");
8897
9184
  num.className = "gdp-source-virtual-line-number";
8898
9185
  num.textContent = String(index + 1);
9186
+ bindSourceLineNumber(num, wrap, target, index + 1);
8899
9187
  const code2 = document.createElement("span");
8900
9188
  code2.className = "gdp-source-virtual-line-code";
8901
9189
  const line = lines[index] ?? "";
@@ -9143,6 +9431,7 @@
9143
9431
  return;
9144
9432
  if (!rendered)
9145
9433
  return;
9434
+ scrollStandaloneSourceLine(card, lineTargetStart(STATE.route.screen === "file" ? STATE.route.line : undefined));
9146
9435
  finishSourceLoad(req);
9147
9436
  }
9148
9437
  } catch (err) {
@@ -9156,6 +9445,19 @@
9156
9445
  renderSourceError(card, target, "Cannot load " + target.path + " at " + target.ref);
9157
9446
  }
9158
9447
  }
9448
+ function scrollStandaloneSourceLine(card, line) {
9449
+ if (!line || line < 1)
9450
+ return;
9451
+ const virtualScroller = card.querySelector(".gdp-source-virtual-scroller");
9452
+ if (virtualScroller) {
9453
+ const centeredOffset = virtualScroller.clientHeight / 2 - VIRTUAL_SOURCE_ROW_HEIGHT / 2;
9454
+ virtualScroller.scrollTop = Math.max(0, (line - 1) * VIRTUAL_SOURCE_ROW_HEIGHT - Math.max(0, centeredOffset));
9455
+ return;
9456
+ }
9457
+ const row = card.querySelector('.gdp-source-table tr[data-line="' + String(line) + '"]');
9458
+ if (row)
9459
+ row.scrollIntoView({ block: "center" });
9460
+ }
9159
9461
  function applySourceRouteToShell() {
9160
9462
  const target = sourceTargetFromRoute();
9161
9463
  setPageMode();
@@ -9344,6 +9646,7 @@
9344
9646
  card.classList.add("loaded");
9345
9647
  card.style.minHeight = "";
9346
9648
  mountDiff(card, file, data);
9649
+ applyDiffRouteFocus(card);
9347
9650
  card.style.containIntrinsicSize = Math.max(card.offsetHeight, file.estimated_height_px || 200) + "px";
9348
9651
  applyViewedToCard(card, STATE.viewedFiles.has(file.path), true);
9349
9652
  if (data.truncated && data.mode === "preview") {
@@ -9775,10 +10078,409 @@
9775
10078
  input.focus();
9776
10079
  input.select();
9777
10080
  }
10081
+ let PALETTE = null;
10082
+ const REPO_FILE_CACHE = new Map;
10083
+ function paletteSource() {
10084
+ if (STATE.route.screen === "diff")
10085
+ return "diff";
10086
+ if (STATE.route.screen === "file" && STATE.route.view !== "blob")
10087
+ return "diff";
10088
+ return "repo";
10089
+ }
10090
+ function paletteRef(source) {
10091
+ if (source === "diff")
10092
+ return STATE.to && STATE.to !== "worktree" ? STATE.to : "worktree";
10093
+ if (STATE.route.screen === "repo")
10094
+ return STATE.route.ref || "worktree";
10095
+ if (STATE.route.screen === "file")
10096
+ return STATE.route.ref || "worktree";
10097
+ return STATE.repoRef || "worktree";
10098
+ }
10099
+ function closeSearchPalette() {
10100
+ if (!PALETTE)
10101
+ return;
10102
+ PALETTE.controller?.abort();
10103
+ if (PALETTE.debounce)
10104
+ window.clearTimeout(PALETTE.debounce);
10105
+ PALETTE.root.remove();
10106
+ PALETTE = null;
10107
+ }
10108
+ function createPalette(mode) {
10109
+ closeSearchPalette();
10110
+ const root = document.createElement("div");
10111
+ root.className = "gdp-palette-backdrop";
10112
+ const dialog = document.createElement("div");
10113
+ dialog.className = "gdp-palette";
10114
+ dialog.setAttribute("role", "dialog");
10115
+ dialog.setAttribute("aria-modal", "true");
10116
+ const label = document.createElement("div");
10117
+ label.className = "gdp-palette-label";
10118
+ label.textContent = mode === "file" ? "Files" : "Grep";
10119
+ const input = document.createElement("input");
10120
+ input.className = "gdp-palette-input";
10121
+ input.type = "search";
10122
+ input.autocomplete = "off";
10123
+ input.spellcheck = false;
10124
+ input.placeholder = mode === "file" ? "Search files" : "Search text";
10125
+ input.setAttribute("role", "combobox");
10126
+ input.setAttribute("aria-expanded", "true");
10127
+ input.setAttribute("aria-controls", "gdp-palette-list");
10128
+ const status = document.createElement("div");
10129
+ status.className = "gdp-palette-status";
10130
+ const controls = document.createElement("div");
10131
+ controls.className = "gdp-palette-controls";
10132
+ const list2 = document.createElement("div");
10133
+ list2.id = "gdp-palette-list";
10134
+ list2.className = "gdp-palette-list";
10135
+ list2.setAttribute("role", "listbox");
10136
+ dialog.append(label, input, controls, status, list2);
10137
+ root.appendChild(dialog);
10138
+ document.body.appendChild(root);
10139
+ const state = {
10140
+ root,
10141
+ input,
10142
+ controls,
10143
+ list: list2,
10144
+ status,
10145
+ mode,
10146
+ grepRegex: false,
10147
+ selected: -1,
10148
+ items: [],
10149
+ composing: false,
10150
+ diffSnapshot: [...STATE.files]
10151
+ };
10152
+ PALETTE = state;
10153
+ root.addEventListener("mousedown", (e2) => {
10154
+ if (e2.target === root)
10155
+ closeSearchPalette();
10156
+ });
10157
+ input.addEventListener("compositionstart", () => {
10158
+ state.composing = true;
10159
+ });
10160
+ input.addEventListener("compositionend", () => {
10161
+ state.composing = false;
10162
+ });
10163
+ input.addEventListener("input", () => updatePaletteResults(state));
10164
+ input.addEventListener("keydown", (e2) => handlePaletteKeydown(e2, state));
10165
+ input.focus();
10166
+ updatePaletteResults(state);
10167
+ return state;
10168
+ }
10169
+ function renderPaletteControls(state) {
10170
+ state.controls.innerHTML = "";
10171
+ if (state.mode === "file") {
10172
+ const hint2 = document.createElement("span");
10173
+ hint2.className = "gdp-palette-mode-hint";
10174
+ hint2.textContent = isGlobPathQuery(state.input.value) ? "Glob: * ? []" : "Fuzzy path search";
10175
+ state.controls.appendChild(hint2);
10176
+ return;
10177
+ }
10178
+ const plain = document.createElement("button");
10179
+ plain.type = "button";
10180
+ plain.className = "gdp-palette-mode-button";
10181
+ plain.setAttribute("aria-pressed", String(!state.grepRegex));
10182
+ plain.textContent = "Plain";
10183
+ plain.addEventListener("mousedown", (e2) => {
10184
+ e2.preventDefault();
10185
+ state.grepRegex = false;
10186
+ renderPaletteControls(state);
10187
+ updatePaletteResults(state);
10188
+ state.input.focus();
10189
+ });
10190
+ const regex = document.createElement("button");
10191
+ regex.type = "button";
10192
+ regex.className = "gdp-palette-mode-button";
10193
+ regex.setAttribute("aria-pressed", String(state.grepRegex));
10194
+ regex.textContent = ".* Regex";
10195
+ regex.title = "Alt+R";
10196
+ regex.addEventListener("mousedown", (e2) => {
10197
+ e2.preventDefault();
10198
+ state.grepRegex = true;
10199
+ renderPaletteControls(state);
10200
+ updatePaletteResults(state);
10201
+ state.input.focus();
10202
+ });
10203
+ const hint = document.createElement("span");
10204
+ hint.className = "gdp-palette-mode-hint";
10205
+ hint.textContent = "Alt+R toggles regex";
10206
+ state.controls.append(plain, regex, hint);
10207
+ }
10208
+ function regexQueryIsValid(query) {
10209
+ try {
10210
+ new RegExp(query);
10211
+ return true;
10212
+ } catch {
10213
+ return false;
10214
+ }
10215
+ }
10216
+ function appendHighlightedPath(parent, path, ranges) {
10217
+ let cursor = 0;
10218
+ for (const range of ranges) {
10219
+ if (range.start > cursor)
10220
+ parent.appendChild(document.createTextNode(path.slice(cursor, range.start)));
10221
+ const mark = document.createElement("mark");
10222
+ mark.textContent = path.slice(range.start, range.end);
10223
+ parent.appendChild(mark);
10224
+ cursor = range.end;
10225
+ }
10226
+ if (cursor < path.length)
10227
+ parent.appendChild(document.createTextNode(path.slice(cursor)));
10228
+ }
10229
+ function renderPalette(state) {
10230
+ state.list.innerHTML = "";
10231
+ state.items.forEach((item, index) => {
10232
+ const row = document.createElement("button");
10233
+ row.type = "button";
10234
+ row.id = "gdp-palette-item-" + index;
10235
+ row.className = "gdp-palette-row";
10236
+ row.setAttribute("role", "option");
10237
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10238
+ const title = document.createElement("span");
10239
+ title.className = "gdp-palette-row-title";
10240
+ const detail = document.createElement("span");
10241
+ detail.className = "gdp-palette-row-detail";
10242
+ if (item.kind === "file") {
10243
+ title.textContent = item.path.split("/").pop() || item.path;
10244
+ appendHighlightedPath(detail, item.displayPath, item.ranges);
10245
+ if (item.old_path && item.displayPath !== item.old_path) {
10246
+ detail.appendChild(document.createTextNode(" " + item.old_path));
10247
+ }
10248
+ } else {
10249
+ title.textContent = item.path + ":" + item.line;
10250
+ detail.textContent = item.preview;
10251
+ }
10252
+ row.append(title, detail);
10253
+ row.addEventListener("mouseenter", () => {
10254
+ state.selected = index;
10255
+ syncPaletteSelection(state);
10256
+ });
10257
+ row.addEventListener("mousedown", (e2) => {
10258
+ e2.preventDefault();
10259
+ state.selected = index;
10260
+ selectPaletteItem(state);
10261
+ });
10262
+ state.list.appendChild(row);
10263
+ });
10264
+ syncPaletteSelection(state);
10265
+ }
10266
+ function syncPaletteSelection(state) {
10267
+ state.input.setAttribute("aria-activedescendant", state.selected >= 0 ? "gdp-palette-item-" + state.selected : "");
10268
+ state.list.querySelectorAll(".gdp-palette-row").forEach((row, index) => {
10269
+ row.setAttribute("aria-selected", index === state.selected ? "true" : "false");
10270
+ if (index === state.selected)
10271
+ row.scrollIntoView({ block: "nearest" });
10272
+ });
10273
+ }
10274
+ async function repoPaletteFiles(ref) {
10275
+ const cached = REPO_FILE_CACHE.get(ref);
10276
+ if (cached && cached.generation === SERVER_GENERATION)
10277
+ return cached;
10278
+ const params = new URLSearchParams;
10279
+ params.set("ref", ref);
10280
+ const res = await trackLoad(fetch("/_files?" + params.toString()).then((r2) => {
10281
+ if (!r2.ok)
10282
+ throw new Error("failed to load files");
10283
+ return r2.json();
10284
+ }));
10285
+ REPO_FILE_CACHE.set(ref, res);
10286
+ return res;
10287
+ }
10288
+ function diffFilePaletteItems(state, query) {
10289
+ const matchPath = isGlobPathQuery(query) ? globMatchPath : fuzzyMatchPath;
10290
+ const candidates = state.diffSnapshot.map((file) => {
10291
+ const current = matchPath(query, file.path);
10292
+ const old = file.old_path ? matchPath(query, file.old_path) : null;
10293
+ const best = old && (!current || old.score > current.score) ? { match: old, displayPath: file.old_path || file.path } : current ? { match: current, displayPath: file.path } : null;
10294
+ return best ? { file, ...best } : null;
10295
+ }).filter((item) => item !== null).sort((a2, b2) => b2.match.score - a2.match.score || a2.file.path.localeCompare(b2.file.path));
10296
+ return limitPaletteResults(candidates).map((candidate) => ({
10297
+ kind: "file",
10298
+ path: candidate.file.path,
10299
+ old_path: candidate.file.old_path,
10300
+ displayPath: candidate.displayPath,
10301
+ ref: paletteRef("diff"),
10302
+ targetPath: fileSourceTarget(candidate.file).path,
10303
+ targetRef: fileSourceTarget(candidate.file).ref,
10304
+ source: "diff",
10305
+ ranges: candidate.match.ranges
10306
+ }));
10307
+ }
10308
+ async function updateFilePalette(state, query) {
10309
+ renderPaletteControls(state);
10310
+ const source = paletteSource();
10311
+ if (!query.trim()) {
10312
+ const base2 = source === "diff" ? state.diffSnapshot.map((file) => {
10313
+ const target = fileSourceTarget(file);
10314
+ return { kind: "file", path: file.path, old_path: file.old_path, displayPath: file.path, ref: paletteRef(source), targetPath: target.path, targetRef: target.ref, source, ranges: [] };
10315
+ }) : [];
10316
+ state.items = limitPaletteResults(base2);
10317
+ state.selected = state.items.length ? 0 : -1;
10318
+ state.status.textContent = source === "diff" ? state.diffSnapshot.length + " diff files" : "Type to search repository files";
10319
+ renderPalette(state);
10320
+ return;
10321
+ }
10322
+ if (source === "diff") {
10323
+ state.items = diffFilePaletteItems(state, query);
10324
+ } else {
10325
+ state.status.textContent = "Loading files...";
10326
+ const ref = paletteRef(source);
10327
+ const response = await repoPaletteFiles(ref);
10328
+ if (PALETTE !== state || state.input.value !== query)
10329
+ return;
10330
+ state.items = limitPaletteResults(rankPathMatches(query, response.files)).map((match2) => ({
10331
+ kind: "file",
10332
+ path: match2.item.path,
10333
+ displayPath: match2.item.path,
10334
+ ref,
10335
+ source,
10336
+ ranges: match2.ranges
10337
+ }));
10338
+ }
10339
+ state.selected = state.items.length ? 0 : -1;
10340
+ state.status.textContent = state.items.length ? state.items.length + " results" : "No results";
10341
+ renderPalette(state);
10342
+ }
10343
+ function updateGrepPalette(state, query) {
10344
+ renderPaletteControls(state);
10345
+ state.controller?.abort();
10346
+ if (state.debounce)
10347
+ window.clearTimeout(state.debounce);
10348
+ if (!query.trim()) {
10349
+ state.items = [];
10350
+ state.selected = -1;
10351
+ state.status.textContent = "Type to grep";
10352
+ renderPalette(state);
10353
+ return;
10354
+ }
10355
+ if (state.grepRegex && !regexQueryIsValid(query)) {
10356
+ state.controller?.abort();
10357
+ state.items = [];
10358
+ state.selected = -1;
10359
+ state.status.textContent = "Invalid regular expression";
10360
+ renderPalette(state);
10361
+ return;
10362
+ }
10363
+ state.status.textContent = "Searching...";
10364
+ state.debounce = window.setTimeout(() => {
10365
+ const source = paletteSource();
10366
+ const ref = paletteRef(source);
10367
+ const params = new URLSearchParams;
10368
+ params.set("ref", ref);
10369
+ params.set("q", query);
10370
+ params.set("max", "200");
10371
+ if (state.grepRegex)
10372
+ params.set("regex", "1");
10373
+ if (source === "diff") {
10374
+ for (const file of state.diffSnapshot)
10375
+ params.append("path", file.path);
10376
+ }
10377
+ const controller = new AbortController;
10378
+ state.controller = controller;
10379
+ trackLoad(fetch("/_grep?" + params.toString(), { signal: controller.signal }).then((r2) => {
10380
+ if (!r2.ok)
10381
+ throw new Error("grep failed");
10382
+ return r2.json();
10383
+ })).then((response) => {
10384
+ if (PALETTE !== state || controller.signal.aborted)
10385
+ return;
10386
+ state.items = limitPaletteResults(response.matches.map((match2) => ({
10387
+ kind: "grep",
10388
+ path: match2.path,
10389
+ line: match2.line,
10390
+ column: match2.column,
10391
+ preview: match2.preview,
10392
+ ref,
10393
+ source
10394
+ })));
10395
+ state.selected = state.items.length ? 0 : -1;
10396
+ state.status.textContent = response.engine + (state.grepRegex ? " regex" : " plain") + (response.truncated ? " truncated" : "") + " - " + state.items.length + " results";
10397
+ renderPalette(state);
10398
+ }).catch((err) => {
10399
+ if (isAbortError(err))
10400
+ return;
10401
+ state.status.textContent = "Search failed";
10402
+ });
10403
+ }, 80);
10404
+ }
10405
+ function updatePaletteResults(state) {
10406
+ const query = state.input.value;
10407
+ if (state.mode === "file") {
10408
+ updateFilePalette(state, query).catch(() => {
10409
+ state.status.textContent = "Search failed";
10410
+ });
10411
+ } else {
10412
+ updateGrepPalette(state, query);
10413
+ }
10414
+ }
10415
+ function selectPaletteItem(state) {
10416
+ const item = state.items[state.selected];
10417
+ if (!item)
10418
+ return;
10419
+ closeSearchPalette();
10420
+ if (item.kind === "file") {
10421
+ if (item.source === "diff") {
10422
+ if (STATE.route.screen === "file") {
10423
+ setRoute({ screen: "file", path: item.targetPath || item.path, ref: item.targetRef || item.ref, range: currentRange() });
10424
+ applySourceRouteToShell();
10425
+ } else {
10426
+ scrollToFile(item.path);
10427
+ }
10428
+ } else {
10429
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", range: currentRange() });
10430
+ renderStandaloneSource({ path: item.path, ref: item.ref });
10431
+ }
10432
+ return;
10433
+ }
10434
+ if (item.source === "diff") {
10435
+ setRoute({ screen: "diff", range: currentRange(), path: item.path, line: item.line });
10436
+ scrollToFile(item.path, item.line);
10437
+ } else {
10438
+ setRoute({ screen: "file", path: item.path, ref: item.ref, view: "blob", line: item.line, range: currentRange() });
10439
+ renderStandaloneSource({ path: item.path, ref: item.ref });
10440
+ }
10441
+ }
10442
+ function handlePaletteKeydown(e2, state) {
10443
+ if (e2.key === "Escape") {
10444
+ e2.preventDefault();
10445
+ closeSearchPalette();
10446
+ return;
10447
+ }
10448
+ if (e2.key === "Enter") {
10449
+ if (state.composing)
10450
+ return;
10451
+ e2.preventDefault();
10452
+ selectPaletteItem(state);
10453
+ return;
10454
+ }
10455
+ if (state.mode === "grep" && e2.altKey && e2.key.toLowerCase() === "r") {
10456
+ e2.preventDefault();
10457
+ state.grepRegex = !state.grepRegex;
10458
+ updatePaletteResults(state);
10459
+ return;
10460
+ }
10461
+ const direction = e2.key === "ArrowDown" || e2.ctrlKey && e2.key.toLowerCase() === "n" ? 1 : e2.key === "ArrowUp" || e2.ctrlKey && e2.key.toLowerCase() === "p" ? -1 : 0;
10462
+ if (direction) {
10463
+ e2.preventDefault();
10464
+ state.selected = movePaletteSelection(state.selected, state.items.length, direction);
10465
+ syncPaletteSelection(state);
10466
+ }
10467
+ }
10468
+ function openSearchPalette(mode) {
10469
+ createPalette(mode);
10470
+ }
9778
10471
  document.addEventListener("keydown", (e2) => {
9779
10472
  if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "k") {
9780
10473
  e2.preventDefault();
9781
- focusFileFilter();
10474
+ if (PALETTE?.mode === "file")
10475
+ return;
10476
+ openSearchPalette("file");
10477
+ return;
10478
+ }
10479
+ if ((e2.metaKey || e2.ctrlKey) && e2.key.toLowerCase() === "g") {
10480
+ e2.preventDefault();
10481
+ if (PALETTE?.mode === "grep")
10482
+ return;
10483
+ openSearchPalette("grep");
9782
10484
  return;
9783
10485
  }
9784
10486
  const targetEl = e2.target;