@youtyan/code-viewer 0.1.24 → 0.1.26

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
@@ -6942,7 +6942,13 @@ ${frontmatter.yaml}
6942
6942
  let SIDEBAR_ROW_BY_PATH = new Map;
6943
6943
  let SIDEBAR_VIRTUAL_ACTIVE_PATH = "";
6944
6944
  const SIDEBAR_TREE_ITEMS_CACHE = new WeakMap;
6945
+ let REPO_SORT = {
6946
+ key: "name",
6947
+ direction: "asc"
6948
+ };
6945
6949
  let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
6950
+ let SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = [];
6951
+ const UNDO_STACK = [];
6946
6952
  let PENDING_G_SCOPE = null;
6947
6953
  let PENDING_G_UNTIL = 0;
6948
6954
  let SOURCE_CURSOR = null;
@@ -6950,6 +6956,7 @@ ${frontmatter.yaml}
6950
6956
  const HELP_LANGUAGES = ["en", "ja"];
6951
6957
  const HELP_SECTIONS = ["keybindings"];
6952
6958
  const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
6959
+ const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
6953
6960
  const SIDEBAR_FONT_SIZE_STORAGE_KEY = "gdp:sidebar-font-size";
6954
6961
  const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
6955
6962
  const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
@@ -6980,6 +6987,7 @@ ${frontmatter.yaml}
6980
6987
  "bin",
6981
6988
  "obj"
6982
6989
  ];
6990
+ const CLIENT_SCOPE_EXCLUDE_NAMES_DEFAULT = [".DS_Store"];
6983
6991
  const HELP_CONTENT = {
6984
6992
  en: {
6985
6993
  languageLabel: "Language",
@@ -7251,9 +7259,18 @@ ${frontmatter.yaml}
7251
7259
  ...new Set(raw.map((item) => item.trim()).filter((item) => item && item.length <= 64 && !item.includes("/") && !item.includes("\\") && item !== "." && item !== ".." && item !== ".git"))
7252
7260
  ].slice(0, 100).sort((a2, b2) => a2.localeCompare(b2));
7253
7261
  }
7262
+ function normalizeScopeExcludeNames(value) {
7263
+ const raw = Array.isArray(value) ? value : value.split(/[\n,]+/);
7264
+ return [
7265
+ ...new Set(raw.map((item) => item.trim()).filter((item) => item && item.length <= 128 && !item.includes("/") && !item.includes("\\") && item !== "." && item !== ".." && item !== ".git"))
7266
+ ].slice(0, 200).sort((a2, b2) => a2.localeCompare(b2));
7267
+ }
7254
7268
  function scopeOmitDirsStorageKey() {
7255
7269
  return SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
7256
7270
  }
7271
+ function scopeExcludeNamesStorageKey() {
7272
+ return SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
7273
+ }
7257
7274
  function setProjectName(project) {
7258
7275
  if (!project)
7259
7276
  return;
@@ -7271,16 +7288,36 @@ ${frontmatter.yaml}
7271
7288
  return normalizeScopeOmitDirs(raw);
7272
7289
  }
7273
7290
  }
7291
+ function savedScopeExcludeNames() {
7292
+ const raw = localStorage.getItem(scopeExcludeNamesStorageKey());
7293
+ if (raw == null)
7294
+ return null;
7295
+ try {
7296
+ const parsed = JSON.parse(raw);
7297
+ return normalizeScopeExcludeNames(Array.isArray(parsed) ? parsed : []);
7298
+ } catch {
7299
+ return normalizeScopeExcludeNames(raw);
7300
+ }
7301
+ }
7274
7302
  function serverScopeOmitDirsDefault() {
7275
7303
  return SERVER_SCOPE_OMIT_DIRS_DEFAULT.length ? SERVER_SCOPE_OMIT_DIRS_DEFAULT : CLIENT_SCOPE_OMIT_DIRS_DEFAULT;
7276
7304
  }
7305
+ function serverScopeExcludeNamesDefault() {
7306
+ return SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT.length ? SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT : CLIENT_SCOPE_EXCLUDE_NAMES_DEFAULT;
7307
+ }
7277
7308
  function effectiveScopeOmitDirs() {
7278
7309
  return savedScopeOmitDirs() ?? serverScopeOmitDirsDefault();
7279
7310
  }
