dsh-vscode-mode 0.1.28 → 0.1.30
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/lib/client.js +859 -27
- package/lib/client.js.map +1 -1
- package/lib/index.js +513 -41
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/compat.ts +1 -1
- package/src/client/index.ts +15 -0
- package/src/client/keybindings.ts +169 -0
- package/src/client/monaco/diffRender.ts +7 -0
- package/src/client/rpc.ts +13 -2
- package/src/client/sidebar/panels/SearchPanel.ts +294 -0
- package/src/client/sidebar/panels/index.ts +18 -2
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +40 -0
- package/src/client/styles/mcp.css +18 -0
- package/src/client/ui/EditorView.ts +76 -15
- package/src/client/ui/KeybindingsPanel.ts +187 -0
- package/src/client/ui/McpSettings.ts +7 -4
- package/src/client/ui/QuickOpen.ts +6 -3
- package/src/fileOpenSettings.ts +20 -4
- package/src/index.ts +4 -1
- package/src/rpc.ts +106 -22
- package/src/search/content.ts +319 -0
- package/src/search/fallback.ts +1 -1
- package/src/search/orchestrator.ts +17 -10
- package/src/search/ripgrep.ts +17 -2
- package/src/search/types.ts +40 -0
- package/src/shared/keybindings.ts +39 -0
- package/src/shared/rpc.ts +11 -0
package/lib/index.js
CHANGED
|
@@ -106,11 +106,37 @@ async function refreshMcp(ctx, id) {
|
|
|
106
106
|
return serverOf(ctx, loader.resolve(entry.id));
|
|
107
107
|
}
|
|
108
108
|
//#endregion
|
|
109
|
+
//#region src/shared/keybindings.ts
|
|
110
|
+
/**
|
|
111
|
+
* dsh-vscode-mode shared — 快捷键命令目录与默认键位(双面契约)。
|
|
112
|
+
* 纯数据模块:host(settings schema 默认值)与 client(执行匹配/设置页)共用。
|
|
113
|
+
* 键位格式:修饰符 + 主键,`+` 连接,如 `Ctrl+Shift+F`;空串 = 未绑定。
|
|
114
|
+
* 作者 ddj 2026年08月26号
|
|
115
|
+
*/
|
|
116
|
+
/** 命令 id → 默认键位。命令目录以此为准,新增命令只需加一项。 */
|
|
117
|
+
const KEYBINDING_DEFAULTS = {
|
|
118
|
+
"edrv.save": "Ctrl+S",
|
|
119
|
+
"edrv.quickOpen": "Ctrl+P",
|
|
120
|
+
"edrv.toggleSidebar": "Ctrl+B",
|
|
121
|
+
"edrv.searchInFiles": "Ctrl+Shift+F"
|
|
122
|
+
};
|
|
123
|
+
//#endregion
|
|
109
124
|
//#region src/fileOpenSettings.ts
|
|
110
125
|
const FILE_OPEN_SETTINGS_NS = "dsh-vscode-mode";
|
|
111
126
|
const FILE_OPEN_DEFAULT = "auto";
|
|
112
127
|
let depsPromise;
|
|
113
128
|
/**
|
|
129
|
+
* 按命令目录构建 keybindings 显式键形状(每键独立默认值)。
|
|
130
|
+
* @author ddj 2026年08月26号
|
|
131
|
+
* @param z schemastery 命名空间
|
|
132
|
+
* @returns keybindings 字段形状
|
|
133
|
+
*/
|
|
134
|
+
function keybindingsShape(z) {
|
|
135
|
+
const shape = {};
|
|
136
|
+
for (const [id, chord] of Object.entries(KEYBINDING_DEFAULTS)) shape[id] = z.string().default(chord);
|
|
137
|
+
return shape;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
114
140
|
* 动态加载设置依赖(模块级缓存;任一缺失返回 null 而非抛错)。
|
|
115
141
|
* @author ddj 2026年08月24号
|
|
116
142
|
* @returns 设置依赖或 null
|
|
@@ -142,7 +168,10 @@ function configValue(config) {
|
|
|
142
168
|
async function installOpenSettingsSection(ctx, ns, entry, hooks, loader = loadSettingsDeps) {
|
|
143
169
|
const deps = await loader();
|
|
144
170
|
if (!deps) return false;
|
|
145
|
-
const schema = deps.z.object({
|
|
171
|
+
const schema = deps.z.object({
|
|
172
|
+
fileOpenTool: deps.z.string().default(FILE_OPEN_DEFAULT),
|
|
173
|
+
keybindings: deps.z.object(keybindingsShape(deps.z)).default({ ...KEYBINDING_DEFAULTS })
|
|
174
|
+
});
|
|
146
175
|
deps.installSettingsSection(ctx, ns, schema, entry, hooks);
|
|
147
176
|
return true;
|
|
148
177
|
}
|
|
@@ -1457,9 +1486,10 @@ function pathMatch(value, query) {
|
|
|
1457
1486
|
* 通过独立 argv 调用打包 rg,避免先构建巨型全量文件数组。
|
|
1458
1487
|
* 作者 ddj 2026年08月24号
|
|
1459
1488
|
*/
|
|
1460
|
-
const STDOUT_CAP = 4 << 20;
|
|
1461
|
-
const STDERR_CAP = 65536;
|
|
1462
|
-
const GRACE_MS = 2e4;
|
|
1489
|
+
const STDOUT_CAP$1 = 4 << 20;
|
|
1490
|
+
const STDERR_CAP$1 = 65536;
|
|
1491
|
+
const GRACE_MS$1 = 2e4;
|
|
1492
|
+
/** 默认排除目录(文件/内容搜索共用,导出供 provider 复用)。 */
|
|
1463
1493
|
const EXCLUDES = [
|
|
1464
1494
|
"node_modules",
|
|
1465
1495
|
".git",
|
|
@@ -1469,7 +1499,22 @@ const EXCLUDES = [
|
|
|
1469
1499
|
"build",
|
|
1470
1500
|
"vendor",
|
|
1471
1501
|
"coverage",
|
|
1472
|
-
"__pycache__"
|
|
1502
|
+
"__pycache__",
|
|
1503
|
+
".pnpm-store",
|
|
1504
|
+
".npm-cache",
|
|
1505
|
+
".codegraph",
|
|
1506
|
+
".workbuddy"
|
|
1507
|
+
];
|
|
1508
|
+
/**
|
|
1509
|
+
* 默认排除文件(glob 模式,文件/内容搜索共用)。
|
|
1510
|
+
* 插件自身 sidecar(内容搜索会命中其历史编辑文本,且单行巨型 JSON 会撑爆输出上限);
|
|
1511
|
+
* *.map 源映射(纯构建产物,内容搜索无意义且常为单行巨文件)。
|
|
1512
|
+
*/
|
|
1513
|
+
const FILE_EXCLUDES = [
|
|
1514
|
+
"**/.dsh-edit-review.json",
|
|
1515
|
+
"**/.dsh-edit-review-archive.json",
|
|
1516
|
+
"**/.dsh-edit-review-debug.log",
|
|
1517
|
+
"**/*.map"
|
|
1473
1518
|
];
|
|
1474
1519
|
/**
|
|
1475
1520
|
* 将 query 转成 rg 的字面 glob。
|
|
@@ -1556,16 +1601,17 @@ async function searchRipgrep(input) {
|
|
|
1556
1601
|
queryGlob(prepareQuery(input.query).text)
|
|
1557
1602
|
];
|
|
1558
1603
|
for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
|
|
1604
|
+
for (const excluded of FILE_EXCLUDES) argv.push("--glob", "!" + excluded);
|
|
1559
1605
|
argv.push("--", root);
|
|
1560
1606
|
const handle = sub.spawn({
|
|
1561
1607
|
argv,
|
|
1562
1608
|
cwd: root,
|
|
1563
1609
|
stdio: {
|
|
1564
1610
|
stdin: "ignore",
|
|
1565
|
-
stdout: { maxBytes: STDOUT_CAP },
|
|
1566
|
-
stderr: { maxBytes: STDERR_CAP }
|
|
1611
|
+
stdout: { maxBytes: STDOUT_CAP$1 },
|
|
1612
|
+
stderr: { maxBytes: STDERR_CAP$1 }
|
|
1567
1613
|
},
|
|
1568
|
-
graceMs: GRACE_MS,
|
|
1614
|
+
graceMs: GRACE_MS$1,
|
|
1569
1615
|
signal: input.signal
|
|
1570
1616
|
});
|
|
1571
1617
|
let outcome;
|
|
@@ -1603,7 +1649,14 @@ const EXCLUDED = /* @__PURE__ */ new Set([
|
|
|
1603
1649
|
"build",
|
|
1604
1650
|
"vendor",
|
|
1605
1651
|
"coverage",
|
|
1606
|
-
"__pycache__"
|
|
1652
|
+
"__pycache__",
|
|
1653
|
+
".pnpm-store",
|
|
1654
|
+
".npm-cache",
|
|
1655
|
+
".codegraph",
|
|
1656
|
+
".workbuddy",
|
|
1657
|
+
".dsh-edit-review.json",
|
|
1658
|
+
".dsh-edit-review-archive.json",
|
|
1659
|
+
".dsh-edit-review-debug.log"
|
|
1607
1660
|
]);
|
|
1608
1661
|
/**
|
|
1609
1662
|
* 在一个目录树中递归查找匹配文件。
|
|
@@ -1710,16 +1763,25 @@ function rankCandidates(candidates, query) {
|
|
|
1710
1763
|
const CACHE_TTL = 6e4;
|
|
1711
1764
|
const CACHE_LIMIT = 100;
|
|
1712
1765
|
const RESULT_LIMIT = 50;
|
|
1713
|
-
const PROVIDER_VERSION = "ripgrep-v1";
|
|
1766
|
+
const PROVIDER_VERSION$1 = "ripgrep-v1";
|
|
1714
1767
|
const POLICY_VERSION = "search-policy-v1";
|
|
1715
1768
|
/**
|
|
1716
|
-
*
|
|
1769
|
+
* 短期有界搜索缓存(泛型:文件/内容搜索共用;clone 由调用方保证写隔离)。
|
|
1717
1770
|
* @author ddj 2026年08月24号
|
|
1718
|
-
* @
|
|
1771
|
+
* @param clone 结果写隔离克隆(缺省恒等)
|
|
1719
1772
|
*/
|
|
1720
1773
|
var SearchCache = class {
|
|
1774
|
+
clone;
|
|
1721
1775
|
entries = /* @__PURE__ */ new Map();
|
|
1722
1776
|
/**
|
|
1777
|
+
* 创建缓存。
|
|
1778
|
+
* @author ddj 2026年08月24号
|
|
1779
|
+
* @param clone 结果克隆函数(缺省直接引用)
|
|
1780
|
+
*/
|
|
1781
|
+
constructor(clone = (value) => value) {
|
|
1782
|
+
this.clone = clone;
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1723
1785
|
* 读取未过期条目。
|
|
1724
1786
|
* @author ddj 2026年08月24号
|
|
1725
1787
|
* @param key 缓存 key
|
|
@@ -1735,10 +1797,7 @@ var SearchCache = class {
|
|
|
1735
1797
|
}
|
|
1736
1798
|
this.entries.delete(key);
|
|
1737
1799
|
this.entries.set(key, entry);
|
|
1738
|
-
return
|
|
1739
|
-
...entry.result,
|
|
1740
|
-
files: [...entry.result.files]
|
|
1741
|
-
};
|
|
1800
|
+
return this.clone(entry.result);
|
|
1742
1801
|
}
|
|
1743
1802
|
/**
|
|
1744
1803
|
* 写入成功 provider 结果。
|
|
@@ -1751,10 +1810,7 @@ var SearchCache = class {
|
|
|
1751
1810
|
this.entries.delete(key);
|
|
1752
1811
|
this.entries.set(key, {
|
|
1753
1812
|
at: now,
|
|
1754
|
-
result:
|
|
1755
|
-
...result,
|
|
1756
|
-
files: [...result.files]
|
|
1757
|
-
}
|
|
1813
|
+
result: this.clone(result)
|
|
1758
1814
|
});
|
|
1759
1815
|
while (this.entries.size > CACHE_LIMIT) this.entries.delete(this.entries.keys().next().value);
|
|
1760
1816
|
}
|
|
@@ -1841,7 +1897,10 @@ function mergeCandidates(result, activePaths, root, query) {
|
|
|
1841
1897
|
*/
|
|
1842
1898
|
var SearchOrchestrator = class {
|
|
1843
1899
|
ctx;
|
|
1844
|
-
cache = new SearchCache()
|
|
1900
|
+
cache = new SearchCache((r) => ({
|
|
1901
|
+
...r,
|
|
1902
|
+
files: [...r.files]
|
|
1903
|
+
}));
|
|
1845
1904
|
inflight = /* @__PURE__ */ new Map();
|
|
1846
1905
|
rootsByCwd = /* @__PURE__ */ new Map();
|
|
1847
1906
|
provider;
|
|
@@ -1887,7 +1946,7 @@ var SearchOrchestrator = class {
|
|
|
1887
1946
|
rootKey,
|
|
1888
1947
|
query.text,
|
|
1889
1948
|
policyVersion(this.ctx, request.session),
|
|
1890
|
-
PROVIDER_VERSION
|
|
1949
|
+
PROVIDER_VERSION$1
|
|
1891
1950
|
].join("|");
|
|
1892
1951
|
const cached = this.cache.get(key);
|
|
1893
1952
|
if (cached) return this.finish(cached, request.activePaths, root, query);
|
|
@@ -2004,6 +2063,331 @@ function newSearcher(ctx) {
|
|
|
2004
2063
|
return new SearchOrchestrator(ctx);
|
|
2005
2064
|
}
|
|
2006
2065
|
//#endregion
|
|
2066
|
+
//#region src/search/content.ts
|
|
2067
|
+
const STDOUT_CAP = 16 << 20;
|
|
2068
|
+
const STDERR_CAP = 65536;
|
|
2069
|
+
const GRACE_MS = 2e4;
|
|
2070
|
+
const DEFAULT_MAX_MATCHES = 500;
|
|
2071
|
+
const DEFAULT_MAX_FILES = 100;
|
|
2072
|
+
const PROVIDER_VERSION = "content-ripgrep-v1";
|
|
2073
|
+
/** 单行 JSON 记录上限:巨型单行文件(源映射/打包产物)的命中行对搜索 UI 无意义,直接跳过。 */
|
|
2074
|
+
const MAX_RECORD_LINE = 1 << 20;
|
|
2075
|
+
/**
|
|
2076
|
+
* 把行内字节偏移转成 UTF-16 列(1-based;越界钳到行尾)。
|
|
2077
|
+
* @author ddj 2026年08月26号
|
|
2078
|
+
* @param text 行文本
|
|
2079
|
+
* @param byteOffset 行内字节偏移(rg submatches.start/end,实测为行相对)
|
|
2080
|
+
* @returns 1-based UTF-16 列
|
|
2081
|
+
*/
|
|
2082
|
+
function byteToUtf16Col(text, byteOffset) {
|
|
2083
|
+
let bytes = 0;
|
|
2084
|
+
let col = 1;
|
|
2085
|
+
for (const char of text) {
|
|
2086
|
+
if (bytes >= byteOffset) break;
|
|
2087
|
+
bytes += Buffer.byteLength(char);
|
|
2088
|
+
col++;
|
|
2089
|
+
}
|
|
2090
|
+
return col;
|
|
2091
|
+
}
|
|
2092
|
+
/**
|
|
2093
|
+
* 根内绝对路径 → 工作区相对显示路径(供 edrv.read 与面板展示)。
|
|
2094
|
+
* @author ddj 2026年08月26号
|
|
2095
|
+
* @param value rg 输出路径
|
|
2096
|
+
* @param root 搜索根(subprocess 执行世界)
|
|
2097
|
+
* @returns 相对路径(根外路径原样返回)
|
|
2098
|
+
*/
|
|
2099
|
+
function displayPathOf(value, root) {
|
|
2100
|
+
const path = pathText(String(value ?? ""));
|
|
2101
|
+
if (!path) return path;
|
|
2102
|
+
const base = pathText(root).replace(/\/$/, "");
|
|
2103
|
+
const lowerPath = path.toLocaleLowerCase("en-US");
|
|
2104
|
+
const lowerBase = base.toLocaleLowerCase("en-US");
|
|
2105
|
+
if (lowerPath === lowerBase) return ".";
|
|
2106
|
+
if (lowerPath.startsWith(lowerBase + "/")) return path.slice(base.length + 1);
|
|
2107
|
+
return path;
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* 解析 rg --json 输出为扁平命中列表(多子匹配展开,列转 UTF-16)。
|
|
2111
|
+
* @author ddj 2026年08月26号
|
|
2112
|
+
* @param text stdout 文本
|
|
2113
|
+
* @param root 搜索根
|
|
2114
|
+
* @returns 命中列表
|
|
2115
|
+
*/
|
|
2116
|
+
function parseRgJsonLines(text, root) {
|
|
2117
|
+
const matches = [];
|
|
2118
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2119
|
+
const trimmed = line.trim();
|
|
2120
|
+
if (!trimmed) continue;
|
|
2121
|
+
let item;
|
|
2122
|
+
try {
|
|
2123
|
+
item = JSON.parse(trimmed);
|
|
2124
|
+
} catch {
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
if (item?.type !== "match" || !item.data) continue;
|
|
2128
|
+
const data = item.data;
|
|
2129
|
+
const path = displayPathOf(data.path?.text, root);
|
|
2130
|
+
const lineText = (data.lines?.text ?? "").replace(/\r?\n$/, "").replace(/\r$/, "");
|
|
2131
|
+
if (lineText.length > MAX_RECORD_LINE) continue;
|
|
2132
|
+
const lineNumber = data.line_number ?? 0;
|
|
2133
|
+
for (const sub of data.submatches ?? []) matches.push({
|
|
2134
|
+
path,
|
|
2135
|
+
line: lineNumber,
|
|
2136
|
+
startColumn: byteToUtf16Col(lineText, sub.start ?? 0),
|
|
2137
|
+
endColumn: byteToUtf16Col(lineText, sub.end ?? 0),
|
|
2138
|
+
text: lineText
|
|
2139
|
+
});
|
|
2140
|
+
}
|
|
2141
|
+
return matches;
|
|
2142
|
+
}
|
|
2143
|
+
/**
|
|
2144
|
+
* 命中截断:匹配数/文件数双上限,超限标记 truncated。
|
|
2145
|
+
* @author ddj 2026年08月26号
|
|
2146
|
+
* @param matches 全部命中
|
|
2147
|
+
* @param maxMatches 匹配数上限
|
|
2148
|
+
* @param maxFiles 文件数上限
|
|
2149
|
+
* @returns 截断后的命中与标志
|
|
2150
|
+
*/
|
|
2151
|
+
function applyCaps(matches, maxMatches, maxFiles) {
|
|
2152
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2153
|
+
const out = [];
|
|
2154
|
+
let truncated = false;
|
|
2155
|
+
for (const match of matches) {
|
|
2156
|
+
if (out.length >= maxMatches) {
|
|
2157
|
+
truncated = true;
|
|
2158
|
+
break;
|
|
2159
|
+
}
|
|
2160
|
+
if (!seen.has(match.path)) {
|
|
2161
|
+
if (seen.size >= maxFiles) {
|
|
2162
|
+
truncated = true;
|
|
2163
|
+
break;
|
|
2164
|
+
}
|
|
2165
|
+
seen.add(match.path);
|
|
2166
|
+
}
|
|
2167
|
+
out.push(match);
|
|
2168
|
+
}
|
|
2169
|
+
return {
|
|
2170
|
+
matches: out,
|
|
2171
|
+
truncated
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
/**
|
|
2175
|
+
* 构造 rg 内容搜索 argv(pattern 放 `--` 后首位置,支持前导 `-` 的查询)。
|
|
2176
|
+
* include/exclude 为用户 glob(gitignore 式,原样透传不转义),
|
|
2177
|
+
* 分别转正选 `--glob` 与排除 `--glob !`。
|
|
2178
|
+
* @author ddj 2026年08月26号
|
|
2179
|
+
* @param binary rg 路径
|
|
2180
|
+
* @param root 搜索根
|
|
2181
|
+
* @param input 搜索输入
|
|
2182
|
+
* @returns argv 数组
|
|
2183
|
+
*/
|
|
2184
|
+
function contentArgv(binary, root, input) {
|
|
2185
|
+
const argv = [
|
|
2186
|
+
binary,
|
|
2187
|
+
"--no-config",
|
|
2188
|
+
"--json",
|
|
2189
|
+
"--line-number",
|
|
2190
|
+
"--no-heading",
|
|
2191
|
+
"--color",
|
|
2192
|
+
"never",
|
|
2193
|
+
"--hidden",
|
|
2194
|
+
"--no-ignore"
|
|
2195
|
+
];
|
|
2196
|
+
if (input.matchCase === true) argv.push("--case-sensitive");
|
|
2197
|
+
if (input.wholeWord === true) argv.push("--word-regexp");
|
|
2198
|
+
if (input.regex !== true) argv.push("--fixed-strings");
|
|
2199
|
+
for (const excluded of EXCLUDES) argv.push("--glob", "!**/" + excluded + "/**");
|
|
2200
|
+
for (const excluded of FILE_EXCLUDES) argv.push("--glob", "!" + excluded);
|
|
2201
|
+
for (const pattern of input.include ?? []) {
|
|
2202
|
+
const glob = String(pattern).trim();
|
|
2203
|
+
if (glob) argv.push("--glob", glob);
|
|
2204
|
+
}
|
|
2205
|
+
for (const pattern of input.exclude ?? []) {
|
|
2206
|
+
const glob = String(pattern).trim();
|
|
2207
|
+
if (glob) argv.push("--glob", "!" + glob);
|
|
2208
|
+
}
|
|
2209
|
+
argv.push("--", input.query, root);
|
|
2210
|
+
return argv;
|
|
2211
|
+
}
|
|
2212
|
+
/**
|
|
2213
|
+
* 使用打包 ripgrep 搜索文件内容。
|
|
2214
|
+
* @author ddj 2026年08月26号
|
|
2215
|
+
* @param input provider 输入
|
|
2216
|
+
* @returns 有界内容搜索结果
|
|
2217
|
+
*/
|
|
2218
|
+
async function searchRipgrepContent(input) {
|
|
2219
|
+
const sub = input.ctx.get("subprocess");
|
|
2220
|
+
if (!sub) throw new Error("缺少 subprocess");
|
|
2221
|
+
const binary = ripgrepPath();
|
|
2222
|
+
if (!binary) throw new Error("ripgrep 不可用");
|
|
2223
|
+
const root = input.root ?? await searchRoot(input.ctx, input.session);
|
|
2224
|
+
const handle = sub.spawn({
|
|
2225
|
+
argv: contentArgv(binary, root, input),
|
|
2226
|
+
cwd: root,
|
|
2227
|
+
stdio: {
|
|
2228
|
+
stdin: "ignore",
|
|
2229
|
+
stdout: { maxBytes: STDOUT_CAP },
|
|
2230
|
+
stderr: { maxBytes: STDERR_CAP }
|
|
2231
|
+
},
|
|
2232
|
+
graceMs: GRACE_MS,
|
|
2233
|
+
signal: input.signal
|
|
2234
|
+
});
|
|
2235
|
+
let outcome;
|
|
2236
|
+
try {
|
|
2237
|
+
outcome = await handle.done;
|
|
2238
|
+
} catch (error) {
|
|
2239
|
+
throw new Error("ripgrep 启动失败:" + String(error));
|
|
2240
|
+
}
|
|
2241
|
+
const code = outcome.exitCode ?? outcome.code;
|
|
2242
|
+
if (code !== 0 && code !== 1) throw new Error("ripgrep 退出码:" + String(code));
|
|
2243
|
+
const reader = handle.collected?.stdout;
|
|
2244
|
+
if (!reader) throw new Error("ripgrep stdout 不可用");
|
|
2245
|
+
const output = reader.readFrom(0);
|
|
2246
|
+
const capped = applyCaps(parseRgJsonLines(output.text, root), Math.max(1, Math.min(input.maxResults ?? DEFAULT_MAX_MATCHES, DEFAULT_MAX_MATCHES)), DEFAULT_MAX_FILES);
|
|
2247
|
+
const truncated = Boolean(output.lossy) || capped.truncated;
|
|
2248
|
+
return {
|
|
2249
|
+
...capped,
|
|
2250
|
+
complete: !truncated,
|
|
2251
|
+
source: "ripgrep"
|
|
2252
|
+
};
|
|
2253
|
+
}
|
|
2254
|
+
/**
|
|
2255
|
+
* 内容搜索编排器:短期缓存 + 同根在途取消 + 会话清理。
|
|
2256
|
+
* @author ddj 2026年08月26号
|
|
2257
|
+
*/
|
|
2258
|
+
var ContentSearcher = class {
|
|
2259
|
+
ctx;
|
|
2260
|
+
provider;
|
|
2261
|
+
cache = new SearchCache((r) => ({
|
|
2262
|
+
...r,
|
|
2263
|
+
matches: [...r.matches]
|
|
2264
|
+
}));
|
|
2265
|
+
inflight = /* @__PURE__ */ new Map();
|
|
2266
|
+
rootsByCwd = /* @__PURE__ */ new Map();
|
|
2267
|
+
/**
|
|
2268
|
+
* 创建内容搜索编排器。
|
|
2269
|
+
* @author ddj 2026年08月26号
|
|
2270
|
+
* @param ctx DSH 上下文
|
|
2271
|
+
* @param provider 内容搜索 provider(测试可替换)
|
|
2272
|
+
*/
|
|
2273
|
+
constructor(ctx, provider = { search: searchRipgrepContent }) {
|
|
2274
|
+
this.ctx = ctx;
|
|
2275
|
+
this.provider = provider;
|
|
2276
|
+
}
|
|
2277
|
+
/**
|
|
2278
|
+
* 执行一次工作区内容搜索。
|
|
2279
|
+
* @author ddj 2026年08月26号
|
|
2280
|
+
* @param request 搜索请求
|
|
2281
|
+
* @returns RPC 响应字段(provider 失败抛错,abort 返回空)
|
|
2282
|
+
*/
|
|
2283
|
+
async search(request) {
|
|
2284
|
+
const query = String(request.query ?? "").trim();
|
|
2285
|
+
if (query.length < 2) return {
|
|
2286
|
+
matches: [],
|
|
2287
|
+
truncated: false
|
|
2288
|
+
};
|
|
2289
|
+
let root;
|
|
2290
|
+
try {
|
|
2291
|
+
root = await searchRoot(this.ctx, request.session);
|
|
2292
|
+
} catch {
|
|
2293
|
+
return {
|
|
2294
|
+
matches: [],
|
|
2295
|
+
truncated: false
|
|
2296
|
+
};
|
|
2297
|
+
}
|
|
2298
|
+
const rootKey = pathText(root);
|
|
2299
|
+
const roots = this.rootsByCwd.get(request.cwd) ?? /* @__PURE__ */ new Set();
|
|
2300
|
+
roots.add(rootKey);
|
|
2301
|
+
this.rootsByCwd.set(request.cwd, roots);
|
|
2302
|
+
const includeKey = (request.include ?? []).join(",");
|
|
2303
|
+
const excludeKey = (request.exclude ?? []).join(",");
|
|
2304
|
+
const key = [
|
|
2305
|
+
rootKey,
|
|
2306
|
+
query,
|
|
2307
|
+
request.matchCase ? "mc" : "",
|
|
2308
|
+
request.wholeWord ? "ww" : "",
|
|
2309
|
+
request.regex ? "rx" : "",
|
|
2310
|
+
includeKey,
|
|
2311
|
+
excludeKey,
|
|
2312
|
+
PROVIDER_VERSION
|
|
2313
|
+
].join("|");
|
|
2314
|
+
const cached = this.cache.get(key);
|
|
2315
|
+
if (cached) return {
|
|
2316
|
+
matches: cached.matches,
|
|
2317
|
+
truncated: cached.truncated
|
|
2318
|
+
};
|
|
2319
|
+
this.inflight.get(root)?.abort();
|
|
2320
|
+
const controller = new AbortController();
|
|
2321
|
+
this.inflight.set(root, controller);
|
|
2322
|
+
try {
|
|
2323
|
+
const result = await this.provider.search({
|
|
2324
|
+
ctx: this.ctx,
|
|
2325
|
+
session: request.session,
|
|
2326
|
+
cwd: request.cwd,
|
|
2327
|
+
query,
|
|
2328
|
+
matchCase: request.matchCase,
|
|
2329
|
+
wholeWord: request.wholeWord,
|
|
2330
|
+
regex: request.regex,
|
|
2331
|
+
maxResults: request.maxResults,
|
|
2332
|
+
include: request.include,
|
|
2333
|
+
exclude: request.exclude,
|
|
2334
|
+
signal: controller.signal,
|
|
2335
|
+
root
|
|
2336
|
+
});
|
|
2337
|
+
if (controller.signal.aborted) return {
|
|
2338
|
+
matches: [],
|
|
2339
|
+
truncated: false
|
|
2340
|
+
};
|
|
2341
|
+
this.cache.set(key, result);
|
|
2342
|
+
return {
|
|
2343
|
+
matches: result.matches,
|
|
2344
|
+
truncated: result.truncated
|
|
2345
|
+
};
|
|
2346
|
+
} catch (error) {
|
|
2347
|
+
if (controller.signal.aborted) return {
|
|
2348
|
+
matches: [],
|
|
2349
|
+
truncated: false
|
|
2350
|
+
};
|
|
2351
|
+
throw error;
|
|
2352
|
+
} finally {
|
|
2353
|
+
if (this.inflight.get(root) === controller) this.inflight.delete(root);
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
/**
|
|
2357
|
+
* 清理会话对应根目录的缓存和在途搜索。
|
|
2358
|
+
* @author ddj 2026年08月26号
|
|
2359
|
+
* @param cwd 会话工作区
|
|
2360
|
+
*/
|
|
2361
|
+
dispose(cwd) {
|
|
2362
|
+
const roots = this.rootsByCwd.get(cwd) ?? /* @__PURE__ */ new Set([cwd]);
|
|
2363
|
+
for (const root of roots) {
|
|
2364
|
+
this.inflight.get(root)?.abort();
|
|
2365
|
+
this.inflight.delete(root);
|
|
2366
|
+
this.cache.clearRoot(root);
|
|
2367
|
+
}
|
|
2368
|
+
this.rootsByCwd.delete(cwd);
|
|
2369
|
+
}
|
|
2370
|
+
/**
|
|
2371
|
+
* 清理全部状态。
|
|
2372
|
+
* @author ddj 2026年08月26号
|
|
2373
|
+
*/
|
|
2374
|
+
disposeAll() {
|
|
2375
|
+
for (const controller of this.inflight.values()) controller.abort();
|
|
2376
|
+
this.inflight.clear();
|
|
2377
|
+
this.rootsByCwd.clear();
|
|
2378
|
+
this.cache.clear();
|
|
2379
|
+
}
|
|
2380
|
+
};
|
|
2381
|
+
/**
|
|
2382
|
+
* 创建默认内容搜索编排器。
|
|
2383
|
+
* @author ddj 2026年08月26号
|
|
2384
|
+
* @param ctx DSH 上下文
|
|
2385
|
+
* @returns 内容搜索编排器
|
|
2386
|
+
*/
|
|
2387
|
+
function newContentSearcher(ctx) {
|
|
2388
|
+
return new ContentSearcher(ctx);
|
|
2389
|
+
}
|
|
2390
|
+
//#endregion
|
|
2007
2391
|
//#region src/revert.ts
|
|
2008
2392
|
/**
|
|
2009
2393
|
* 删除新建文件(拒绝创建时):subprocess 删除,路径先经 fs.contains 校验工作区边界。
|
|
@@ -2543,10 +2927,73 @@ async function revealInExplorer(ctx, absPath, isDir) {
|
|
|
2543
2927
|
}
|
|
2544
2928
|
//#endregion
|
|
2545
2929
|
//#region src/rpc.ts
|
|
2546
|
-
/** cwd →
|
|
2930
|
+
/** cwd → 内存缓冲:行数组 + 累计字节数 + 待触发 flush 定时器(攒批落盘,避免每条日志全文件读改写)。 */
|
|
2931
|
+
const debugBuffers = /* @__PURE__ */ new Map();
|
|
2932
|
+
/** cwd → Promise 链:串行化 debug 日志落盘(fs read+write 非原子,避免并发丢行)。 */
|
|
2547
2933
|
const debugWriteQueues = /* @__PURE__ */ new Map();
|
|
2548
2934
|
/** debug 日志单文件上限:超限截断保留尾部,防文件无限增长拖慢每次追加。 */
|
|
2549
2935
|
const DEBUG_LOG_CAP = 524288;
|
|
2936
|
+
/** debug 日志批量缓冲上限:攒满即落盘(一次读改写),上限内不逐条写文件。 */
|
|
2937
|
+
const DEBUG_BUF_CAP = 32768;
|
|
2938
|
+
/** debug 日志空闲 flush 延迟:缓冲未满时,静默一段时间后落盘一次。 */
|
|
2939
|
+
const DEBUG_FLUSH_IDLE_MS = 1e3;
|
|
2940
|
+
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
2941
|
+
const staleCheckedAt = /* @__PURE__ */ new Map();
|
|
2942
|
+
/** stale 自动清理最小间隔。 */
|
|
2943
|
+
const STALE_CHECK_MIN_MS = 1e4;
|
|
2944
|
+
/**
|
|
2945
|
+
* 调试日志入队:先攒内存缓冲(行 + 字节数),满 DEBUG_BUF_CAP 立即落盘,
|
|
2946
|
+
* 否则空闲 DEBUG_FLUSH_IDLE_MS 后落盘;落盘 = 读旧文件 + 追加整批 + 超限截断 + 一次写入。
|
|
2947
|
+
* @author ddj 2026年08月26号
|
|
2948
|
+
* @param ctx DSH 上下文
|
|
2949
|
+
* @param cwd 工作区(日志文件按工作区旁车存放)
|
|
2950
|
+
* @param policy 会话沙箱策略(入队时刻捕获)
|
|
2951
|
+
* @param line 单条日志文本(不含换行)
|
|
2952
|
+
*/
|
|
2953
|
+
function enqueueDebug(ctx, cwd, policy, line) {
|
|
2954
|
+
const st = debugBuffers.get(cwd) ?? {
|
|
2955
|
+
lines: [],
|
|
2956
|
+
len: 0,
|
|
2957
|
+
timer: null
|
|
2958
|
+
};
|
|
2959
|
+
st.lines.push(line);
|
|
2960
|
+
st.len += line.length;
|
|
2961
|
+
const flush = () => {
|
|
2962
|
+
st.timer = null;
|
|
2963
|
+
const batch = st.lines;
|
|
2964
|
+
st.lines = [];
|
|
2965
|
+
st.len = 0;
|
|
2966
|
+
flushDebug(ctx, cwd, policy, batch);
|
|
2967
|
+
};
|
|
2968
|
+
if (st.len >= DEBUG_BUF_CAP) {
|
|
2969
|
+
if (st.timer) clearTimeout(st.timer);
|
|
2970
|
+
flush();
|
|
2971
|
+
} else if (!st.timer) st.timer = setTimeout(flush, DEBUG_FLUSH_IDLE_MS);
|
|
2972
|
+
debugBuffers.set(cwd, st);
|
|
2973
|
+
}
|
|
2974
|
+
/**
|
|
2975
|
+
* 批量落盘一条 debug 日志缓冲:读旧文件 → 追加 → 超上限截断保留尾部 → 一次写回。
|
|
2976
|
+
* 串行链保证同一 cwd 的读改写不交错丢行;写失败静默忽略(调试日志不阻塞业务)。
|
|
2977
|
+
* @author ddj 2026年08月26号
|
|
2978
|
+
* @param ctx DSH 上下文
|
|
2979
|
+
* @param cwd 工作区
|
|
2980
|
+
* @param policy 会话沙箱策略(enqueue 时刻捕获)
|
|
2981
|
+
* @param batch 本批日志行
|
|
2982
|
+
*/
|
|
2983
|
+
function flushDebug(ctx, cwd, policy, batch) {
|
|
2984
|
+
const task = (debugWriteQueues.get(cwd) ?? Promise.resolve()).then(async () => {
|
|
2985
|
+
try {
|
|
2986
|
+
const fs = ctx.get("fs");
|
|
2987
|
+
if (!fs) return;
|
|
2988
|
+
const target = await fs.resolve(".dsh-edit-review-debug.log", { cwd });
|
|
2989
|
+
const appended = await fs.readText(target).catch(() => "") + batch.map((l) => (/* @__PURE__ */ new Date()).toISOString() + " " + l + "\n").join("");
|
|
2990
|
+
const next = appended.length > DEBUG_LOG_CAP ? appended.slice(-Math.floor(DEBUG_LOG_CAP / 2)) : appended;
|
|
2991
|
+
await fs.writeText(target, next, void 0, void 0, policy);
|
|
2992
|
+
} catch (e) {}
|
|
2993
|
+
});
|
|
2994
|
+
debugWriteQueues.set(cwd, task);
|
|
2995
|
+
return task;
|
|
2996
|
+
}
|
|
2550
2997
|
/** 记录 → 客户端视图(不含 before 全文,仅长度)。 */
|
|
2551
2998
|
function recView(record) {
|
|
2552
2999
|
return {
|
|
@@ -2627,7 +3074,7 @@ async function applyDecisions(ctx, session, cwd, bucket, items) {
|
|
|
2627
3074
|
return results;
|
|
2628
3075
|
}
|
|
2629
3076
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
2630
|
-
function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
3077
|
+
function buildHandlers(ctx, registry, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
|
|
2631
3078
|
return {
|
|
2632
3079
|
"edrv.list": async (args) => {
|
|
2633
3080
|
const sc = await requireSession(ctx, args.sessionId);
|
|
@@ -2637,7 +3084,13 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2637
3084
|
};
|
|
2638
3085
|
const bucket = await bucketOf(registry, ctx, sc.cwd);
|
|
2639
3086
|
const want = Array.isArray(args.callIds) ? new Set(args.callIds) : null;
|
|
2640
|
-
if (!want && args.skipStale !== true)
|
|
3087
|
+
if (!want && args.skipStale !== true) {
|
|
3088
|
+
const now = Date.now();
|
|
3089
|
+
if (now - (staleCheckedAt.get(sc.cwd) ?? 0) > STALE_CHECK_MIN_MS) {
|
|
3090
|
+
staleCheckedAt.set(sc.cwd, now);
|
|
3091
|
+
await autoArchiveStale(ctx, sc.session, sc.cwd, bucket);
|
|
3092
|
+
}
|
|
3093
|
+
}
|
|
2641
3094
|
const out = [];
|
|
2642
3095
|
for (const rec of bucket.values()) {
|
|
2643
3096
|
if (!want && rec.archived) continue;
|
|
@@ -2943,19 +3396,8 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2943
3396
|
ok: false,
|
|
2944
3397
|
error: sc.err
|
|
2945
3398
|
};
|
|
2946
|
-
const fs = ctx.get("fs");
|
|
2947
3399
|
const text = String(args.text ?? "");
|
|
2948
|
-
|
|
2949
|
-
try {
|
|
2950
|
-
const target = await fs.resolve(".dsh-edit-review-debug.log", { cwd: sc.cwd });
|
|
2951
|
-
const old = await fs.readText(target).catch(() => "");
|
|
2952
|
-
const line = (/* @__PURE__ */ new Date()).toISOString() + " " + text + "\n";
|
|
2953
|
-
const next = old.length + line.length > DEBUG_LOG_CAP ? old.slice(-Math.floor(DEBUG_LOG_CAP / 2)) + line : old + line;
|
|
2954
|
-
await fs.writeText(target, next, void 0, void 0, policyOf(ctx, sc.session));
|
|
2955
|
-
} catch (e) {}
|
|
2956
|
-
});
|
|
2957
|
-
debugWriteQueues.set(sc.cwd, task);
|
|
2958
|
-
await task;
|
|
3400
|
+
enqueueDebug(ctx, sc.cwd, policyOf(ctx, sc.session), text);
|
|
2959
3401
|
console.error("[edrv-debug] " + text);
|
|
2960
3402
|
return { ok: true };
|
|
2961
3403
|
},
|
|
@@ -2978,6 +3420,34 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
2978
3420
|
})
|
|
2979
3421
|
};
|
|
2980
3422
|
},
|
|
3423
|
+
"edrv.searchContent": async (args) => {
|
|
3424
|
+
const sc = await requireSession(ctx, args.sessionId);
|
|
3425
|
+
if ("err" in sc) return {
|
|
3426
|
+
ok: false,
|
|
3427
|
+
error: sc.err
|
|
3428
|
+
};
|
|
3429
|
+
try {
|
|
3430
|
+
return {
|
|
3431
|
+
ok: true,
|
|
3432
|
+
...await contentSearcher.search({
|
|
3433
|
+
session: sc.session,
|
|
3434
|
+
cwd: sc.cwd,
|
|
3435
|
+
query: args.query,
|
|
3436
|
+
matchCase: args.matchCase,
|
|
3437
|
+
wholeWord: args.wholeWord,
|
|
3438
|
+
regex: args.regex,
|
|
3439
|
+
maxResults: args.maxResults,
|
|
3440
|
+
include: args.include,
|
|
3441
|
+
exclude: args.exclude
|
|
3442
|
+
})
|
|
3443
|
+
};
|
|
3444
|
+
} catch (error) {
|
|
3445
|
+
return {
|
|
3446
|
+
ok: false,
|
|
3447
|
+
error: "搜索失败:" + String(error)
|
|
3448
|
+
};
|
|
3449
|
+
}
|
|
3450
|
+
},
|
|
2981
3451
|
"edrv.listDir": async (args) => {
|
|
2982
3452
|
const sc = await requireSession(ctx, args.sessionId);
|
|
2983
3453
|
if ("err" in sc) return {
|
|
@@ -3240,10 +3710,10 @@ function buildHandlers(ctx, registry, searcher = newSearcher(ctx)) {
|
|
|
3240
3710
|
}
|
|
3241
3711
|
/**
|
|
3242
3712
|
* 统一入口:按方法分发到 handler 表。
|
|
3243
|
-
* @author ddj 2026年08月20号
|
|
3713
|
+
* @author ddj 2026年08月20号 / 2026年08月26号
|
|
3244
3714
|
*/
|
|
3245
|
-
async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx)) {
|
|
3246
|
-
const handler = buildHandlers(ctx, registry, searcher)[method];
|
|
3715
|
+
async function handleRpc(ctx, registry, method, args, searcher = newSearcher(ctx), contentSearcher = newContentSearcher(ctx)) {
|
|
3716
|
+
const handler = buildHandlers(ctx, registry, searcher, contentSearcher)[method];
|
|
3247
3717
|
if (!handler) return {
|
|
3248
3718
|
ok: false,
|
|
3249
3719
|
error: "未知方法: " + String(method)
|
|
@@ -3414,6 +3884,7 @@ const inject = [
|
|
|
3414
3884
|
function apply(ctx, config) {
|
|
3415
3885
|
const registry = /* @__PURE__ */ new Map();
|
|
3416
3886
|
const searcher = newSearcher(ctx);
|
|
3887
|
+
const contentSearcher = newContentSearcher(ctx);
|
|
3417
3888
|
/** 兼容性警告收集(route 护栏等写入,启动日志一并输出)。 */
|
|
3418
3889
|
const warnings = [];
|
|
3419
3890
|
setupOpenSettings(ctx, config, () => {});
|
|
@@ -3426,9 +3897,10 @@ function apply(ctx, config) {
|
|
|
3426
3897
|
registry.delete(cwd);
|
|
3427
3898
|
dropFileIndex(cwd);
|
|
3428
3899
|
searcher.dispose(cwd);
|
|
3900
|
+
contentSearcher.dispose(cwd);
|
|
3429
3901
|
}
|
|
3430
3902
|
});
|
|
3431
|
-
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher), (warning) => warnings.push(warning));
|
|
3903
|
+
registerRoutes(ctx, config, (method, args) => handleRpc(ctx, registry, method, args, searcher, contentSearcher), (warning) => warnings.push(warning));
|
|
3432
3904
|
installIsolation(ctx);
|
|
3433
3905
|
logCompatSummary(ctx, warnings);
|
|
3434
3906
|
ctx.logger?.info?.("[dsh-vscode-mode] 编辑差异审查已装配(/edrv/rpc 路由就绪,项目 MCP 隔离已启用)");
|