@youtyan/code-viewer 0.1.25 → 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/README.md +3 -0
- package/dist/code-viewer.js +344 -38
- package/package.json +1 -1
- package/web/app.js +295 -15
- package/web/index.html +3 -1
- package/web/style.css +83 -2
package/README.md
CHANGED
|
@@ -108,6 +108,9 @@ Repository scope settings control recursive repository browsing and search scope
|
|
|
108
108
|
for the left tree, Ctrl+K file palette, and Ctrl+G grep palette. The in-app Scope
|
|
109
109
|
Settings popover stores only a browser-local override in localStorage; edit
|
|
110
110
|
`.code-viewer.json` directly for project defaults shared with the repository.
|
|
111
|
+
Use `scope.omitDirs` for directories that should stay visible as skipped, and
|
|
112
|
+
`scope.excludeNames` for file or directory names that should be hidden entirely.
|
|
113
|
+
`.DS_Store` is hidden by default.
|
|
111
114
|
|
|
112
115
|
## Development
|
|
113
116
|
|
package/dist/code-viewer.js
CHANGED
|
@@ -6,14 +6,17 @@ import {
|
|
|
6
6
|
constants,
|
|
7
7
|
existsSync as existsSync3,
|
|
8
8
|
lstatSync as lstatSync3,
|
|
9
|
+
mkdirSync,
|
|
9
10
|
openSync,
|
|
10
11
|
readFileSync as readFileSync2,
|
|
11
12
|
realpathSync,
|
|
13
|
+
renameSync,
|
|
12
14
|
statSync,
|
|
13
15
|
unlinkSync,
|
|
14
16
|
watch,
|
|
15
17
|
writeFileSync
|
|
16
18
|
} from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
17
20
|
import { basename as basename2, dirname as dirname2, extname, join as join4, relative } from "node:path";
|
|
18
21
|
|
|
19
22
|
// web-src/routes.ts
|
|
@@ -568,7 +571,13 @@ function omittedWorktreeDirectoryReason(name, omitDirNames) {
|
|
|
568
571
|
return "internal";
|
|
569
572
|
return omitDirNames.has(name) ? "heavy" : undefined;
|
|
570
573
|
}
|
|
571
|
-
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
|
|
574
|
+
function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames, excludeNames) {
|
|
575
|
+
if (excludeNames.has(name.toLowerCase()))
|
|
576
|
+
return {
|
|
577
|
+
name,
|
|
578
|
+
path: "",
|
|
579
|
+
type: isDirectory ? "tree" : "blob"
|
|
580
|
+
};
|
|
572
581
|
const entryPath = base ? `${base}/${name}` : name;
|
|
573
582
|
const type = isDirectory ? hasDotGitEntry(join2(dir, name)) ? "commit" : "tree" : "blob";
|
|
574
583
|
const omittedReason = type === "tree" ? omittedWorktreeDirectoryReason(name, omitDirNames) : undefined;
|
|
@@ -580,14 +589,15 @@ function worktreeEntryFromDirent(base, dir, name, isDirectory, omitDirNames) {
|
|
|
580
589
|
children_omitted_reason: omittedReason
|
|
581
590
|
} : { name, path: entryPath, type };
|
|
582
591
|
}
|
|
583
|
-
function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES) {
|
|
592
|
+
function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES, excludeNames = []) {
|
|
584
593
|
const base = normalizeTreePath(path);
|
|
585
594
|
const root = join2(cwd, base);
|
|
586
595
|
const omitDirNameSet = new Set(omitDirNames);
|
|
596
|
+
const excludeNameSet = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
587
597
|
let directEntries;
|
|
588
598
|
try {
|
|
589
599
|
const dirents = readdirSync(root, { withFileTypes: true });
|
|
590
|
-
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet)));
|
|
600
|
+
directEntries = sortTreeEntries(dirents.map((entry) => worktreeEntryFromDirent(base, root, entry.name, entry.isDirectory(), omitDirNameSet, excludeNameSet)).filter((entry) => entry.path));
|
|
591
601
|
} catch {
|
|
592
602
|
return [];
|
|
593
603
|
}
|
|
@@ -624,6 +634,8 @@ function worktreeFilesystemEntries(cwd, path, recursive, omitDirNames = DEFAULT_
|
|
|
624
634
|
return;
|
|
625
635
|
}
|
|
626
636
|
for (const entry of entries) {
|
|
637
|
+
if (excludeNameSet.has(entry.name.toLowerCase()))
|
|
638
|
+
continue;
|
|
627
639
|
const entryPath = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
628
640
|
const full = join2(dir, entry.name);
|
|
629
641
|
if (entry.isDirectory()) {
|
|
@@ -699,15 +711,12 @@ function combineDirectAndRecursiveFiles(directEntries, fileEntries) {
|
|
|
699
711
|
...fileEntries.filter((entry) => !seen.has(entry.path))
|
|
700
712
|
];
|
|
701
713
|
}
|
|
702
|
-
function worktreeFiles(cwd) {
|
|
703
|
-
return listTree("worktree", "", cwd, { recursive: true }).entries;
|
|
704
|
-
}
|
|
705
714
|
function listTree(ref, path, cwd, options = {}) {
|
|
706
715
|
const base = normalizeTreePath(path);
|
|
707
716
|
if (ref === "worktree") {
|
|
708
717
|
return {
|
|
709
718
|
code: 0,
|
|
710
|
-
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames),
|
|
719
|
+
entries: worktreeFilesystemEntries(cwd, base, !!options.recursive, options.omitDirNames, options.excludeNames),
|
|
711
720
|
stderr: ""
|
|
712
721
|
};
|
|
713
722
|
}
|
|
@@ -1099,17 +1108,19 @@ var GREP_DEFAULT_MAX = 200;
|
|
|
1099
1108
|
var GREP_ABSOLUTE_MAX = 500;
|
|
1100
1109
|
var GREP_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
1101
1110
|
var FILE_SEARCH_ABSOLUTE_MAX = 50000;
|
|
1111
|
+
var DEFAULT_EXCLUDE_NAMES = [".DS_Store"];
|
|
1102
1112
|
function normalizeGrepMax(value) {
|
|
1103
1113
|
const parsed = Number(value || "");
|
|
1104
1114
|
if (!Number.isInteger(parsed) || parsed <= 0)
|
|
1105
1115
|
return GREP_DEFAULT_MAX;
|
|
1106
1116
|
return Math.min(parsed, GREP_ABSOLUTE_MAX);
|
|
1107
1117
|
}
|
|
1108
|
-
function isSkippableSearchPath(path, omitDirNames = []) {
|
|
1118
|
+
function isSkippableSearchPath(path, omitDirNames = [], excludeNames = []) {
|
|
1109
1119
|
const omitDirs = new Set(omitDirNames.map((name) => name.toLowerCase()));
|
|
1120
|
+
const excluded = new Set(excludeNames.map((name) => name.toLowerCase()));
|
|
1110
1121
|
return path.split(/[\\/]+/).some((part) => {
|
|
1111
1122
|
const lower = part.toLowerCase();
|
|
1112
|
-
return lower === ".git" || omitDirs.has(lower);
|
|
1123
|
+
return lower === ".git" || omitDirs.has(lower) || excluded.has(lower);
|
|
1113
1124
|
});
|
|
1114
1125
|
}
|
|
1115
1126
|
function fixedStringLineMatches(path, text, query, max) {
|
|
@@ -1142,7 +1153,7 @@ function buildFileSearchList(ref, generation, entries) {
|
|
|
1142
1153
|
truncated: entries.length > FILE_SEARCH_ABSOLUTE_MAX
|
|
1143
1154
|
};
|
|
1144
1155
|
}
|
|
1145
|
-
function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
1156
|
+
function buildRgArgs(query, max, paths, regex = false, omitDirNames = [], excludeNames = []) {
|
|
1146
1157
|
const safePaths = paths.length ? paths : ["."];
|
|
1147
1158
|
const omitGlobs = omitDirNames.flatMap((name) => [
|
|
1148
1159
|
"--glob",
|
|
@@ -1150,6 +1161,12 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1150
1161
|
"--glob",
|
|
1151
1162
|
`!**/${name}/**`
|
|
1152
1163
|
]);
|
|
1164
|
+
const excludeGlobs = excludeNames.flatMap((name) => [
|
|
1165
|
+
"--glob",
|
|
1166
|
+
`!${name}`,
|
|
1167
|
+
"--glob",
|
|
1168
|
+
`!**/${name}`
|
|
1169
|
+
]);
|
|
1153
1170
|
const args = [
|
|
1154
1171
|
"rg",
|
|
1155
1172
|
"--no-config",
|
|
@@ -1164,6 +1181,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1164
1181
|
"--max-filesize",
|
|
1165
1182
|
"2M",
|
|
1166
1183
|
...omitGlobs,
|
|
1184
|
+
...excludeGlobs,
|
|
1167
1185
|
"-e",
|
|
1168
1186
|
query,
|
|
1169
1187
|
"--",
|
|
@@ -1173,7 +1191,7 @@ function buildRgArgs(query, max, paths, regex = false, omitDirNames = []) {
|
|
|
1173
1191
|
args.splice(8, 0, "--fixed-strings");
|
|
1174
1192
|
return args;
|
|
1175
1193
|
}
|
|
1176
|
-
function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
1194
|
+
function parseRgOutput(stdout, max, omitDirNames = [], excludeNames = []) {
|
|
1177
1195
|
const matches = [];
|
|
1178
1196
|
for (const line of stdout.split(`
|
|
1179
1197
|
`)) {
|
|
@@ -1186,7 +1204,7 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1186
1204
|
const lineNo = Number(parsed[2]);
|
|
1187
1205
|
const column = Number(parsed[3]);
|
|
1188
1206
|
const preview = parsed[4];
|
|
1189
|
-
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames))
|
|
1207
|
+
if (!path || !lineNo || !column || isSkippableSearchPath(path, omitDirNames, excludeNames))
|
|
1190
1208
|
continue;
|
|
1191
1209
|
matches.push({
|
|
1192
1210
|
path,
|
|
@@ -1197,12 +1215,12 @@ function parseRgOutput(stdout, max, omitDirNames = []) {
|
|
|
1197
1215
|
}
|
|
1198
1216
|
return matches;
|
|
1199
1217
|
}
|
|
1200
|
-
function parseGitGrepOutput(stdout, ref, max, omitDirNames = []) {
|
|
1218
|
+
function parseGitGrepOutput(stdout, ref, max, omitDirNames = [], excludeNames = []) {
|
|
1201
1219
|
const prefix = `${ref}:`;
|
|
1202
1220
|
const normalized = stdout.split(`
|
|
1203
1221
|
`).map((line) => line.startsWith(prefix) ? line.slice(prefix.length) : line).join(`
|
|
1204
1222
|
`);
|
|
1205
|
-
return parseRgOutput(normalized, max, omitDirNames);
|
|
1223
|
+
return parseRgOutput(normalized, max, omitDirNames, excludeNames);
|
|
1206
1224
|
}
|
|
1207
1225
|
|
|
1208
1226
|
// web-src/server/preview.ts
|
|
@@ -1255,6 +1273,7 @@ var uploadAllowedByCli = false;
|
|
|
1255
1273
|
var openAfterStart = false;
|
|
1256
1274
|
var scopeOmitDirNames = DEFAULT_WORKTREE_OMIT_DIR_NAMES;
|
|
1257
1275
|
var scopeOmitDirCliOverride = null;
|
|
1276
|
+
var scopeExcludeNames = DEFAULT_EXCLUDE_NAMES;
|
|
1258
1277
|
var rgAvailableCache = null;
|
|
1259
1278
|
var enc = new TextEncoder;
|
|
1260
1279
|
var sseClients = new Set;
|
|
@@ -1329,11 +1348,14 @@ Examples:
|
|
|
1329
1348
|
if (!uploadAllowedByCli)
|
|
1330
1349
|
allowUpload = loadProjectConfigUploadEnabled();
|
|
1331
1350
|
const configScopeOmitDirs = loadProjectConfigScopeOmitDirs();
|
|
1351
|
+
const configScopeExcludeNames = loadProjectConfigScopeExcludeNames();
|
|
1332
1352
|
if (scopeOmitDirCliOverride) {
|
|
1333
1353
|
scopeOmitDirNames = scopeOmitDirCliOverride;
|
|
1334
1354
|
} else if (configScopeOmitDirs) {
|
|
1335
1355
|
scopeOmitDirNames = configScopeOmitDirs;
|
|
1336
1356
|
}
|
|
1357
|
+
if (configScopeExcludeNames)
|
|
1358
|
+
scopeExcludeNames = configScopeExcludeNames;
|
|
1337
1359
|
}
|
|
1338
1360
|
function json(data, init = {}) {
|
|
1339
1361
|
return new Response(JSON.stringify(data), {
|
|
@@ -1606,6 +1628,13 @@ function normalizeScopeOmitDirNames(names) {
|
|
|
1606
1628
|
...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 64 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
|
|
1607
1629
|
].sort((a, b) => a.localeCompare(b));
|
|
1608
1630
|
}
|
|
1631
|
+
function normalizeScopeExcludeNames(names) {
|
|
1632
|
+
if (!Array.isArray(names))
|
|
1633
|
+
return [];
|
|
1634
|
+
return [
|
|
1635
|
+
...new Set(names.filter((name) => typeof name === "string").map((name) => name.trim()).filter((name) => name && name.length <= 128 && !name.includes("/") && !name.includes("\\") && !name.includes("\x00") && name !== "." && name !== ".." && name !== ".git"))
|
|
1636
|
+
].sort((a, b) => a.localeCompare(b));
|
|
1637
|
+
}
|
|
1609
1638
|
function parseScopeOmitDirNamesQuery(value) {
|
|
1610
1639
|
const names = value ? value.split(",") : [];
|
|
1611
1640
|
if (names.length > 100)
|
|
@@ -1617,6 +1646,17 @@ function parseScopeOmitDirNamesQuery(value) {
|
|
|
1617
1646
|
}
|
|
1618
1647
|
return normalizeScopeOmitDirNames(names);
|
|
1619
1648
|
}
|
|
1649
|
+
function parseScopeExcludeNamesQuery(value) {
|
|
1650
|
+
const names = value ? value.split(",") : [];
|
|
1651
|
+
if (names.length > 200)
|
|
1652
|
+
return null;
|
|
1653
|
+
for (const raw of names) {
|
|
1654
|
+
const name = raw.trim();
|
|
1655
|
+
if (!name || name.length > 128 || name.includes("/") || name.includes("\\") || name.includes("\x00") || name === "." || name === ".." || name === ".git")
|
|
1656
|
+
return null;
|
|
1657
|
+
}
|
|
1658
|
+
return normalizeScopeExcludeNames(names);
|
|
1659
|
+
}
|
|
1620
1660
|
function loadProjectConfig() {
|
|
1621
1661
|
const full = join4(cwd, ".code-viewer.json");
|
|
1622
1662
|
if (!existsSync3(full))
|
|
@@ -1650,14 +1690,31 @@ function loadProjectConfigScopeOmitDirs() {
|
|
|
1650
1690
|
return null;
|
|
1651
1691
|
return normalizeScopeOmitDirNames(config.scope.omitDirs);
|
|
1652
1692
|
}
|
|
1693
|
+
function loadProjectConfigScopeExcludeNames() {
|
|
1694
|
+
const config = loadProjectConfig();
|
|
1695
|
+
if (!config?.scope || !Array.isArray(config.scope.excludeNames))
|
|
1696
|
+
return null;
|
|
1697
|
+
return normalizeScopeExcludeNames(config.scope.excludeNames);
|
|
1698
|
+
}
|
|
1653
1699
|
function scopeOmitDirNamesFromQuery(url) {
|
|
1654
1700
|
if (!url.searchParams.has("omit_dirs"))
|
|
1655
1701
|
return scopeOmitDirNames;
|
|
1656
1702
|
return parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "") || scopeOmitDirNames;
|
|
1657
1703
|
}
|
|
1704
|
+
function scopeExcludeNamesFromQuery(url) {
|
|
1705
|
+
if (!url.searchParams.has("exclude_names"))
|
|
1706
|
+
return scopeExcludeNames;
|
|
1707
|
+
return parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "") || scopeExcludeNames;
|
|
1708
|
+
}
|
|
1658
1709
|
function invalidScopeOmitDirNamesQuery(url) {
|
|
1659
1710
|
return url.searchParams.has("omit_dirs") && !parseScopeOmitDirNamesQuery(url.searchParams.get("omit_dirs") || "");
|
|
1660
1711
|
}
|
|
1712
|
+
function invalidScopeExcludeNamesQuery(url) {
|
|
1713
|
+
return url.searchParams.has("exclude_names") && !parseScopeExcludeNamesQuery(url.searchParams.get("exclude_names") || "");
|
|
1714
|
+
}
|
|
1715
|
+
function isExcludedScopePath(path, excludeNames) {
|
|
1716
|
+
return path.split(/[\\/]+/).some((part) => excludeNames.some((name) => part.toLowerCase() === name.toLowerCase()));
|
|
1717
|
+
}
|
|
1661
1718
|
function isGitInternalPath(path) {
|
|
1662
1719
|
return path.split(/[\\/]+/).some((part) => part.toLowerCase() === ".git");
|
|
1663
1720
|
}
|
|
@@ -1684,6 +1741,9 @@ function safeWorktreePath(path) {
|
|
|
1684
1741
|
return null;
|
|
1685
1742
|
return realFull;
|
|
1686
1743
|
}
|
|
1744
|
+
function worktreePath(path) {
|
|
1745
|
+
return join4(cwd, path);
|
|
1746
|
+
}
|
|
1687
1747
|
function safeOpenWorktreePath(path) {
|
|
1688
1748
|
if (path === "") {
|
|
1689
1749
|
try {
|
|
@@ -1798,10 +1858,14 @@ function handleTree(url) {
|
|
|
1798
1858
|
const recursive = url.searchParams.get("recursive") === "1";
|
|
1799
1859
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1800
1860
|
return text("invalid omit dirs", 400);
|
|
1861
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
1862
|
+
return text("invalid exclude names", 400);
|
|
1863
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
1801
1864
|
const entries = listTree(target, path, cwd, {
|
|
1802
1865
|
recursive,
|
|
1803
|
-
omitDirNames: scopeOmitDirNamesFromQuery(url)
|
|
1804
|
-
|
|
1866
|
+
omitDirNames: scopeOmitDirNamesFromQuery(url),
|
|
1867
|
+
excludeNames
|
|
1868
|
+
}).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
|
|
1805
1869
|
return json({
|
|
1806
1870
|
ref: target,
|
|
1807
1871
|
path,
|
|
@@ -1818,6 +1882,8 @@ function handleSettings() {
|
|
|
1818
1882
|
scope: {
|
|
1819
1883
|
omit_dirs_effective: scopeOmitDirNames,
|
|
1820
1884
|
omit_dirs_built_in: DEFAULT_WORKTREE_OMIT_DIR_NAMES,
|
|
1885
|
+
exclude_names_effective: scopeExcludeNames,
|
|
1886
|
+
exclude_names_built_in: DEFAULT_EXCLUDE_NAMES,
|
|
1821
1887
|
max_entries: WORKTREE_RECURSIVE_ENTRY_LIMIT
|
|
1822
1888
|
}
|
|
1823
1889
|
});
|
|
@@ -1828,22 +1894,26 @@ function handleFiles(url) {
|
|
|
1828
1894
|
return text("invalid target", 400);
|
|
1829
1895
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1830
1896
|
return text("invalid omit dirs", 400);
|
|
1897
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
1898
|
+
return text("invalid exclude names", 400);
|
|
1831
1899
|
const omitDirNames = scopeOmitDirNamesFromQuery(url);
|
|
1832
|
-
const
|
|
1900
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
1901
|
+
const key = `${target || "worktree"}\x00${omitDirNames.join("\x00")}\x00${excludeNames.join("\x00")}`;
|
|
1833
1902
|
const cached = fileListCache.get(key);
|
|
1834
1903
|
if (cached && cached.generation === generation)
|
|
1835
1904
|
return json(cached.body);
|
|
1836
1905
|
const ref = target || "worktree";
|
|
1837
1906
|
const entries = listTree(ref, "", cwd, {
|
|
1838
1907
|
recursive: true,
|
|
1839
|
-
omitDirNames
|
|
1840
|
-
|
|
1908
|
+
omitDirNames,
|
|
1909
|
+
excludeNames
|
|
1910
|
+
}).entries.filter((entry) => !isExcludedScopePath(entry.path, excludeNames));
|
|
1841
1911
|
const body = buildFileSearchList(ref, generation, entries);
|
|
1842
1912
|
fileListCache.set(key, { generation, body });
|
|
1843
1913
|
return json(body);
|
|
1844
1914
|
}
|
|
1845
|
-
function parseGrepPaths(url, omitDirNames) {
|
|
1846
|
-
return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
|
|
1915
|
+
function parseGrepPaths(url, omitDirNames, excludeNames) {
|
|
1916
|
+
return url.searchParams.getAll("path").filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
|
|
1847
1917
|
}
|
|
1848
1918
|
function rgAvailable() {
|
|
1849
1919
|
if (rgAvailableCache !== null)
|
|
@@ -1852,13 +1922,17 @@ function rgAvailable() {
|
|
|
1852
1922
|
rgAvailableCache = proc.code === 0;
|
|
1853
1923
|
return rgAvailableCache;
|
|
1854
1924
|
}
|
|
1855
|
-
function grepWorktreeFallback(query, max, paths, omitDirNames) {
|
|
1856
|
-
const candidates = paths.length ? paths :
|
|
1925
|
+
function grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames) {
|
|
1926
|
+
const candidates = paths.length ? paths : listTree("worktree", "", cwd, {
|
|
1927
|
+
recursive: true,
|
|
1928
|
+
omitDirNames,
|
|
1929
|
+
excludeNames
|
|
1930
|
+
}).entries.map((entry) => entry.path);
|
|
1857
1931
|
const matches = [];
|
|
1858
1932
|
for (const path of candidates) {
|
|
1859
1933
|
if (matches.length >= max)
|
|
1860
1934
|
break;
|
|
1861
|
-
if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames))
|
|
1935
|
+
if (!safePath(path) || isGitInternalPath(path) || isSkippableSearchPath(path, omitDirNames, excludeNames))
|
|
1862
1936
|
continue;
|
|
1863
1937
|
const full = safeWorktreePath(path);
|
|
1864
1938
|
if (!full)
|
|
@@ -1883,13 +1957,13 @@ function grepWorktreeFallback(query, max, paths, omitDirNames) {
|
|
|
1883
1957
|
}
|
|
1884
1958
|
return matches;
|
|
1885
1959
|
}
|
|
1886
|
-
function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
1960
|
+
function grepWorktree(query, max, paths, regex, omitDirNames, excludeNames) {
|
|
1887
1961
|
if (rgAvailable()) {
|
|
1888
|
-
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames) && safeWorktreePath(path));
|
|
1889
|
-
const args = buildRgArgs(query, max, safePaths, regex, omitDirNames);
|
|
1962
|
+
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames) && safeWorktreePath(path));
|
|
1963
|
+
const args = buildRgArgs(query, max, safePaths, regex, omitDirNames, excludeNames);
|
|
1890
1964
|
const proc = runSync(args, cwd, { timeout: 5000 });
|
|
1891
1965
|
const stdout = proc.stdout;
|
|
1892
|
-
const matches2 = parseRgOutput(stdout, max, omitDirNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames) && !!safeWorktreePath(match.path));
|
|
1966
|
+
const matches2 = parseRgOutput(stdout, max, omitDirNames, excludeNames).filter((match) => safePath(match.path) && !isGitInternalPath(match.path) && !isSkippableSearchPath(match.path, omitDirNames, excludeNames) && !!safeWorktreePath(match.path));
|
|
1893
1967
|
return {
|
|
1894
1968
|
ref: "worktree",
|
|
1895
1969
|
engine: "rg",
|
|
@@ -1904,7 +1978,7 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
|
1904
1978
|
truncated: false,
|
|
1905
1979
|
matches: []
|
|
1906
1980
|
};
|
|
1907
|
-
const matches = grepWorktreeFallback(query, max, paths, omitDirNames);
|
|
1981
|
+
const matches = grepWorktreeFallback(query, max, paths, omitDirNames, excludeNames);
|
|
1908
1982
|
return {
|
|
1909
1983
|
ref: "worktree",
|
|
1910
1984
|
engine: "fallback",
|
|
@@ -1912,8 +1986,8 @@ function grepWorktree(query, max, paths, regex, omitDirNames) {
|
|
|
1912
1986
|
matches
|
|
1913
1987
|
};
|
|
1914
1988
|
}
|
|
1915
|
-
function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
|
|
1916
|
-
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames));
|
|
1989
|
+
function grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames) {
|
|
1990
|
+
const safePaths = paths.filter((path) => safePath(path) && !isGitInternalPath(path) && !isSkippableSearchPath(path, omitDirNames, excludeNames));
|
|
1917
1991
|
const args = [
|
|
1918
1992
|
"git",
|
|
1919
1993
|
"-c",
|
|
@@ -1932,7 +2006,7 @@ function grepTreeRef(ref, query, max, paths, regex, omitDirNames) {
|
|
|
1932
2006
|
];
|
|
1933
2007
|
const proc = runSync(args, cwd, { timeout: 5000 });
|
|
1934
2008
|
const stdout = proc.stdout;
|
|
1935
|
-
const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames).slice(0, max);
|
|
2009
|
+
const matches = parseGitGrepOutput(stdout, ref, max, omitDirNames, excludeNames).slice(0, max);
|
|
1936
2010
|
return { ref, engine: "git", truncated: matches.length >= max, matches };
|
|
1937
2011
|
}
|
|
1938
2012
|
function handleGrep(url) {
|
|
@@ -1941,8 +2015,11 @@ function handleGrep(url) {
|
|
|
1941
2015
|
const max = normalizeGrepMax(url.searchParams.get("max"));
|
|
1942
2016
|
if (invalidScopeOmitDirNamesQuery(url))
|
|
1943
2017
|
return text("invalid omit dirs", 400);
|
|
2018
|
+
if (invalidScopeExcludeNamesQuery(url))
|
|
2019
|
+
return text("invalid exclude names", 400);
|
|
1944
2020
|
const omitDirNames = scopeOmitDirNamesFromQuery(url);
|
|
1945
|
-
const
|
|
2021
|
+
const excludeNames = scopeExcludeNamesFromQuery(url);
|
|
2022
|
+
const paths = parseGrepPaths(url, omitDirNames, excludeNames);
|
|
1946
2023
|
const regex = url.searchParams.get("regex") === "1";
|
|
1947
2024
|
if (!query.trim())
|
|
1948
2025
|
return json({
|
|
@@ -1952,10 +2029,10 @@ function handleGrep(url) {
|
|
|
1952
2029
|
matches: []
|
|
1953
2030
|
});
|
|
1954
2031
|
if (ref === "worktree" || ref === "")
|
|
1955
|
-
return json(grepWorktree(query, max, paths, regex, omitDirNames));
|
|
2032
|
+
return json(grepWorktree(query, max, paths, regex, omitDirNames, excludeNames));
|
|
1956
2033
|
if (!verifyTreeRef(ref, cwd))
|
|
1957
2034
|
return text("invalid target", 400);
|
|
1958
|
-
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames));
|
|
2035
|
+
return json(grepTreeRef(ref, query, max, paths, regex, omitDirNames, excludeNames));
|
|
1959
2036
|
}
|
|
1960
2037
|
function handleRefCommits(url) {
|
|
1961
2038
|
const query = url.searchParams.get("q") || "";
|
|
@@ -2468,6 +2545,152 @@ function openOsPath(path) {
|
|
|
2468
2545
|
const cmd = process.platform === "darwin" ? ["open", "--", path] : process.platform === "win32" ? ["explorer.exe", path] : ["xdg-open", path];
|
|
2469
2546
|
spawnDetached(cmd);
|
|
2470
2547
|
}
|
|
2548
|
+
function windowsTrashScript(path) {
|
|
2549
|
+
const quotedPath = path.replace(/'/g, "''");
|
|
2550
|
+
return [
|
|
2551
|
+
"$ErrorActionPreference = 'Stop';",
|
|
2552
|
+
`$path = '${quotedPath}';`,
|
|
2553
|
+
"Add-Type -TypeDefinition @'",
|
|
2554
|
+
"using System;",
|
|
2555
|
+
"using System.Runtime.InteropServices;",
|
|
2556
|
+
"public static class CodeViewerRecycleBin {",
|
|
2557
|
+
" [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]",
|
|
2558
|
+
" public struct SHFILEOPSTRUCT {",
|
|
2559
|
+
" public IntPtr hwnd;",
|
|
2560
|
+
" public uint wFunc;",
|
|
2561
|
+
" public string pFrom;",
|
|
2562
|
+
" public string pTo;",
|
|
2563
|
+
" public ushort fFlags;",
|
|
2564
|
+
" [MarshalAs(UnmanagedType.Bool)] public bool fAnyOperationsAborted;",
|
|
2565
|
+
" public IntPtr hNameMappings;",
|
|
2566
|
+
" public string lpszProgressTitle;",
|
|
2567
|
+
" }",
|
|
2568
|
+
' [DllImport("shell32.dll", CharSet = CharSet.Unicode)]',
|
|
2569
|
+
" private static extern int SHFileOperationW(ref SHFILEOPSTRUCT lpFileOp);",
|
|
2570
|
+
" public static void MoveToRecycleBin(string path) {",
|
|
2571
|
+
" const uint FO_DELETE = 0x0003;",
|
|
2572
|
+
" const ushort FOF_SILENT = 0x0004;",
|
|
2573
|
+
" const ushort FOF_NOCONFIRMATION = 0x0010;",
|
|
2574
|
+
" const ushort FOF_ALLOWUNDO = 0x0040;",
|
|
2575
|
+
" const ushort FOF_NOERRORUI = 0x0400;",
|
|
2576
|
+
" var op = new SHFILEOPSTRUCT {",
|
|
2577
|
+
" hwnd = IntPtr.Zero,",
|
|
2578
|
+
" wFunc = FO_DELETE,",
|
|
2579
|
+
' pFrom = path + "\\0\\0",',
|
|
2580
|
+
" pTo = null,",
|
|
2581
|
+
" fFlags = (ushort)(FOF_ALLOWUNDO | FOF_NOCONFIRMATION | FOF_NOERRORUI | FOF_SILENT),",
|
|
2582
|
+
" fAnyOperationsAborted = false,",
|
|
2583
|
+
" hNameMappings = IntPtr.Zero,",
|
|
2584
|
+
" lpszProgressTitle = null",
|
|
2585
|
+
" };",
|
|
2586
|
+
" int result = SHFileOperationW(ref op);",
|
|
2587
|
+
' if (result != 0) throw new InvalidOperationException("SHFileOperationW failed: " + result);',
|
|
2588
|
+
' if (op.fAnyOperationsAborted) throw new OperationCanceledException("SHFileOperationW aborted");',
|
|
2589
|
+
" }",
|
|
2590
|
+
"}",
|
|
2591
|
+
"'@;",
|
|
2592
|
+
"[CodeViewerRecycleBin]::MoveToRecycleBin($path);"
|
|
2593
|
+
].join(" ");
|
|
2594
|
+
}
|
|
2595
|
+
function windowsRestoreTrashScript(originalPath) {
|
|
2596
|
+
const quotedPath = originalPath.replace(/'/g, "''");
|
|
2597
|
+
return [
|
|
2598
|
+
"$ErrorActionPreference = 'Stop';",
|
|
2599
|
+
`$original = '${quotedPath}';`,
|
|
2600
|
+
"$parent = [System.IO.Path]::GetDirectoryName($original);",
|
|
2601
|
+
"$name = [System.IO.Path]::GetFileName($original);",
|
|
2602
|
+
"$shell = New-Object -ComObject Shell.Application;",
|
|
2603
|
+
"$bin = $shell.Namespace(10);",
|
|
2604
|
+
"$restored = $false;",
|
|
2605
|
+
"foreach ($item in $bin.Items()) {",
|
|
2606
|
+
" $deletedFrom = $item.ExtendedProperty('System.Recycle.DeletedFrom');",
|
|
2607
|
+
" if ($item.Name -eq $name -and $deletedFrom -eq $parent) {",
|
|
2608
|
+
" $item.InvokeVerb('ESTORE');",
|
|
2609
|
+
" $restored = $true;",
|
|
2610
|
+
" break;",
|
|
2611
|
+
" }",
|
|
2612
|
+
"}",
|
|
2613
|
+
"if (-not $restored) { throw 'recycle bin item not found'; }"
|
|
2614
|
+
].join(" ");
|
|
2615
|
+
}
|
|
2616
|
+
function makeUndoId() {
|
|
2617
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 10)}`;
|
|
2618
|
+
}
|
|
2619
|
+
function clearMutableCaches() {
|
|
2620
|
+
fileCache.clear();
|
|
2621
|
+
metaCache.clear();
|
|
2622
|
+
fileListCache.clear();
|
|
2623
|
+
}
|
|
2624
|
+
function moveMacPathIntoTrash(path) {
|
|
2625
|
+
const trashDir = join4(homedir(), ".Trash");
|
|
2626
|
+
const base = basename2(path) || "code-viewer-trash-item";
|
|
2627
|
+
const target = join4(trashDir, `${base}-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2, 8)}`);
|
|
2628
|
+
try {
|
|
2629
|
+
mkdirSync(trashDir, { recursive: true });
|
|
2630
|
+
renameSync(path, target);
|
|
2631
|
+
return { ok: true, trashPath: target };
|
|
2632
|
+
} catch (error) {
|
|
2633
|
+
return { ok: false, error: String(error) };
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
function movePathToTrash(path) {
|
|
2637
|
+
lstatSync3(path);
|
|
2638
|
+
if (process.platform === "darwin") {
|
|
2639
|
+
return moveMacPathIntoTrash(path);
|
|
2640
|
+
}
|
|
2641
|
+
if (process.platform === "win32") {
|
|
2642
|
+
const res = runSync([
|
|
2643
|
+
"powershell.exe",
|
|
2644
|
+
"-NoProfile",
|
|
2645
|
+
"-NonInteractive",
|
|
2646
|
+
"-ExecutionPolicy",
|
|
2647
|
+
"Bypass",
|
|
2648
|
+
"-Command",
|
|
2649
|
+
windowsTrashScript(path)
|
|
2650
|
+
], cwd, { timeout: 60000 });
|
|
2651
|
+
return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
|
|
2652
|
+
}
|
|
2653
|
+
return { ok: false, error: "trash unsupported" };
|
|
2654
|
+
}
|
|
2655
|
+
function restoreTrashPath(originalPath, trashPath) {
|
|
2656
|
+
const parent = parentRepoPath(originalPath);
|
|
2657
|
+
const parentFullPath = safeOpenWorktreePath(parent);
|
|
2658
|
+
if (!parentFullPath)
|
|
2659
|
+
return { ok: false, error: "invalid restore target" };
|
|
2660
|
+
const original = worktreePath(originalPath);
|
|
2661
|
+
if (existsSync3(original))
|
|
2662
|
+
return { ok: false, error: "restore target exists" };
|
|
2663
|
+
if (trashPath) {
|
|
2664
|
+
if (process.platform !== "darwin")
|
|
2665
|
+
return { ok: false, error: "invalid trash handle" };
|
|
2666
|
+
if (!existsSync3(trashPath))
|
|
2667
|
+
return { ok: false, error: "trash item not found" };
|
|
2668
|
+
try {
|
|
2669
|
+
const trashRoot = join4(homedir(), ".Trash");
|
|
2670
|
+
const trashRelative = relative(trashRoot, trashPath);
|
|
2671
|
+
if (trashRelative === "" || trashRelative.startsWith("..") || trashRelative.startsWith("/") || trashRelative.startsWith("\\"))
|
|
2672
|
+
return { ok: false, error: "invalid trash handle" };
|
|
2673
|
+
mkdirSync(dirname2(original), { recursive: true });
|
|
2674
|
+
renameSync(trashPath, original);
|
|
2675
|
+
return { ok: true };
|
|
2676
|
+
} catch (error) {
|
|
2677
|
+
return { ok: false, error: String(error) };
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
if (process.platform === "win32") {
|
|
2681
|
+
const res = runSync([
|
|
2682
|
+
"powershell.exe",
|
|
2683
|
+
"-NoProfile",
|
|
2684
|
+
"-NonInteractive",
|
|
2685
|
+
"-ExecutionPolicy",
|
|
2686
|
+
"Bypass",
|
|
2687
|
+
"-Command",
|
|
2688
|
+
windowsRestoreTrashScript(original)
|
|
2689
|
+
], cwd, { timeout: 60000 });
|
|
2690
|
+
return res.code === 0 ? { ok: true } : { ok: false, error: res.stderr || res.stdout };
|
|
2691
|
+
}
|
|
2692
|
+
return { ok: false, error: "undo unavailable for this trash operation" };
|
|
2693
|
+
}
|
|
2471
2694
|
async function handleOpenPath(req) {
|
|
2472
2695
|
if (req.method !== "POST")
|
|
2473
2696
|
return text("method not allowed", 405);
|
|
@@ -2508,6 +2731,87 @@ async function handleOpenPath(req) {
|
|
|
2508
2731
|
openOsPath(target);
|
|
2509
2732
|
return json({ ok: true });
|
|
2510
2733
|
}
|
|
2734
|
+
async function handleTrashPath(req) {
|
|
2735
|
+
if (req.method !== "POST")
|
|
2736
|
+
return text("method not allowed", 405);
|
|
2737
|
+
if (!sideEffectRequestAllowed(req))
|
|
2738
|
+
return text("forbidden", 403);
|
|
2739
|
+
const contentType = req.headers.get("content-type") || "";
|
|
2740
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
2741
|
+
return text("unsupported media type", 415);
|
|
2742
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
2743
|
+
if (length > 1024)
|
|
2744
|
+
return text("payload too large", 413);
|
|
2745
|
+
let body = {};
|
|
2746
|
+
try {
|
|
2747
|
+
const raw = await req.text();
|
|
2748
|
+
if (raw.length > 1024)
|
|
2749
|
+
return text("payload too large", 413);
|
|
2750
|
+
body = JSON.parse(raw);
|
|
2751
|
+
} catch {
|
|
2752
|
+
return text("invalid json", 400);
|
|
2753
|
+
}
|
|
2754
|
+
const path = typeof body.path === "string" ? body.path.replace(/^\/+|\/+$/g, "") : "";
|
|
2755
|
+
if (!path)
|
|
2756
|
+
return text("invalid path", 400);
|
|
2757
|
+
if (!safeRepoPath(path))
|
|
2758
|
+
return text("invalid path", 400);
|
|
2759
|
+
if (isGitInternalPath(path))
|
|
2760
|
+
return text("forbidden", 403);
|
|
2761
|
+
const originalFullPath = safeWorktreePath(path);
|
|
2762
|
+
if (!originalFullPath)
|
|
2763
|
+
return text("not found", 404);
|
|
2764
|
+
const moved = movePathToTrash(worktreePath(path));
|
|
2765
|
+
if (!moved.ok)
|
|
2766
|
+
return text(moved.error || "trash failed", 500);
|
|
2767
|
+
const undo = {
|
|
2768
|
+
id: makeUndoId(),
|
|
2769
|
+
type: "trash",
|
|
2770
|
+
label: `Restore ${path}`,
|
|
2771
|
+
payload: {
|
|
2772
|
+
original_path: path,
|
|
2773
|
+
trashPath: moved.trashPath
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
generation++;
|
|
2777
|
+
clearMutableCaches();
|
|
2778
|
+
sendSse("update");
|
|
2779
|
+
return json({ ok: true, generation, undo });
|
|
2780
|
+
}
|
|
2781
|
+
async function handleRestoreTrash(req) {
|
|
2782
|
+
if (req.method !== "POST")
|
|
2783
|
+
return text("method not allowed", 405);
|
|
2784
|
+
if (!sideEffectRequestAllowed(req))
|
|
2785
|
+
return text("forbidden", 403);
|
|
2786
|
+
const contentType = req.headers.get("content-type") || "";
|
|
2787
|
+
if (!/^application\/json(?:;|$)/i.test(contentType))
|
|
2788
|
+
return text("unsupported media type", 415);
|
|
2789
|
+
const length = Number(req.headers.get("content-length") || "0");
|
|
2790
|
+
if (length > 1024)
|
|
2791
|
+
return text("payload too large", 413);
|
|
2792
|
+
let body = {};
|
|
2793
|
+
try {
|
|
2794
|
+
const raw = await req.text();
|
|
2795
|
+
if (raw.length > 1024)
|
|
2796
|
+
return text("payload too large", 413);
|
|
2797
|
+
body = JSON.parse(raw);
|
|
2798
|
+
} catch {
|
|
2799
|
+
return text("invalid json", 400);
|
|
2800
|
+
}
|
|
2801
|
+
const originalPath = typeof body.original_path === "string" ? body.original_path.replace(/^\/+|\/+$/g, "") : "";
|
|
2802
|
+
const trashPath = typeof body.trashPath === "string" ? body.trashPath : "";
|
|
2803
|
+
if (!originalPath || !safeRepoPath(originalPath))
|
|
2804
|
+
return text("invalid restore target", 400);
|
|
2805
|
+
if (isGitInternalPath(originalPath))
|
|
2806
|
+
return text("forbidden", 403);
|
|
2807
|
+
const restored = restoreTrashPath(originalPath, trashPath || undefined);
|
|
2808
|
+
if (!restored.ok)
|
|
2809
|
+
return text(restored.error || "undo failed", 409);
|
|
2810
|
+
generation++;
|
|
2811
|
+
clearMutableCaches();
|
|
2812
|
+
sendSse("update");
|
|
2813
|
+
return json({ ok: true, generation });
|
|
2814
|
+
}
|
|
2511
2815
|
function sendSse(event, data = "tick") {
|
|
2512
2816
|
const payload = enc.encode(`event: ${event}
|
|
2513
2817
|
data: ${data}
|
|
@@ -2556,6 +2860,10 @@ var server = await startServer({
|
|
|
2556
2860
|
return handleRawFile(req, url);
|
|
2557
2861
|
if (url.pathname === "/_open_path")
|
|
2558
2862
|
return handleOpenPath(req);
|
|
2863
|
+
if (url.pathname === "/_trash_path")
|
|
2864
|
+
return handleTrashPath(req);
|
|
2865
|
+
if (url.pathname === "/_restore_trash")
|
|
2866
|
+
return handleRestoreTrash(req);
|
|
2559
2867
|
if (url.pathname === "/_upload_files")
|
|
2560
2868
|
return handleUploadFiles(req);
|
|
2561
2869
|
if (url.pathname === "/_refs")
|
|
@@ -2564,9 +2872,7 @@ var server = await startServer({
|
|
|
2564
2872
|
if (!sideEffectRequestAllowed(req))
|
|
2565
2873
|
return text("forbidden", 403);
|
|
2566
2874
|
generation++;
|
|
2567
|
-
|
|
2568
|
-
metaCache.clear();
|
|
2569
|
-
fileListCache.clear();
|
|
2875
|
+
clearMutableCaches();
|
|
2570
2876
|
sendSse("update");
|
|
2571
2877
|
return json({ ok: true, generation });
|
|
2572
2878
|
}
|
package/package.json
CHANGED
package/web/app.js
CHANGED
|
@@ -6947,6 +6947,8 @@ ${frontmatter.yaml}
|
|
|
6947
6947
|
direction: "asc"
|
|
6948
6948
|
};
|
|
6949
6949
|
let SERVER_SCOPE_OMIT_DIRS_DEFAULT = [];
|
|
6950
|
+
let SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = [];
|
|
6951
|
+
const UNDO_STACK = [];
|
|
6950
6952
|
let PENDING_G_SCOPE = null;
|
|
6951
6953
|
let PENDING_G_UNTIL = 0;
|
|
6952
6954
|
let SOURCE_CURSOR = null;
|
|
@@ -6954,6 +6956,7 @@ ${frontmatter.yaml}
|
|
|
6954
6956
|
const HELP_LANGUAGES = ["en", "ja"];
|
|
6955
6957
|
const HELP_SECTIONS = ["keybindings"];
|
|
6956
6958
|
const SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX = "gdp:scope-omit-dirs:";
|
|
6959
|
+
const SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX = "gdp:scope-exclude-names:";
|
|
6957
6960
|
const SIDEBAR_FONT_SIZE_STORAGE_KEY = "gdp:sidebar-font-size";
|
|
6958
6961
|
const CODE_FONT_SIZE_STORAGE_KEY = "gdp:code-font-size";
|
|
6959
6962
|
const CLIENT_SCOPE_OMIT_DIRS_DEFAULT = [
|
|
@@ -6984,6 +6987,7 @@ ${frontmatter.yaml}
|
|
|
6984
6987
|
"bin",
|
|
6985
6988
|
"obj"
|
|
6986
6989
|
];
|
|
6990
|
+
const CLIENT_SCOPE_EXCLUDE_NAMES_DEFAULT = [".DS_Store"];
|
|
6987
6991
|
const HELP_CONTENT = {
|
|
6988
6992
|
en: {
|
|
6989
6993
|
languageLabel: "Language",
|
|
@@ -7255,9 +7259,18 @@ ${frontmatter.yaml}
|
|
|
7255
7259
|
...new Set(raw.map((item) => item.trim()).filter((item) => item && item.length <= 64 && !item.includes("/") && !item.includes("\\") && item !== "." && item !== ".." && item !== ".git"))
|
|
7256
7260
|
].slice(0, 100).sort((a2, b2) => a2.localeCompare(b2));
|
|
7257
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
|
+
}
|
|
7258
7268
|
function scopeOmitDirsStorageKey() {
|
|
7259
7269
|
return SCOPE_OMIT_DIRS_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
|
|
7260
7270
|
}
|
|
7271
|
+
function scopeExcludeNamesStorageKey() {
|
|
7272
|
+
return SCOPE_EXCLUDE_NAMES_STORAGE_KEY_PREFIX + (PROJECT_NAME || "default");
|
|
7273
|
+
}
|
|
7261
7274
|
function setProjectName(project) {
|
|
7262
7275
|
if (!project)
|
|
7263
7276
|
return;
|
|
@@ -7275,16 +7288,36 @@ ${frontmatter.yaml}
|
|
|
7275
7288
|
return normalizeScopeOmitDirs(raw);
|
|
7276
7289
|
}
|
|
7277
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
|
+
}
|
|
7278
7302
|
function serverScopeOmitDirsDefault() {
|
|
7279
7303
|
return SERVER_SCOPE_OMIT_DIRS_DEFAULT.length ? SERVER_SCOPE_OMIT_DIRS_DEFAULT : CLIENT_SCOPE_OMIT_DIRS_DEFAULT;
|
|
7280
7304
|
}
|
|
7305
|
+
function serverScopeExcludeNamesDefault() {
|
|
7306
|
+
return SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT.length ? SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT : CLIENT_SCOPE_EXCLUDE_NAMES_DEFAULT;
|
|
7307
|
+
}
|
|
7281
7308
|
function effectiveScopeOmitDirs() {
|
|
7282
7309
|
return savedScopeOmitDirs() ?? serverScopeOmitDirsDefault();
|
|
7283
7310
|
}
|
|
7284
|
-
function
|
|
7285
|
-
|
|
7286
|
-
|
|
7287
|
-
|
|
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(","));
|
|
7288
7321
|
}
|
|
7289
7322
|
function normalizeViewerFontSize(value) {
|
|
7290
7323
|
return value === "compact" || value === "large" || value === "xlarge" ? value : "regular";
|
|
@@ -7319,8 +7352,9 @@ ${frontmatter.yaml}
|
|
|
7319
7352
|
syncSidebarHeaderHeight();
|
|
7320
7353
|
}
|
|
7321
7354
|
function repoFileCacheKey(ref) {
|
|
7322
|
-
const
|
|
7323
|
-
|
|
7355
|
+
const omit = savedScopeOmitDirs();
|
|
7356
|
+
const exclude = savedScopeExcludeNames();
|
|
7357
|
+
return `${ref}\x00${omit ? omit.join("\x00") : "server"}\x00${exclude ? exclude.join("\x00") : "server"}`;
|
|
7324
7358
|
}
|
|
7325
7359
|
async function loadSettings() {
|
|
7326
7360
|
try {
|
|
@@ -7330,6 +7364,7 @@ ${frontmatter.yaml}
|
|
|
7330
7364
|
const settings = await res.json();
|
|
7331
7365
|
setProjectName(settings.project || "");
|
|
7332
7366
|
SERVER_SCOPE_OMIT_DIRS_DEFAULT = normalizeScopeOmitDirs(settings.scope.omit_dirs_effective);
|
|
7367
|
+
SERVER_SCOPE_EXCLUDE_NAMES_DEFAULT = normalizeScopeExcludeNames(settings.scope.exclude_names_effective);
|
|
7333
7368
|
return settings;
|
|
7334
7369
|
} catch {
|
|
7335
7370
|
return null;
|
|
@@ -7671,7 +7706,7 @@ ${frontmatter.yaml}
|
|
|
7671
7706
|
applySidebarHidden(!STATE.sidebarHidden);
|
|
7672
7707
|
}
|
|
7673
7708
|
function scopeOmitSourceLabel() {
|
|
7674
|
-
return savedScopeOmitDirs() != null ? "Browser override" : "Server default";
|
|
7709
|
+
return savedScopeOmitDirs() != null || savedScopeExcludeNames() != null ? "Browser override" : "Server default";
|
|
7675
7710
|
}
|
|
7676
7711
|
function refreshRepositoryTreeAfterSettings() {
|
|
7677
7712
|
REPO_FILE_CACHE.clear();
|
|
@@ -7687,15 +7722,18 @@ ${frontmatter.yaml}
|
|
|
7687
7722
|
async function openScopeSettings() {
|
|
7688
7723
|
const pop = document.querySelector("#scope-settings-popover");
|
|
7689
7724
|
const input = document.querySelector("#scope-omit-dirs");
|
|
7725
|
+
const excludeInput = document.querySelector("#scope-exclude-names");
|
|
7690
7726
|
const sidebarFontSize = document.querySelector("#sidebar-font-size");
|
|
7691
7727
|
const codeFontSize = document.querySelector("#code-font-size");
|
|
7692
7728
|
const source = document.querySelector("#scope-omit-source");
|
|
7693
|
-
if (!pop || !input || !sidebarFontSize || !codeFontSize || !source)
|
|
7729
|
+
if (!pop || !input || !excludeInput || !sidebarFontSize || !codeFontSize || !source)
|
|
7694
7730
|
return;
|
|
7695
7731
|
await loadSettings();
|
|
7696
7732
|
sidebarFontSize.value = savedSidebarFontSize();
|
|
7697
7733
|
codeFontSize.value = savedCodeFontSize();
|
|
7698
7734
|
input.value = effectiveScopeOmitDirs().join(`
|
|
7735
|
+
`);
|
|
7736
|
+
excludeInput.value = effectiveScopeExcludeNames().join(`
|
|
7699
7737
|
`);
|
|
7700
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.";
|
|
7701
7739
|
pop.hidden = false;
|
|
@@ -7708,15 +7746,17 @@ ${frontmatter.yaml}
|
|
|
7708
7746
|
}
|
|
7709
7747
|
function saveScopeSettings() {
|
|
7710
7748
|
const input = document.querySelector("#scope-omit-dirs");
|
|
7749
|
+
const excludeInput = document.querySelector("#scope-exclude-names");
|
|
7711
7750
|
const sidebarFontSize = document.querySelector("#sidebar-font-size");
|
|
7712
7751
|
const codeFontSize = document.querySelector("#code-font-size");
|
|
7713
|
-
if (!input || !sidebarFontSize || !codeFontSize)
|
|
7752
|
+
if (!input || !excludeInput || !sidebarFontSize || !codeFontSize)
|
|
7714
7753
|
return;
|
|
7715
7754
|
localStorage.setItem(SIDEBAR_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(sidebarFontSize.value));
|
|
7716
7755
|
localStorage.setItem(CODE_FONT_SIZE_STORAGE_KEY, normalizeViewerFontSize(codeFontSize.value));
|
|
7717
7756
|
applySidebarFontSize();
|
|
7718
7757
|
applyCodeFontSize();
|
|
7719
7758
|
localStorage.setItem(scopeOmitDirsStorageKey(), JSON.stringify(normalizeScopeOmitDirs(input.value)));
|
|
7759
|
+
localStorage.setItem(scopeExcludeNamesStorageKey(), JSON.stringify(normalizeScopeExcludeNames(excludeInput.value)));
|
|
7720
7760
|
closeScopeSettings();
|
|
7721
7761
|
refreshRepositoryTreeAfterSettings();
|
|
7722
7762
|
}
|
|
@@ -7726,6 +7766,7 @@ ${frontmatter.yaml}
|
|
|
7726
7766
|
applySidebarFontSize("regular");
|
|
7727
7767
|
applyCodeFontSize("regular");
|
|
7728
7768
|
localStorage.removeItem(scopeOmitDirsStorageKey());
|
|
7769
|
+
localStorage.removeItem(scopeExcludeNamesStorageKey());
|
|
7729
7770
|
closeScopeSettings();
|
|
7730
7771
|
refreshRepositoryTreeAfterSettings();
|
|
7731
7772
|
}
|
|
@@ -7809,6 +7850,8 @@ ${frontmatter.yaml}
|
|
|
7809
7850
|
li.className = "tree-dir";
|
|
7810
7851
|
li.tabIndex = -1;
|
|
7811
7852
|
li.dataset.dirpath = dir.path;
|
|
7853
|
+
if (dir.children_omitted_reason)
|
|
7854
|
+
li.dataset.childrenOmittedReason = dir.children_omitted_reason;
|
|
7812
7855
|
if (dir.explicit)
|
|
7813
7856
|
li.dataset.explicit = "true";
|
|
7814
7857
|
if (dir.children_omitted) {
|
|
@@ -7952,6 +7995,8 @@ ${frontmatter.yaml}
|
|
|
7952
7995
|
li.className = "tree-dir";
|
|
7953
7996
|
li.tabIndex = -1;
|
|
7954
7997
|
li.dataset.dirpath = dir.path;
|
|
7998
|
+
if (dir.children_omitted_reason)
|
|
7999
|
+
li.dataset.childrenOmittedReason = dir.children_omitted_reason;
|
|
7955
8000
|
if (dir.explicit)
|
|
7956
8001
|
li.dataset.explicit = "true";
|
|
7957
8002
|
if (dir.children_omitted) {
|
|
@@ -8944,6 +8989,224 @@ ${frontmatter.yaml}
|
|
|
8944
8989
|
button.disabled = false;
|
|
8945
8990
|
}
|
|
8946
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
|
+
}
|
|
8947
9210
|
function createOpenPathButton(path, kind, title = "open folder in OS") {
|
|
8948
9211
|
const button = document.createElement("button");
|
|
8949
9212
|
button.type = "button";
|
|
@@ -9198,6 +9461,7 @@ ${frontmatter.yaml}
|
|
|
9198
9461
|
renderStandaloneSource({ path: entry.path, ref: meta.ref });
|
|
9199
9462
|
}
|
|
9200
9463
|
});
|
|
9464
|
+
row.addEventListener("contextmenu", (event) => showRepoContextMenu(event, entry, meta.ref, () => loadRepo()));
|
|
9201
9465
|
list2.appendChild(row);
|
|
9202
9466
|
});
|
|
9203
9467
|
if (!meta.entries.length) {
|
|
@@ -9271,7 +9535,7 @@ ${frontmatter.yaml}
|
|
|
9271
9535
|
const params = new URLSearchParams;
|
|
9272
9536
|
params.set("ref", normalizedRef);
|
|
9273
9537
|
params.set("recursive", "1");
|
|
9274
|
-
|
|
9538
|
+
appendScopeParams(params);
|
|
9275
9539
|
REPO_SIDEBAR_LOAD_REF = normalizedRef;
|
|
9276
9540
|
const load2 = trackLoad(fetch(`/_tree?${params.toString()}`).then((r2) => {
|
|
9277
9541
|
if (!r2.ok)
|
|
@@ -9289,6 +9553,7 @@ ${frontmatter.yaml}
|
|
|
9289
9553
|
children_omitted: entry.children_omitted,
|
|
9290
9554
|
children_omitted_reason: entry.children_omitted_reason
|
|
9291
9555
|
}));
|
|
9556
|
+
REPO_SIDEBAR_REF = normalizedRef;
|
|
9292
9557
|
renderSidebar(files, (file) => {
|
|
9293
9558
|
if (file.type === "tree") {
|
|
9294
9559
|
setRoute(repoRoute(normalizedRef, file.path));
|
|
@@ -9304,7 +9569,6 @@ ${frontmatter.yaml}
|
|
|
9304
9569
|
});
|
|
9305
9570
|
renderStandaloneSource({ path: file.path, ref: normalizedRef });
|
|
9306
9571
|
});
|
|
9307
|
-
REPO_SIDEBAR_REF = normalizedRef;
|
|
9308
9572
|
activateRepoSidebarPath(currentPath);
|
|
9309
9573
|
}).catch(() => {
|
|
9310
9574
|
REPO_SIDEBAR_REF = null;
|
|
@@ -11579,6 +11843,13 @@ ${frontmatter.yaml}
|
|
|
11579
11843
|
name.appendChild(copy);
|
|
11580
11844
|
name.appendChild(createOpenPathButton(target.path, "file-parent", "open parent folder in OS"));
|
|
11581
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
|
+
}
|
|
11582
11853
|
loadRawFileInfo(target).then((meta) => {
|
|
11583
11854
|
if (req !== SOURCE_REQ_SEQ || !sourceTargetsEqual(sourceTargetFromRoute(), target))
|
|
11584
11855
|
return;
|
|
@@ -12781,7 +13052,7 @@ ${frontmatter.yaml}
|
|
|
12781
13052
|
return cached;
|
|
12782
13053
|
const params = new URLSearchParams;
|
|
12783
13054
|
params.set("ref", ref);
|
|
12784
|
-
|
|
13055
|
+
appendScopeParams(params);
|
|
12785
13056
|
const res = await trackLoad(fetch(`/_files?${params.toString()}`).then((r2) => {
|
|
12786
13057
|
if (!r2.ok)
|
|
12787
13058
|
throw new Error("failed to load files");
|
|
@@ -12885,7 +13156,7 @@ ${frontmatter.yaml}
|
|
|
12885
13156
|
params.set("max", "200");
|
|
12886
13157
|
if (state.grepRegex)
|
|
12887
13158
|
params.set("regex", "1");
|
|
12888
|
-
|
|
13159
|
+
appendScopeParams(params);
|
|
12889
13160
|
if (source === "diff") {
|
|
12890
13161
|
for (const file of state.diffSnapshot)
|
|
12891
13162
|
params.append("path", file.path);
|
|
@@ -13176,10 +13447,19 @@ ${frontmatter.yaml}
|
|
|
13176
13447
|
document.addEventListener("keydown", handleVirtualSourcePagingKeydown, {
|
|
13177
13448
|
capture: true
|
|
13178
13449
|
});
|
|
13179
|
-
document.addEventListener("
|
|
13450
|
+
document.addEventListener("click", closeRepoContextMenu);
|
|
13451
|
+
$("#filelist").addEventListener("contextmenu", handleSidebarContextMenu);
|
|
13452
|
+
document.addEventListener("keydown", async (e2) => {
|
|
13453
|
+
if (e2.key === "Escape")
|
|
13454
|
+
closeRepoContextMenu();
|
|
13180
13455
|
if (e2.__gdpVirtualSourcePagingHandled)
|
|
13181
13456
|
return;
|
|
13182
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
|
+
}
|
|
13183
13463
|
if ((e2.ctrlKey || e2.metaKey) && e2.key.toLowerCase() === "f" && !isEditableKeyTarget(targetEl)) {
|
|
13184
13464
|
if (openVirtualSourceSearchFromKeyboard(targetEl)) {
|
|
13185
13465
|
e2.preventDefault();
|
|
@@ -13214,7 +13494,7 @@ ${frontmatter.yaml}
|
|
|
13214
13494
|
params.set("ref", STATE.route.ref || "worktree");
|
|
13215
13495
|
if (STATE.route.path)
|
|
13216
13496
|
params.set("path", STATE.route.path);
|
|
13217
|
-
|
|
13497
|
+
appendScopeParams(params);
|
|
13218
13498
|
return trackLoad(fetch(`/_tree?${params.toString()}`).then((r2) => {
|
|
13219
13499
|
if (!r2.ok)
|
|
13220
13500
|
throw new Error("failed to load repository tree");
|
package/web/index.html
CHANGED
|
@@ -124,7 +124,9 @@
|
|
|
124
124
|
<div class="scope-settings-section">
|
|
125
125
|
<strong class="scope-settings-section-title">Excluded directories</strong>
|
|
126
126
|
<label for="scope-omit-dirs">Skip these directory names while browsing and searching</label>
|
|
127
|
-
<textarea id="scope-omit-dirs" rows="
|
|
127
|
+
<textarea id="scope-omit-dirs" rows="6" spellcheck="false"></textarea>
|
|
128
|
+
<label for="scope-exclude-names">Hide these file or directory names completely</label>
|
|
129
|
+
<textarea id="scope-exclude-names" rows="4" spellcheck="false"></textarea>
|
|
128
130
|
<p id="scope-omit-source"></p>
|
|
129
131
|
<div class="scope-settings-actions">
|
|
130
132
|
<button id="scope-omit-reset" type="button">Reset</button>
|
package/web/style.css
CHANGED
|
@@ -1171,10 +1171,11 @@ body.gdp-resizing * { user-select: none !important; }
|
|
|
1171
1171
|
font: inherit;
|
|
1172
1172
|
font-size: 12.5px;
|
|
1173
1173
|
}
|
|
1174
|
-
#scope-omit-dirs
|
|
1174
|
+
#scope-omit-dirs,
|
|
1175
|
+
#scope-exclude-names {
|
|
1175
1176
|
box-sizing: border-box;
|
|
1176
1177
|
width: 100%;
|
|
1177
|
-
min-height:
|
|
1178
|
+
min-height: 110px;
|
|
1178
1179
|
resize: vertical;
|
|
1179
1180
|
border: 1px solid var(--border-muted);
|
|
1180
1181
|
border-radius: 6px;
|
|
@@ -2590,6 +2591,10 @@ table.d2h-diff-table tr.gdp-diff-line-target > td:first-child {
|
|
|
2590
2591
|
flex-shrink: 0;
|
|
2591
2592
|
color: var(--fg-muted);
|
|
2592
2593
|
}
|
|
2594
|
+
.gdp-file-detail-header .gdp-trash-path {
|
|
2595
|
+
flex-shrink: 0;
|
|
2596
|
+
color: var(--danger);
|
|
2597
|
+
}
|
|
2593
2598
|
.gdp-file-detail-meta {
|
|
2594
2599
|
display: flex;
|
|
2595
2600
|
align-items: center;
|
|
@@ -3257,6 +3262,82 @@ body.gdp-file-detail-page #empty {
|
|
|
3257
3262
|
font-size: 12px;
|
|
3258
3263
|
white-space: nowrap;
|
|
3259
3264
|
}
|
|
3265
|
+
.gdp-context-menu {
|
|
3266
|
+
position: fixed;
|
|
3267
|
+
z-index: 1000;
|
|
3268
|
+
min-width: 180px;
|
|
3269
|
+
padding: 6px;
|
|
3270
|
+
border: 1px solid var(--border);
|
|
3271
|
+
border-radius: 6px;
|
|
3272
|
+
background: var(--bg);
|
|
3273
|
+
box-shadow: 0 8px 24px rgba(31, 35, 40, 0.18);
|
|
3274
|
+
}
|
|
3275
|
+
.gdp-context-menu button {
|
|
3276
|
+
width: 100%;
|
|
3277
|
+
min-height: 30px;
|
|
3278
|
+
padding: 5px 8px;
|
|
3279
|
+
border: 0;
|
|
3280
|
+
border-radius: 4px;
|
|
3281
|
+
background: transparent;
|
|
3282
|
+
color: var(--fg);
|
|
3283
|
+
font: inherit;
|
|
3284
|
+
font-size: 13px;
|
|
3285
|
+
text-align: left;
|
|
3286
|
+
cursor: pointer;
|
|
3287
|
+
}
|
|
3288
|
+
.gdp-context-menu button:hover {
|
|
3289
|
+
background: var(--bg-soft);
|
|
3290
|
+
}
|
|
3291
|
+
.gdp-context-menu .danger {
|
|
3292
|
+
color: var(--danger);
|
|
3293
|
+
}
|
|
3294
|
+
.gdp-trash-dialog-backdrop {
|
|
3295
|
+
position: fixed;
|
|
3296
|
+
inset: 0;
|
|
3297
|
+
z-index: 1100;
|
|
3298
|
+
display: flex;
|
|
3299
|
+
align-items: flex-start;
|
|
3300
|
+
justify-content: center;
|
|
3301
|
+
padding-top: min(14vh, 120px);
|
|
3302
|
+
background: rgba(31, 35, 40, 0.18);
|
|
3303
|
+
}
|
|
3304
|
+
[data-theme="dark"] .gdp-trash-dialog-backdrop {
|
|
3305
|
+
background: rgba(1, 4, 9, 0.45);
|
|
3306
|
+
}
|
|
3307
|
+
.gdp-trash-dialog {
|
|
3308
|
+
width: min(420px, calc(100vw - 32px));
|
|
3309
|
+
border: 1px solid var(--border);
|
|
3310
|
+
border-radius: 8px;
|
|
3311
|
+
background: var(--bg);
|
|
3312
|
+
box-shadow: 0 16px 48px rgba(31, 35, 40, 0.28);
|
|
3313
|
+
}
|
|
3314
|
+
.gdp-trash-dialog-title {
|
|
3315
|
+
padding: 14px 16px 0;
|
|
3316
|
+
color: var(--fg);
|
|
3317
|
+
font-size: 15px;
|
|
3318
|
+
font-weight: 600;
|
|
3319
|
+
}
|
|
3320
|
+
.gdp-trash-dialog-body {
|
|
3321
|
+
padding: 8px 16px 14px;
|
|
3322
|
+
color: var(--fg-muted);
|
|
3323
|
+
font-size: 13px;
|
|
3324
|
+
line-height: 1.5;
|
|
3325
|
+
overflow-wrap: anywhere;
|
|
3326
|
+
}
|
|
3327
|
+
.gdp-trash-dialog-actions {
|
|
3328
|
+
display: flex;
|
|
3329
|
+
justify-content: flex-end;
|
|
3330
|
+
gap: 8px;
|
|
3331
|
+
padding: 12px 16px;
|
|
3332
|
+
border-top: 1px solid var(--border-muted);
|
|
3333
|
+
}
|
|
3334
|
+
.gdp-trash-dialog-danger {
|
|
3335
|
+
border-color: var(--danger);
|
|
3336
|
+
color: var(--danger);
|
|
3337
|
+
}
|
|
3338
|
+
.gdp-trash-dialog-danger:hover {
|
|
3339
|
+
background: var(--diff-del-bg);
|
|
3340
|
+
}
|
|
3260
3341
|
.gdp-repo-empty {
|
|
3261
3342
|
padding: 32px;
|
|
3262
3343
|
color: var(--fg-muted);
|