7280
- function appendScopeOmitDirsParam(params) {
7281
- const saved = savedScopeOmitDirs();
7282
- if (saved != null)
7283
- params.set("omit_dirs", saved.join(","));
7311
+ function effectiveScopeExcludeNames() {
7312
+ return savedScopeExcludeNames() ?? serverScopeExcludeNamesDefault();
7313
+ }
7314
+ function appendScopeParams(params) {
7315
+ const omit = savedScopeOmitDirs();
7316
+ if (omit != null)
7317
+ params.set("omit_dirs", omit.join(","));
7318
+ const exclude = savedScopeExcludeNames();
7319
+ if (exclude != null)
7320
+ params.set("exclude_names", exclude.join(","));
7284
7321
  }
7285
7322
  function normalizeViewerFontSize(value) {
7286
7323
  return value === "compact" || value === "large" || value === "xlarge" ? value : "regular";
@@ -7315,8 +7352,9 @@ ${frontmatter.yaml}
7315
7352
  syncSidebarHeaderHeight();
7316
7353
  }
7317
7354
  function repoFileCacheKey(ref) {
7318
- const saved = savedScopeOmitDirs();
7319
- return `${ref}\x00${saved ? saved.join("\x00") : "server"}`;
7355
+ const omit = savedScopeOmitDirs();
7356
+ const exclude = savedScopeExcludeNames();
7357
+ return `${ref}\x00${omit ? omit.join("\x00") : "server"}\x00${exclude ? exclude.join("\x00") : "server"}`;
7320
7358
  }
7321
7359
  async function loadSettings() {
7322
7360
  try {
@@ -7326,6 +7364,7 @@ ${frontmatter.yaml}
7326
7364
  const settings = await res.json();
7327
7365
  setProjectName(settings.project || "");
7328
7366
  SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
7367
+ SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(settings.scope.exclude_names_effective);
7329
7368
  return settings;
7330
7369
  } catch {
7331
7370
  return null;
@@ -7667,7 +7706,7 @@ ${frontmatter.yaml}
7667
7706
  applySidebarHidden(!STATE.sidebarHidden);
7668
7707
  }
7669
7708
  function scopeOmitSourceLabel() {
7670
- return savedScopeOmitDirs() != null ? "Browser override" : "Server default";
7709
+ return savedScopeOmitDirs() != null || savedScopeExcludeNames() != null ? "Browser override" : "Server default";
7671
7710
  }
7672
7711
  function refreshRepositoryTreeAfterSettings() {
7673
7712
  REPO_FILE_CACHE.clear();
@@ -7683,15 +7722,18 @@ ${frontmatter.yaml}
7683
7722
  async function openScopeSettings() {
7684
7723
  const pop = document.querySelector("#scope-settings-popover");
7685
7724
  const input = document.querySelector("#scope-omit-dirs");
7725
+ const excludeInput = document.querySelector("#scope-exclude-names");
7686
7726
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
7687
7727
  const codeFontSize = document.querySelector("#code-font-size");
7688
7728
  const source = document.querySelector("#scope-omit-source");
7689
- if (!pop || !input || !sidebarFontSize || !codeFontSize || !source)
7729
+ if (!pop || !input || !excludeInput || !sidebarFontSize || !codeFontSize || !source)
7690
7730
  return;
7691
7731
  await loadSettings();
7692
7732
  sidebarFontSize.value = savedSidebarFontSize();
7693
7733
  codeFontSize.value = savedCodeFontSize();
7694
7734
  input.value = effectiveScopeOmitDirs().join(`
7735
+ `);
7736
+ excludeInput.value = effectiveScopeExcludeNames().join(`
7695
7737
  `);
7696
7738
  source.textContent = 'Saved for project "' + (PROJECT_NAME || "default") + '" in this browser. Source: ' + scopeOmitSourceLabel() + ". Used by tree, Ctrl+K, and Ctrl+G. Reset removes the browser override.";
7697
7739
  pop.hidden = false;
@@ -7704,15 +7746,17 @@ ${frontmatter.yaml}
7704
7746
  }
7705
7747
  function saveScopeSettings() {
7706
7748
  const input = document.querySelector("#scope-omit-dirs");
7749
+ const excludeInput = document.querySelector("#scope-exclude-names");
7707
7750
  const sidebarFontSize = document.querySelector("#sidebar-font-size");
7708
7751
  const codeFontSize = document.querySelector("#code-font-size");
7709
- if (!input || !sidebarFontSize || !codeFontSize)
7752
+ if (!input || !excludeInput || !sidebarFontSize || !codeFontSize)
7710
7753
  return;
7711
7754
  localStorage.setItem(SIDEBAR_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(sidebarFontSize.value));
7712
7755
  localStorage.setItem(CODE_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(codeFontSize.value));
7713
7756
  applySidebarFontSize();
7714
7757
  applyCodeFontSize();
7715
7758
  localStorage.setItem(scopeOmitDirsStorageKey(), JSON.stringify(normalizeScopeOmitDirs(input.value)));
7759
+ localStorage.setItem(scopeExcludeNamesStorageKey(), JSON.stringify(normalizeScopeExcludeNames(excludeInput.value)));
7716
7760
  closeScopeSettings();
7717
7761
  refreshRepositoryTreeAfterSettings();
7718
7762
  }
@@ -7722,6 +7766,7 @@ ${frontmatter.yaml}
7722
7766
  applySidebarFontSize("regular");
7723
7767
  applyCodeFontSize("regular");
7724
7768
  localStorage.removeItem(scopeOmitDirsStorageKey());
7769
+ localStorage.removeItem(scopeExcludeNamesStorageKey());
7725
7770
  closeScopeSettings();
7726
7771
  refreshRepositoryTreeAfterSettings();
7727
7772
  }
@@ -7805,6 +7850,8 @@ ${frontmatter.yaml}
7805
7850
  li.className = "tree-dir";
7806
7851
  li.tabIndex = -1;
7807
7852
  li.dataset.dirpath = dir.path;
7853
+ if (dir.children_omitted_reason)
7854
+ li.dataset.childrenOmittedReason = dir.children_omitted_reason;
7808
7855
  if (dir.explicit)
7809
7856
  li.dataset.explicit = "true";
7810
7857
  if (dir.children_omitted) {
@@ -7948,6 +7995,8 @@ ${frontmatter.yaml}
7948
7995
  li.className = "tree-dir";
7949
7996
  li.tabIndex = -1;
7950
7997
  li.dataset.dirpath = dir.path;
7998
+ if (dir.children_omitted_reason)
7999
+ li.dataset.childrenOmittedReason = dir.children_omitted_reason;
7951
8000
  if (dir.explicit)
7952
8001
  li.dataset.explicit = "true";
7953
8002
  if (dir.children_omitted) {
@@ -8940,6 +8989,224 @@ ${frontmatter.yaml}
8940
8989
  button.disabled = false;
8941
8990
  }
8942
8991
  }
8992
+ function closeRepoContextMenu() {
8993
+ document.querySelector(".gdp-context-menu")?.remove();
8994
+ }
8995
+ function closeTrashDialog() {
8996
+ document.querySelector(".gdp-trash-dialog-backdrop")?.remove();
8997
+ }
8998
+ function createTrashDialog(title, body, actions) {
8999
+ closeTrashDialog();
9000
+ const backdrop = document.createElement("div");
9001
+ backdrop.className = "gdp-trash-dialog-backdrop";
9002
+ const dialog = document.createElement("div");
9003
+ dialog.className = "gdp-trash-dialog";
9004
+ const titleId = "gdp-trash-dialog-title";
9005
+ const bodyId = "gdp-trash-dialog-body";
9006
+ dialog.setAttribute("role", "dialog");
9007
+ dialog.setAttribute("aria-modal", "true");
9008
+ dialog.setAttribute("aria-labelledby", titleId);
9009
+ dialog.setAttribute("aria-describedby", bodyId);
9010
+ const heading2 = document.createElement("div");
9011
+ heading2.id = titleId;
9012
+ heading2.className = "gdp-trash-dialog-title";
9013
+ heading2.textContent = title;
9014
+ const message = document.createElement("div");
9015
+ message.id = bodyId;
9016
+ message.className = "gdp-trash-dialog-body";
9017
+ message.textContent = body;
9018
+ const actionRow = document.createElement("div");
9019
+ actionRow.className = "gdp-trash-dialog-actions";
9020
+ actionRow.append(...actions);
9021
+ dialog.append(heading2, message, actionRow);
9022
+ backdrop.appendChild(dialog);
9023
+ document.body.appendChild(backdrop);
9024
+ return backdrop;
9025
+ }
9026
+ function confirmMoveToTrash(path, focusReturnTarget) {
9027
+ return new Promise((resolve) => {
9028
+ const previousFocus = focusReturnTarget || document.activeElement;
9029
+ const cancel = document.createElement("button");
9030
+ cancel.type = "button";
9031
+ cancel.className = "gdp-btn gdp-btn-sm";
9032
+ cancel.textContent = "Cancel";
9033
+ const move = document.createElement("button");
9034
+ move.type = "button";
9035
+ move.className = "gdp-btn gdp-btn-sm gdp-trash-dialog-danger";
9036
+ move.textContent = "Move to Trash";
9037
+ const done = (ok) => {
9038
+ document.removeEventListener("keydown", onKeydown);
9039
+ closeTrashDialog();
9040
+ previousFocus?.focus?.();
9041
+ resolve(ok);
9042
+ };
9043
+ const onKeydown = (event) => {
9044
+ if (event.key === "Escape") {
9045
+ event.preventDefault();
9046
+ event.stopPropagation();
9047
+ done(false);
9048
+ return;
9049
+ }
9050
+ if (event.key !== "Tab")
9051
+ return;
9052
+ const focusables = [cancel, move];
9053
+ const index = focusables.indexOf(document.activeElement);
9054
+ if (index < 0) {
9055
+ event.preventDefault();
9056
+ focusables[0].focus();
9057
+ return;
9058
+ }
9059
+ if (event.shiftKey && index <= 0) {
9060
+ event.preventDefault();
9061
+ focusables[focusables.length - 1].focus();
9062
+ } else if (!event.shiftKey && index === focusables.length - 1) {
9063
+ event.preventDefault();
9064
+ focusables[0].focus();
9065
+ }
9066
+ };
9067
+ cancel.addEventListener("click", () => done(false));
9068
+ move.addEventListener("click", () => done(true));
9069
+ const backdrop = createTrashDialog("Move to Trash?", `Move "${path}" to Trash?`, [cancel, move]);
9070
+ backdrop.addEventListener("pointerdown", (event) => {
9071
+ if (event.target === backdrop)
9072
+ done(false);
9073
+ });
9074
+ document.addEventListener("keydown", onKeydown);
9075
+ cancel.focus();
9076
+ });
9077
+ }
9078
+ function showTrashError(message) {
9079
+ const ok = document.createElement("button");
9080
+ ok.type = "button";
9081
+ ok.className = "gdp-btn gdp-btn-sm";
9082
+ ok.textContent = "OK";
9083
+ ok.addEventListener("click", closeTrashDialog);
9084
+ createTrashDialog("Trash failed", message, [ok]);
9085
+ ok.focus();
9086
+ }
9087
+ async function moveRepoPathToTrash(path) {
9088
+ const res = await fetch("/_trash_path", {
9089
+ method: "POST",
9090
+ headers: {
9091
+ "Content-Type": "application/json",
9092
+ "X-Code-Viewer-Action": "1"
9093
+ },
9094
+ body: JSON.stringify({ path })
9095
+ });
9096
+ if (!res.ok) {
9097
+ showTrashError(`Failed to move "${path}" to Trash: ${await res.text()}`);
9098
+ return false;
9099
+ }
9100
+ const body = await res.json();
9101
+ if (body.undo)
9102
+ UNDO_STACK.unshift(body.undo);
9103
+ return true;
9104
+ }
9105
+ async function runUndoAction(action) {
9106
+ if (action.type !== "trash")
9107
+ return false;
9108
+ const res = await fetch("/_restore_trash", {
9109
+ method: "POST",
9110
+ headers: {
9111
+ "Content-Type": "application/json",
9112
+ "X-Code-Viewer-Action": "1"
9113
+ },
9114
+ body: JSON.stringify(action.payload)
9115
+ });
9116
+ if (!res.ok) {
9117
+ showTrashError(`Failed to undo "${action.label}": ${await res.text()}`);
9118
+ return false;
9119
+ }
9120
+ return true;
9121
+ }
9122
+ async function undoLastAction() {
9123
+ const action = UNDO_STACK.shift();
9124
+ if (!action)
9125
+ return false;
9126
+ if (!await runUndoAction(action)) {
9127
+ UNDO_STACK.unshift(action);
9128
+ return true;
9129
+ }
9130
+ invalidateRepoSidebar();
9131
+ await load();
9132
+ return true;
9133
+ }
9134
+ async function requestMoveToTrash(path, onMoved, options = {}) {
9135
+ if (!await confirmMoveToTrash(path, options.focusReturnTarget))
9136
+ return;
9137
+ if (await moveRepoPathToTrash(path))
9138
+ onMoved();
9139
+ }
9140
+ function canTrashWorktreeRef(ref) {
9141
+ return ref === "worktree" || ref === "";
9142
+ }
9143
+ function showRepoContextMenu(event, entry, ref, onDeleted) {
9144
+ if (document.querySelector(".gdp-trash-dialog-backdrop"))
9145
+ return false;
9146
+ if (!canTrashWorktreeRef(ref))
9147
+ return false;
9148
+ if (entry.children_omitted_reason === "internal")
9149
+ return false;
9150
+ event.preventDefault();
9151
+ closeRepoContextMenu();
9152
+ const menu = document.createElement("div");
9153
+ menu.className = "gdp-context-menu";
9154
+ const anchor = event.target;
9155
+ const focusReturnTarget = anchor?.closest("li, .gdp-repo-row");
9156
+ const anchorRect = anchor?.closest("li, .gdp-repo-row")?.getBoundingClientRect();
9157
+ const anchorX = event.clientX > 0 ? event.clientX : anchorRect?.left || window.innerWidth / 2;
9158
+ const anchorY = event.clientY > 0 ? event.clientY : anchorRect?.bottom || window.innerHeight / 2;
9159
+ menu.style.left = `${anchorX}px`;
9160
+ menu.style.top = `${anchorY}px`;
9161
+ const trash = document.createElement("button");
9162
+ trash.type = "button";
9163
+ trash.className = "danger";
9164
+ trash.textContent = "Move to Trash...";
9165
+ trash.addEventListener("click", async () => {
9166
+ closeRepoContextMenu();
9167
+ await requestMoveToTrash(entry.path, onDeleted, { focusReturnTarget });
9168
+ });
9169
+ menu.appendChild(trash);
9170
+ document.body.appendChild(menu);
9171
+ const rect = menu.getBoundingClientRect();
9172
+ const left = Math.min(anchorX, window.innerWidth - rect.width - 8);
9173
+ const top = Math.min(anchorY, window.innerHeight - rect.height - 8);
9174
+ menu.style.left = `${Math.max(8, left)}px`;
9175
+ menu.style.top = `${Math.max(8, top)}px`;
9176
+ return true;
9177
+ }
9178
+ function sidebarTrashEntryFromEvent(event) {
9179
+ if (!isRepositorySidebarMode())
9180
+ return null;
9181
+ const row = event.target?.closest("#filelist li");
9182
+ if (!row)
9183
+ return null;
9184
+ const path = row.dataset.path || row.dataset.dirpath || "";
9185
+ if (!path)
9186
+ return null;
9187
+ return {
9188
+ path,
9189
+ children_omitted_reason: row.dataset.childrenOmittedReason
9190
+ };
9191
+ }
9192
+ function handleSidebarContextMenu(event) {
9193
+ const entry = sidebarTrashEntryFromEvent(event);
9194
+ if (!entry)
9195
+ return;
9196
+ if (showRepoContextMenu(event, entry, REPO_SIDEBAR_REF || "worktree", () => loadRepo()))
9197
+ markActive(entry.path);
9198
+ }
9199
+ function createMoveToTrashButton(path, onDeleted) {
9200
+ const button = document.createElement("button");
9201
+ button.type = "button";
9202
+ button.className = "gdp-btn gdp-btn-sm gdp-trash-path";
9203
+ button.textContent = "Move to Trash";
9204
+ button.addEventListener("click", async (event) => {
9205
+ event.stopPropagation();
9206
+ await requestMoveToTrash(path, onDeleted, { focusReturnTarget: button });
9207
+ });
9208
+ return button;
9209
+ }
8943
9210
  function createOpenPathButton(path, kind, title = "open folder in OS") {
8944
9211
  const button = document.createElement("button");
8945
9212
  button.type = "button";
@@ -9130,69 +9397,82 @@ ${frontmatter.yaml}
9130
9397
  if (meta.upload_enabled && (meta.ref === "worktree" || meta.ref === "")) {
9131
9398
  listWrapper.appendChild(createRepoUploadPanel(meta.path || ""));
9132
9399
  }
9400
+ const sortHost = document.createElement("div");
9401
+ sortHost.className = "gdp-repo-sort-host";
9133
9402
  const list2 = document.createElement("div");
9134
9403
  list2.className = "gdp-source-viewer gdp-repo-file-list";
9135
- if (meta.path) {
9136
- const parent = meta.path.split("/").slice(0, -1).join("/");
9137
- const row = document.createElement("button");
9138
- row.type = "button";
9139
- row.className = "gdp-repo-row parent";
9140
- const parentIcon = document.createElement("span");
9141
- parentIcon.className = "dir-icon";
9142
- setFolderIcon(parentIcon, false);
9143
- const parentName = document.createElement("span");
9144
- parentName.className = "name";
9145
- parentName.textContent = "..";
9146
- const parentKind = document.createElement("span");
9147
- parentKind.className = "kind";
9148
- parentKind.textContent = "parent";
9149
- row.append(parentIcon, parentName, parentKind);
9150
- row.addEventListener("click", () => {
9151
- setRoute(repoRoute(meta.ref, parent));
9152
- loadRepo();
9153
- });
9154
- list2.appendChild(row);
9155
- }
9156
- meta.entries.forEach((entry) => {
9157
- const row = document.createElement("button");
9158
- row.type = "button";
9159
- row.className = `gdp-repo-row ${entry.type}`;
9160
- const icon = document.createElement("span");
9161
- icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
9162
- if (entry.type === "tree")
9163
- setFolderIcon(icon, true);
9164
- else
9165
- icon.innerHTML = fileEntryIcon();
9166
- const name = document.createElement("span");
9167
- name.className = "name";
9168
- name.textContent = entry.name;
9169
- const kind = document.createElement("span");
9170
- kind.className = "kind";
9171
- kind.textContent = entry.type === "tree" ? "directory" : entry.type === "commit" ? "submodule" : "file";
9172
- row.append(icon, name, kind);
9173
- row.addEventListener("click", () => {
9174
- if (entry.type === "tree") {
9175
- setRoute(repoRoute(meta.ref, entry.path));
9404
+ const renderRepoRows = (focusSortKey) => {
9405
+ sortHost.replaceChildren(createRepoSortHeader(renderRepoRows));
9406
+ if (focusSortKey) {
9407
+ sortHost.querySelector(`[data-repo-sort="${focusSortKey}"]`)?.focus();
9408
+ }
9409
+ list2.replaceChildren();
9410
+ if (meta.path) {
9411
+ const parent = meta.path.split("/").slice(0, -1).join("/");
9412
+ const row = document.createElement("button");
9413
+ row.type = "button";
9414
+ row.className = "gdp-repo-row parent";
9415
+ const parentIcon = document.createElement("span");
9416
+ parentIcon.className = "dir-icon";
9417
+ setFolderIcon(parentIcon, false);
9418
+ const parentName = document.createElement("span");
9419
+ parentName.className = "name";
9420
+ parentName.textContent = "..";
9421
+ const parentKind = document.createElement("span");
9422
+ parentKind.className = "meta";
9423
+ parentKind.textContent = "";
9424
+ const parentSize = document.createElement("span");
9425
+ parentSize.className = "size";
9426
+ row.append(parentIcon, parentName, parentKind, parentSize);
9427
+ row.addEventListener("click", () => {
9428
+ setRoute(repoRoute(meta.ref, parent));
9176
9429
  loadRepo();
9177
- } else if (entry.type === "blob") {
9178
- setRoute({
9179
- screen: "file",
9180
- path: entry.path,
9181
- ref: meta.ref,
9182
- view: "blob",
9183
- range: currentRange()
9184
- });
9185
- renderStandaloneSource({ path: entry.path, ref: meta.ref });
9186
- }
9430
+ });
9431
+ list2.appendChild(row);
9432
+ }
9433
+ sortedRepoEntries(meta.entries).forEach((entry) => {
9434
+ const row = document.createElement("button");
9435
+ row.type = "button";
9436
+ row.className = `gdp-repo-row ${entry.type}`;
9437
+ const icon = document.createElement("span");
9438
+ icon.className = entry.type === "tree" ? "dir-icon" : "d2h-icon-wrapper";
9439
+ if (entry.type === "tree")
9440
+ setFolderIcon(icon, true);
9441
+ else
9442
+ icon.innerHTML = fileEntryIcon();
9443
+ const name = document.createElement("span");
9444
+ name.className = "name";
9445
+ name.textContent = entry.name;
9446
+ const metaBlock = createRepoEntryMeta(entry);
9447
+ const size = createRepoEntrySize(entry);
9448
+ row.append(icon, name, metaBlock, size);
9449
+ row.addEventListener("click", () => {
9450
+ if (entry.type === "tree") {
9451
+ setRoute(repoRoute(meta.ref, entry.path));
9452
+ loadRepo();
9453
+ } else if (entry.type === "blob") {
9454
+ setRoute({
9455
+ screen: "file",
9456
+ path: entry.path,
9457
+ ref: meta.ref,
9458
+ view: "blob",
9459
+ range: currentRange()
9460
+ });
9461
+ renderStandaloneSource({ path: entry.path, ref: meta.ref });
9462
+ }
9463
+ });
9464
+ row.addEventListener("contextmenu", (event) => showRepoContextMenu(event, entry, meta.ref, () => loadRepo()));
9465
+ list2.appendChild(row);
9187
9466
  });
9188
- list2.appendChild(row);
9189
- });
9190
- if (!meta.entries.length) {
9191
- const empty = document.createElement("div");
9192
- empty.className = "gdp-repo-empty";
9193
- empty.textContent = "No files in this directory.";
9194
- list2.appendChild(empty);
9195
- }
9467
+ if (!meta.entries.length) {
9468
+ const empty = document.createElement("div");
9469
+ empty.className = "gdp-repo-empty";
9470
+ empty.textContent = "No files in this directory.";
9471
+ list2.appendChild(empty);
9472
+ }
9473
+ };
9474
+ listWrapper.appendChild(sortHost);
9475
+ renderRepoRows();
9196
9476
  listWrapper.appendChild(list2);
9197
9477
  listCard.appendChild(listWrapper);
9198
9478
  shell.appendChild(listCard);
@@ -9255,7 +9535,7 @@ ${frontmatter.yaml}
9255
9535
  const params = new URLSearchParams;
9256
9536
  params.set("ref", normalizedRef);
9257
9537
  params.set("recursive", "1");
9258
- appendScopeOmitDirsParam(params);
9538
+ appendScopeParams(params);
9259
9539
  REPO_SIDEBAR_LOAD_REF = normalizedRef;
9260
9540
  const load2 = trackLoad(fetch(`/_tree?${params.toString()}`).then((r2) => {
9261
9541
  if (!r2.ok)
@@ -9273,6 +9553,7 @@ ${frontmatter.yaml}
9273
9553
  children_omitted: entry.children_omitted,
9274
9554
  children_omitted_reason: entry.children_omitted_reason
9275
9555
  }));
9556
+ REPO_SIDEBAR_REF = normalizedRef;
9276
9557
  renderSidebar(files, (file) => {
9277
9558
  if (file.type === "tree") {
9278
9559
  setRoute(repoRoute(normalizedRef, file.path));
@@ -9288,7 +9569,6 @@ ${frontmatter.yaml}
9288
9569
  });
9289
9570
  renderStandaloneSource({ path: file.path, ref: normalizedRef });
9290
9571
  });
9291
- REPO_SIDEBAR_REF = normalizedRef;
9292
9572
  activateRepoSidebarPath(currentPath);
9293
9573
  }).catch(() => {
9294
9574
  REPO_SIDEBAR_REF = null;
@@ -10335,6 +10615,117 @@ ${frontmatter.yaml}
10335
10615
  }
10336
10616
  return (unit === 0 ? String(value) : value.toFixed(value >= 10 ? 1 : 2).replace(/\.0+$/, "")) + " " + units[unit];
10337
10617
  }
10618
+ function formatFileDate(value) {
10619
+ if (!value)
10620
+ return "";
10621
+ const date = new Date(value);
10622
+ if (Number.isNaN(date.getTime()))
10623
+ return "";
10624
+ return date.toLocaleString(undefined, {
10625
+ year: "numeric",
10626
+ month: "short",
10627
+ day: "numeric",
10628
+ hour: "2-digit",
10629
+ minute: "2-digit"
10630
+ });
10631
+ }
10632
+ function createRepoEntryMeta(entry) {
10633
+ const meta = document.createElement("span");
10634
+ meta.className = "meta";
10635
+ const updated = formatFileDate(entry.updated_at || entry.commit_updated_at);
10636
+ const created = formatFileDate(entry.created_at);
10637
+ if (entry.type === "tree" && updated) {
10638
+ meta.textContent = updated;
10639
+ if (created)
10640
+ meta.title = `Created ${created}`;
10641
+ return meta;
10642
+ }
10643
+ if (entry.type !== "blob") {
10644
+ meta.textContent = "-";
10645
+ return meta;
10646
+ }
10647
+ meta.textContent = updated ? updated : created ? created : "-";
10648
+ if (created)
10649
+ meta.title = `Created ${created}`;
10650
+ return meta;
10651
+ }
10652
+ function createRepoEntrySize(entry) {
10653
+ const size = document.createElement("span");
10654
+ size.className = "size";
10655
+ size.textContent = entry.type === "blob" && entry.size != null ? formatBytes(entry.size) : "";
10656
+ return size;
10657
+ }
10658
+ function repoEntryUpdatedTime(entry) {
10659
+ const raw = entry.updated_at || entry.commit_updated_at || entry.created_at;
10660
+ if (!raw)
10661
+ return -1;
10662
+ const time = new Date(raw).getTime();
10663
+ return Number.isNaN(time) ? -1 : time;
10664
+ }
10665
+ function sortedRepoEntries(entries) {
10666
+ const direction = REPO_SORT.direction === "asc" ? 1 : -1;
10667
+ return [...entries].sort((a2, b2) => {
10668
+ if (REPO_SORT.key === "name" && a2.type !== b2.type) {
10669
+ if (a2.type === "tree")
10670
+ return -1;
10671
+ if (b2.type === "tree")
10672
+ return 1;
10673
+ }
10674
+ let result = 0;
10675
+ if (REPO_SORT.key === "updated") {
10676
+ const aTime = repoEntryUpdatedTime(a2);
10677
+ const bTime = repoEntryUpdatedTime(b2);
10678
+ if (aTime < 0 && bTime >= 0)
10679
+ return 1;
10680
+ if (bTime < 0 && aTime >= 0)
10681
+ return -1;
10682
+ result = aTime - bTime;
10683
+ } else if (REPO_SORT.key === "size") {
10684
+ if (a2.size == null && b2.size != null)
10685
+ return 1;
10686
+ if (b2.size == null && a2.size != null)
10687
+ return -1;
10688
+ result = (a2.size ?? 0) - (b2.size ?? 0);
10689
+ } else {
10690
+ result = a2.name.localeCompare(b2.name);
10691
+ }
10692
+ if (result === 0)
10693
+ result = a2.name.localeCompare(b2.name);
10694
+ return result * direction;
10695
+ });
10696
+ }
10697
+ function createRepoSortHeader(onSortChange) {
10698
+ const header = document.createElement("div");
10699
+ header.className = "gdp-repo-sort-header";
10700
+ const spacer = document.createElement("span");
10701
+ spacer.className = "gdp-repo-sort-spacer";
10702
+ header.appendChild(spacer);
10703
+ const columns = [
10704
+ { key: "name", label: "Name" },
10705
+ { key: "updated", label: "Updated" },
10706
+ { key: "size", label: "Size" }
10707
+ ];
10708
+ columns.forEach((column) => {
10709
+ const button = document.createElement("button");
10710
+ button.type = "button";
10711
+ button.dataset.repoSort = column.key;
10712
+ button.textContent = column.label + (REPO_SORT.key === column.key ? REPO_SORT.direction === "asc" ? " ↑" : " ↓" : "");
10713
+ button.className = REPO_SORT.key === column.key ? "active" : "";
10714
+ button.addEventListener("click", () => {
10715
+ if (REPO_SORT.key === column.key) {
10716
+ REPO_SORT.direction = REPO_SORT.direction === "asc" ? "desc" : "asc";
10717
+ } else {
10718
+ REPO_SORT = {
10719
+ key: column.key,
10720
+ direction: column.key === "name" ? "asc" : "desc"
10721
+ };
10722
+ }
10723
+ onSortChange(column.key);
10724
+ });
10725
+ header.appendChild(button);
10726
+ });
10727
+ return header;
10728
+ }
10338
10729
  function humanFileKind(path, mime, fallback) {
10339
10730
  const ext = (path.split(".").pop() || "").toLowerCase();
10340
10731
  if (ext === "png")
@@ -10392,12 +10783,41 @@ ${frontmatter.yaml}
10392
10783
  const size = rawSize == null ? NaN : Number(rawSize);
10393
10784
  return {
10394
10785
  size: rawSize != null && Number.isFinite(size) ? size : undefined,
10395
- type: res.headers.get("content-type") || undefined
10786
+ type: res.headers.get("content-type") || undefined,
10787
+ created_at: res.headers.get("x-code-viewer-created-at") || undefined,
10788
+ updated_at: res.headers.get("x-code-viewer-updated-at") || undefined,
10789
+ commit_updated_at: res.headers.get("x-code-viewer-commit-updated-at") || undefined
10396
10790
  };
10397
10791
  } catch {
10398
10792
  return {};
10399
10793
  }
10400
10794
  }
10795
+ function createFileDetailMeta(target, meta) {
10796
+ const wrap = document.createElement("div");
10797
+ wrap.className = "gdp-file-detail-meta";
10798
+ const addItem = (label, value) => {
10799
+ if (!value)
10800
+ return;
10801
+ const item = document.createElement("span");
10802
+ item.className = "gdp-file-detail-meta-item";
10803
+ const labelEl = document.createElement("span");
10804
+ labelEl.className = "label";
10805
+ labelEl.textContent = label;
10806
+ const valueEl = document.createElement("span");
10807
+ valueEl.className = "value";
10808
+ valueEl.textContent = value;
10809
+ item.append(labelEl, valueEl);
10810
+ wrap.appendChild(item);
10811
+ };
10812
+ addItem("Size", meta.size == null ? "" : formatBytes(meta.size));
10813
+ addItem("Updated", formatFileDate(meta.updated_at || meta.commit_updated_at));
10814
+ addItem("Created", formatFileDate(meta.created_at));
10815
+ if (!wrap.childElementCount) {
10816
+ wrap.hidden = true;
10817
+ wrap.dataset.path = target.path;
10818
+ }
10819
+ return wrap;
10820
+ }
10401
10821
  function createSourceFileInfo(target, kind) {
10402
10822
  const info = document.createElement("div");
10403
10823
  info.className = "gdp-source-file-info";
@@ -11423,6 +11843,18 @@ ${frontmatter.yaml}
11423
11843
  name.appendChild(copy);
11424
11844
  name.appendChild(createOpenPathButton(target.path, "file-parent", "open parent folder in OS"));
11425
11845
  header.appendChild(name);
11846
+ if (repoTarget && canTrashWorktreeRef(repoTarget)) {
11847
+ header.appendChild(createMoveToTrashButton(target.path, () => {
11848
+ const parent = target.path.split("/").slice(0, -1).join("/");
11849
+ setRoute(repoRoute(repoTarget, parent));
11850
+ loadRepo();
11851
+ }));
11852
+ }
11853
+ loadRawFileInfo(target).then((meta) => {
11854
+ if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
11855
+ return;
11856
+ header.appendChild(createFileDetailMeta(target, meta));
11857
+ });
11426
11858
  if (!repoTarget) {
11427
11859
  const back = document.createElement("button");
11428
11860
  back.type = "button";
@@ -12620,7 +13052,7 @@ ${frontmatter.yaml}
12620
13052
  return cached;
12621
13053
  const params = new URLSearchParams;
12622
13054
  params.set("ref", ref);
12623
- appendScopeOmitDirsParam(params);
13055
+ appendScopeParams(params);
12624
13056
  const res = await trackLoad(fetch(`/_files?${params.toString()}`).then((r2) => {
12625
13057
  if (!r2.ok)
12626
13058
  throw new Error("failed to load files");
@@ -12724,7 +13156,7 @@ ${frontmatter.yaml}
12724
13156
  params.set("max", "200");
12725
13157
  if (state.grepRegex)
12726
13158
  params.set("regex", "1");
12727
- appendScopeOmitDirsParam(params);
13159
+ appendScopeParams(params);
12728
13160
  if (source === "diff") {
12729
13161
  for (const file of state.diffSnapshot)
12730
13162
  params.append("path", file.path);
@@ -13015,10 +13447,19 @@ ${frontmatter.yaml}
13015
13447
  document.addEventListener("keydown", handleVirtualSourcePagingKeydown, {
13016
13448
  capture: true
13017
13449
  });
13018
- document.addEventListener("keydown", (e2) => {
13450
+ document.addEventListener("click", closeRepoContextMenu);
13451
+ $("#filelist").addEventListener("contextmenu", handleSidebarContextMenu);
13452
+ document.addEventListener("keydown", async (e2) => {
13453
+ if (e2.key === "Escape")
13454
+ closeRepoContextMenu();
13019
13455
  if (e2.__gdpVirtualSourcePagingHandled)
13020
13456
  return;
13021
13457
  const targetEl = e2.target;
13458
+ if ((e2.ctrlKey || e2.metaKey) && !e2.shiftKey && !e2.altKey && e2.key.toLowerCase() === "z" && !isEditableKeyTarget(targetEl)) {
13459
+ if (await undoLastAction())
13460
+ e2.preventDefault();
13461
+ return;
13462
+ }
13022
13463
  if ((e2.ctrlKey || e2.metaKey) && e2.key.toLowerCase() === "f" && !isEditableKeyTarget(targetEl)) {
13023
13464
  if (openVirtualSourceSearchFromKeyboard(targetEl)) {
13024
13465
  e2.preventDefault();
@@ -13053,7 +13494,7 @@ ${frontmatter.yaml}
13053
13494
  params.set("ref", STATE.route.ref || "worktree");
13054
13495
  if (STATE.route.path)
13055
13496
  params.set("path", STATE.route.path);
13056
- appendScopeOmitDirsParam(params);
13497
+ appendScopeParams(params);
13057
13498
  return trackLoad(fetch(`/_tree?${params.toString()}`).then((r2) => {
13058
13499
  if (!r2.ok)
13059
13500
  throw new Error("failed to load repository tree");