dsh-vscode-mode 0.1.46 → 0.1.48
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 +260 -179
- package/lib/client.js.map +1 -1
- package/lib/index.js +485 -29
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +32 -19
- package/src/client/sidebar/menuItems.ts +23 -2
- package/src/client/sidebar/panels/FileExplorer.ts +2 -4
- package/src/client/sidebar/types.ts +3 -0
- package/src/client/ui/EditorView.ts +3 -9
- package/src/client/ui/LspSettings.ts +83 -5
- package/src/client/ui/menuPosition.ts +2 -28
- package/src/lsp/config.ts +5 -1
- package/src/lsp/dotnetProvision.ts +224 -0
- package/src/lsp/envRequirements.ts +92 -0
- package/src/lsp/providers.ts +198 -25
- package/src/lsp/rpc.ts +24 -1
- package/src/lsp/server.ts +8 -1
- package/src/shared/lsp.ts +18 -0
- package/src/shared/rpc.ts +5 -1
package/lib/client.js
CHANGED
|
@@ -2601,6 +2601,154 @@ window.__ModuleLoader__.load({
|
|
|
2601
2601
|
};
|
|
2602
2602
|
}
|
|
2603
2603
|
//#endregion
|
|
2604
|
+
//#region src/client/addToConversation.ts
|
|
2605
|
+
/** 把追加引用结果映射为可读文案(ok 用 okText;busy 提示已降级纯文本;unavailable 提示不可用)。
|
|
2606
|
+
* @author ddj 2026年09月03号
|
|
2607
|
+
* @param outcome 追加结果状态
|
|
2608
|
+
* @param okText 成功文案(如「已添加文件引用」/「已添加文件夹引用」)
|
|
2609
|
+
* @returns 状态栏/通知用文案
|
|
2610
|
+
*/
|
|
2611
|
+
function statusOfAdd(outcome, okText) {
|
|
2612
|
+
if (outcome === "ok") return okText;
|
|
2613
|
+
if (outcome === "busy") return okText + "(输入框忙,已降级纯文本)";
|
|
2614
|
+
return "无法添加到对话(无会话或输入框不可用)";
|
|
2615
|
+
}
|
|
2616
|
+
/** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
|
|
2617
|
+
const REF_SOURCE = "reference";
|
|
2618
|
+
/**
|
|
2619
|
+
* 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
|
|
2620
|
+
* cwd 外/无法相对化的路径回退原路径。
|
|
2621
|
+
* @author ddj 2026年08月25号
|
|
2622
|
+
* @param path 打开的文件路径(相对 cwd 或绝对)
|
|
2623
|
+
* @param cwd 会话工作区目录(可选)
|
|
2624
|
+
* @returns 引用串(如 @src/index.ts 或 @"a b.ts")
|
|
2625
|
+
*/
|
|
2626
|
+
function mentionOf(path, cwd) {
|
|
2627
|
+
const raw = String(path);
|
|
2628
|
+
let rel = raw;
|
|
2629
|
+
if (cwd) {
|
|
2630
|
+
const base = String(cwd).replace(/[\\/]+$/, "");
|
|
2631
|
+
const up = raw.replace(/\\/g, "/");
|
|
2632
|
+
const baseUp = base.replace(/\\/g, "/");
|
|
2633
|
+
if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
|
|
2634
|
+
else if (up === baseUp) rel = "";
|
|
2635
|
+
}
|
|
2636
|
+
rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
2637
|
+
if (!rel) rel = raw.replace(/\\/g, "/");
|
|
2638
|
+
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
|
|
2639
|
+
return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
|
|
2640
|
+
}
|
|
2641
|
+
/** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
|
|
2642
|
+
* @author ddj 2026年08月25号 */
|
|
2643
|
+
function labelOf(path, range) {
|
|
2644
|
+
const base = String(path).split(/[\\/]/).pop() || String(path);
|
|
2645
|
+
if (!range || range.endLine < range.startLine) return base;
|
|
2646
|
+
return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
|
|
2647
|
+
}
|
|
2648
|
+
/** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
|
|
2649
|
+
* @author ddj 2026年08月25号 */
|
|
2650
|
+
function refOf(mention, range) {
|
|
2651
|
+
if (!range || range.endLine < range.startLine) return mention;
|
|
2652
|
+
return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
|
|
2653
|
+
}
|
|
2654
|
+
/** 构造 DSH 文件引用插入载荷(source=reference,发送序列化为 mention(+range) 文本)。
|
|
2655
|
+
* @param path 目标路径(相对 cwd 或绝对)
|
|
2656
|
+
* @param cwd 会话工作区目录(可选)
|
|
2657
|
+
* @param range 行区间(可选)
|
|
2658
|
+
* @param appearance 引用外观:'file'(默认)| 'folder'
|
|
2659
|
+
*/
|
|
2660
|
+
function buildFileRef(path, cwd, range, appearance = "file") {
|
|
2661
|
+
const mention = mentionOf(path, cwd);
|
|
2662
|
+
return {
|
|
2663
|
+
mention,
|
|
2664
|
+
reference: {
|
|
2665
|
+
source: REF_SOURCE,
|
|
2666
|
+
ref: refOf(mention, range),
|
|
2667
|
+
label: labelOf(path, range),
|
|
2668
|
+
appearance,
|
|
2669
|
+
clipboardText: mention
|
|
2670
|
+
}
|
|
2671
|
+
};
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
|
|
2675
|
+
* @author ddj 2026年08月25号
|
|
2676
|
+
* @param ctx 客户端服务上下文
|
|
2677
|
+
* @param sessionId 会话 id
|
|
2678
|
+
* @returns SessionInput 或 undefined
|
|
2679
|
+
*/
|
|
2680
|
+
function inputFor(ctx, sessionId) {
|
|
2681
|
+
if (!sessionId) return void 0;
|
|
2682
|
+
try {
|
|
2683
|
+
const sessions = ctx.get("sessions");
|
|
2684
|
+
const conversation = ctx.get("conversation");
|
|
2685
|
+
const actx = sessions?.scope?.(sessionId);
|
|
2686
|
+
const shell = actx && conversation?.input?.for?.(actx);
|
|
2687
|
+
return shell && typeof shell.setDraft === "function" ? shell : void 0;
|
|
2688
|
+
} catch {
|
|
2689
|
+
return;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
/** 取当前草稿长度与版本;无输入门面时返回 null。
|
|
2693
|
+
* @author ddj 2026年08月25号 */
|
|
2694
|
+
function draftCursor(input) {
|
|
2695
|
+
try {
|
|
2696
|
+
const s = input?.state?.getSnapshot?.();
|
|
2697
|
+
if (!s) return null;
|
|
2698
|
+
return {
|
|
2699
|
+
draft: String(s.draft ?? ""),
|
|
2700
|
+
draftRev: Number(s.draftRev ?? 0)
|
|
2701
|
+
};
|
|
2702
|
+
} catch {
|
|
2703
|
+
return null;
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
/** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
|
|
2707
|
+
* @author ddj 2026年08月25号 */
|
|
2708
|
+
function cwdOf(ctx, sessionId) {
|
|
2709
|
+
if (!sessionId) return void 0;
|
|
2710
|
+
try {
|
|
2711
|
+
return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
|
|
2712
|
+
} catch {
|
|
2713
|
+
return;
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2716
|
+
/** 输入门面插入引用(拦截异常视作未应用,走降级)。
|
|
2717
|
+
* @author ddj 2026年08月25号 */
|
|
2718
|
+
function safeInsert(input, reference, span) {
|
|
2719
|
+
try {
|
|
2720
|
+
return input.insertReference(reference, span) === true;
|
|
2721
|
+
} catch {
|
|
2722
|
+
return false;
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
/**
|
|
2726
|
+
* 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
|
|
2727
|
+
* @author ddj 2026年08月25号
|
|
2728
|
+
* @param ctx 客户端服务上下文(sessions + conversation)
|
|
2729
|
+
* @returns 动作集
|
|
2730
|
+
*/
|
|
2731
|
+
function createAddToConversation(ctx) {
|
|
2732
|
+
const appendReference = async (sessionId, path, range, appearance) => {
|
|
2733
|
+
const input = inputFor(ctx, sessionId);
|
|
2734
|
+
if (!input) return "unavailable";
|
|
2735
|
+
const cur = draftCursor(input);
|
|
2736
|
+
if (!cur) return "unavailable";
|
|
2737
|
+
const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range, appearance);
|
|
2738
|
+
if (!safeInsert(input, reference, {
|
|
2739
|
+
start: cur.draft.length,
|
|
2740
|
+
end: cur.draft.length,
|
|
2741
|
+
draftRev: cur.draftRev
|
|
2742
|
+
})) {
|
|
2743
|
+
const gap = cur.draft.length > 0 && !/\s$/.test(cur.draft) ? " " : "";
|
|
2744
|
+
input.setDraft(cur.draft + gap + mention + " ");
|
|
2745
|
+
return "busy";
|
|
2746
|
+
}
|
|
2747
|
+
return "ok";
|
|
2748
|
+
};
|
|
2749
|
+
return { appendReference };
|
|
2750
|
+
}
|
|
2751
|
+
//#endregion
|
|
2604
2752
|
//#region src/shared/lsp.ts
|
|
2605
2753
|
/** LSP semantic token 标准类型(host 归一化后,client 与 Monaco 共用)。 */
|
|
2606
2754
|
const LSP_SEMANTIC_TOKEN_TYPES = [
|
|
@@ -4551,14 +4699,7 @@ window.__ModuleLoader__.load({
|
|
|
4551
4699
|
const dismissMenus = () => {
|
|
4552
4700
|
setTabMenu(null);
|
|
4553
4701
|
};
|
|
4554
|
-
/**
|
|
4555
|
-
* @author ddj 2026年08月25号 */
|
|
4556
|
-
const statusOfAdd = (outcome, okText) => {
|
|
4557
|
-
if (outcome === "ok") return okText;
|
|
4558
|
-
if (outcome === "busy") return okText + "(输入框忙,已降级纯文本)";
|
|
4559
|
-
return "无法添加到对话(无会话或输入框不可用)";
|
|
4560
|
-
};
|
|
4561
|
-
/** 添加文件/选中区引用到对话(异步,状态栏反馈)。 */
|
|
4702
|
+
/** 添加文件/选中区引用到对话(异步,状态栏反馈;状态文案走共享 statusOfAdd)。 */
|
|
4562
4703
|
const addRefToChat = (path, range) => {
|
|
4563
4704
|
if (!path) {
|
|
4564
4705
|
setStatus("无活动文件");
|
|
@@ -4957,6 +5098,7 @@ window.__ModuleLoader__.load({
|
|
|
4957
5098
|
editor: () => editorRef.current,
|
|
4958
5099
|
outlineSources: props.outlineSources,
|
|
4959
5100
|
fileMenuItems: props.fileMenuItems,
|
|
5101
|
+
addToConversation,
|
|
4960
5102
|
notify: (message) => setStatus(message)
|
|
4961
5103
|
};
|
|
4962
5104
|
const editorArea = react.default.createElement("div", { className: "edrv-editor-area" }, body, hoverEl, overlay);
|
|
@@ -5804,8 +5946,8 @@ window.__ModuleLoader__.load({
|
|
|
5804
5946
|
hint: "优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server"
|
|
5805
5947
|
}, {
|
|
5806
5948
|
id: "csharp",
|
|
5807
|
-
label: "C#(Roslyn / OmniSharp)",
|
|
5808
|
-
hint: "
|
|
5949
|
+
label: "C#(Roslyn / DotRush / OmniSharp)",
|
|
5950
|
+
hint: "优先自动发现 ms-dotnettools.csharp / DotRush 扩展(DotRush 需 .NET 10 运行时);也可手动指定"
|
|
5809
5951
|
}];
|
|
5810
5952
|
const PHASE_LABEL = {
|
|
5811
5953
|
idle: "未启动",
|
|
@@ -5839,8 +5981,17 @@ window.__ModuleLoader__.load({
|
|
|
5839
5981
|
label: "更新"
|
|
5840
5982
|
}
|
|
5841
5983
|
];
|
|
5842
|
-
/**
|
|
5843
|
-
function
|
|
5984
|
+
/** 环境安装按钮文案(按安装进行态)。 */
|
|
5985
|
+
function envButtonLabel(req, envStates) {
|
|
5986
|
+
const state = (envStates ?? []).find((s) => s.id === req.id);
|
|
5987
|
+
if (state?.phase === "downloading") return "下载中…";
|
|
5988
|
+
if (state?.phase === "extracting") return "安装中…";
|
|
5989
|
+
if (state?.phase === "done") return "已完成";
|
|
5990
|
+
if (state?.phase === "failed") return "重试";
|
|
5991
|
+
return "一键安装";
|
|
5992
|
+
}
|
|
5993
|
+
/** 语言卡片:状态 + 缺失环境提示(一键安装/官网下载) + 启用开关 + 命令/路径配置 + 重新检测。 */
|
|
5994
|
+
function LangCard({ lang, config, status, busy, envStates, onInstallEnv, onToggle, onSave, onRedetect }) {
|
|
5844
5995
|
const [pathDraft, setPathDraft] = react.default.useState(config?.path ?? "");
|
|
5845
5996
|
const [commandDraft, setCommandDraft] = react.default.useState(config?.command ?? "");
|
|
5846
5997
|
react.default.useEffect(() => {
|
|
@@ -5852,7 +6003,23 @@ window.__ModuleLoader__.load({
|
|
|
5852
6003
|
className: "vsm-switch " + (config?.enabled !== false ? "on" : ""),
|
|
5853
6004
|
onClick: () => onToggle(lang.id, config?.enabled !== false ? false : true),
|
|
5854
6005
|
"aria-label": "启用/禁用"
|
|
5855
|
-
}, config?.enabled !== false ? "●" : "○"))), react.default.createElement("div", { className: "vsm-mcp-meta" }, PHASE_LABEL[phase] ?? phase, " · ", SOURCE_LABEL[status?.source] ?? status?.source, status?.providerName ? " · " + status.providerName : "", status?.version ? " · v" + status.version : "", phase === "ready" && status?.root ? " · " + String(status.root).split(/[\\/]/).pop() : ""), status?.reason ? react.default.createElement("div", { className: "vsm-mcp-error" }, status.reason) : null, react.default.createElement("div", { className: "vsm-lsp-
|
|
6006
|
+
}, config?.enabled !== false ? "●" : "○"))), react.default.createElement("div", { className: "vsm-mcp-meta" }, PHASE_LABEL[phase] ?? phase, " · ", SOURCE_LABEL[status?.source] ?? status?.source, status?.providerName ? " · " + status.providerName : "", status?.version ? " · v" + status.version : "", phase === "ready" && status?.root ? " · " + String(status.root).split(/[\\/]/).pop() : ""), status?.reason ? react.default.createElement("div", { className: "vsm-mcp-error" }, status.reason) : null, Array.isArray(status?.missingEnv) && status.missingEnv.length ? react.default.createElement("div", { className: "vsm-lsp-hint" }, "缺少运行环境(一键安装后自动生效):", status.missingEnv.map((req) => react.default.createElement("div", {
|
|
6007
|
+
key: req.id,
|
|
6008
|
+
className: "vsm-mcp-actions"
|
|
6009
|
+
}, react.default.createElement("span", null, req.label, req.detail ? "(" + req.detail + ")" : ""), req.installable ? react.default.createElement("button", {
|
|
6010
|
+
className: "vsm-primary vsm-small",
|
|
6011
|
+
disabled: busy === "env:" + req.id,
|
|
6012
|
+
onClick: () => onInstallEnv(lang.id, req)
|
|
6013
|
+
}, envButtonLabel(req, envStates)) : null, req.manualUrl ? react.default.createElement("a", {
|
|
6014
|
+
href: req.manualUrl,
|
|
6015
|
+
target: "_blank",
|
|
6016
|
+
rel: "noreferrer",
|
|
6017
|
+
style: {
|
|
6018
|
+
color: "inherit",
|
|
6019
|
+
textDecoration: "underline",
|
|
6020
|
+
alignSelf: "center"
|
|
6021
|
+
}
|
|
6022
|
+
}, req.manualLabel ?? "官网下载") : null))) : null, react.default.createElement("div", { className: "vsm-lsp-form" }, react.default.createElement("label", null, react.default.createElement("span", null, "可执行文件路径(绝对路径,优先)"), react.default.createElement("input", {
|
|
5856
6023
|
value: pathDraft,
|
|
5857
6024
|
placeholder: "如 C:/.../lua-language-server.exe",
|
|
5858
6025
|
onChange: (e) => setPathDraft(e.target.value)
|
|
@@ -5908,6 +6075,8 @@ window.__ModuleLoader__.load({
|
|
|
5908
6075
|
const [busy, setBusy] = react.default.useState("");
|
|
5909
6076
|
const [error, setError] = react.default.useState("");
|
|
5910
6077
|
const [loading, setLoading] = react.default.useState(true);
|
|
6078
|
+
const [envStates, setEnvStates] = react.default.useState([]);
|
|
6079
|
+
const [envLang, setEnvLang] = react.default.useState("");
|
|
5911
6080
|
const refreshServers = react.default.useCallback(() => {
|
|
5912
6081
|
Promise.all([rpc("edrv.lsp.configGet", {}), rpc("edrv.lsp.status", {})]).then(([cfg, st]) => {
|
|
5913
6082
|
if (cfg?.ok) setConfig(cfg.config ?? {});
|
|
@@ -5922,6 +6091,12 @@ window.__ModuleLoader__.load({
|
|
|
5922
6091
|
setError("");
|
|
5923
6092
|
}).catch((e) => setError(String(e)));
|
|
5924
6093
|
}, []);
|
|
6094
|
+
const refreshEnvStates = react.default.useCallback(() => {
|
|
6095
|
+
return rpc("edrv.lsp.envState", {}).then((res) => {
|
|
6096
|
+
if (res?.ok) setEnvStates(res.states ?? []);
|
|
6097
|
+
return res?.states ?? [];
|
|
6098
|
+
}).catch(() => []);
|
|
6099
|
+
}, []);
|
|
5925
6100
|
const refresh = react.default.useCallback(() => {
|
|
5926
6101
|
setLoading(true);
|
|
5927
6102
|
Promise.all([refreshServers(), refreshExt()]).finally(() => setLoading(false));
|
|
@@ -5941,9 +6116,56 @@ window.__ModuleLoader__.load({
|
|
|
5941
6116
|
setServers((prev) => Array.isArray(list) ? [...prev.filter((s) => s.languageId !== languageId), ...list] : prev);
|
|
5942
6117
|
};
|
|
5943
6118
|
/** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
|
|
5944
|
-
const notifyResync = (languageId) => {
|
|
6119
|
+
const notifyResync = react.default.useCallback((languageId) => {
|
|
5945
6120
|
window.dispatchEvent(new CustomEvent("edrv:lsp-redetect", { detail: { languageId } }));
|
|
6121
|
+
}, []);
|
|
6122
|
+
/** 一键安装缺失环境:启动内置安装器(成功后进入轮询,完成自动重同步)。 */
|
|
6123
|
+
const installEnv = (languageId, req) => {
|
|
6124
|
+
const key = "env:" + req.id;
|
|
6125
|
+
setBusy(key);
|
|
6126
|
+
setError("");
|
|
6127
|
+
setEnvLang(languageId);
|
|
6128
|
+
rpc("edrv.lsp.envInstall", {
|
|
6129
|
+
languageId,
|
|
6130
|
+
id: req.id
|
|
6131
|
+
}).then((res) => {
|
|
6132
|
+
if (!res?.ok) {
|
|
6133
|
+
setError(res?.error ?? "无法启动安装");
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
refreshEnvStates();
|
|
6137
|
+
refreshServers();
|
|
6138
|
+
}).catch((e) => setError(String(e))).finally(() => setBusy(""));
|
|
5946
6139
|
};
|
|
6140
|
+
react.default.useEffect(() => {
|
|
6141
|
+
if (!(envStates ?? []).some((s) => s.phase === "downloading" || s.phase === "extracting")) return;
|
|
6142
|
+
const timer = setInterval(() => {
|
|
6143
|
+
refreshEnvStates();
|
|
6144
|
+
refreshServers();
|
|
6145
|
+
}, 2e3);
|
|
6146
|
+
return () => clearInterval(timer);
|
|
6147
|
+
}, [
|
|
6148
|
+
envStates,
|
|
6149
|
+
refreshEnvStates,
|
|
6150
|
+
refreshServers
|
|
6151
|
+
]);
|
|
6152
|
+
const prevEnvPhases = react.default.useRef({});
|
|
6153
|
+
react.default.useEffect(() => {
|
|
6154
|
+
const states = envStates ?? [];
|
|
6155
|
+
for (const s of states) {
|
|
6156
|
+
const prev = prevEnvPhases.current[s.id];
|
|
6157
|
+
if (prev && prev !== "done" && s.phase === "done" && envLang) {
|
|
6158
|
+
refreshServers();
|
|
6159
|
+
notifyResync(envLang);
|
|
6160
|
+
}
|
|
6161
|
+
prevEnvPhases.current[s.id] = s.phase;
|
|
6162
|
+
}
|
|
6163
|
+
}, [
|
|
6164
|
+
envStates,
|
|
6165
|
+
envLang,
|
|
6166
|
+
refreshServers,
|
|
6167
|
+
notifyResync
|
|
6168
|
+
]);
|
|
5947
6169
|
const saveLang = (languageId, path, command) => {
|
|
5948
6170
|
setBusy(languageId);
|
|
5949
6171
|
setError("");
|
|
@@ -6064,6 +6286,8 @@ window.__ModuleLoader__.load({
|
|
|
6064
6286
|
config: config[lang.id] ?? {},
|
|
6065
6287
|
status: servers.find((s) => s.languageId === lang.id),
|
|
6066
6288
|
busy,
|
|
6289
|
+
envStates,
|
|
6290
|
+
onInstallEnv: installEnv,
|
|
6067
6291
|
onToggle: toggleLang,
|
|
6068
6292
|
onSave: saveLang,
|
|
6069
6293
|
onRedetect: redetectLang
|
|
@@ -7148,147 +7372,6 @@ window.__ModuleLoader__.load({
|
|
|
7148
7372
|
});
|
|
7149
7373
|
}
|
|
7150
7374
|
//#endregion
|
|
7151
|
-
//#region src/client/addToConversation.ts
|
|
7152
|
-
/** DSH reference source 名(dsh-client-ui-reference 注册的 @file/@session 统一源)。 */
|
|
7153
|
-
const REF_SOURCE = "reference";
|
|
7154
|
-
/**
|
|
7155
|
-
* 生成 DSH @file 语法引用串:cwd 相对化、\ → /、含空白时按 @"path" 语法加引号。
|
|
7156
|
-
* cwd 外/无法相对化的路径回退原路径。
|
|
7157
|
-
* @author ddj 2026年08月25号
|
|
7158
|
-
* @param path 打开的文件路径(相对 cwd 或绝对)
|
|
7159
|
-
* @param cwd 会话工作区目录(可选)
|
|
7160
|
-
* @returns 引用串(如 @src/index.ts 或 @"a b.ts")
|
|
7161
|
-
*/
|
|
7162
|
-
function mentionOf(path, cwd) {
|
|
7163
|
-
const raw = String(path);
|
|
7164
|
-
let rel = raw;
|
|
7165
|
-
if (cwd) {
|
|
7166
|
-
const base = String(cwd).replace(/[\\/]+$/, "");
|
|
7167
|
-
const up = raw.replace(/\\/g, "/");
|
|
7168
|
-
const baseUp = base.replace(/\\/g, "/");
|
|
7169
|
-
if (up.startsWith(baseUp + "/")) rel = up.slice(baseUp.length + 1);
|
|
7170
|
-
else if (up === baseUp) rel = "";
|
|
7171
|
-
}
|
|
7172
|
-
rel = rel.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
7173
|
-
if (!rel) rel = raw.replace(/\\/g, "/");
|
|
7174
|
-
if (/[\u0000-\u001f\u007f-\u009f"]/u.test(rel)) return "@" + raw;
|
|
7175
|
-
return /\s/u.test(rel) ? `@"${rel}"` : `@${rel}`;
|
|
7176
|
-
}
|
|
7177
|
-
/** 引用标签:文件名 + 可选行区间(chip 内展示,不带 @ 前缀)。
|
|
7178
|
-
* @author ddj 2026年08月25号 */
|
|
7179
|
-
function labelOf(path, range) {
|
|
7180
|
-
const base = String(path).split(/[\\/]/).pop() || String(path);
|
|
7181
|
-
if (!range || range.endLine < range.startLine) return base;
|
|
7182
|
-
return range.startLine === range.endLine ? `${base} L${range.startLine}` : `${base} L${range.startLine}-${range.endLine}`;
|
|
7183
|
-
}
|
|
7184
|
-
/** 引用 ref:引用串 + 可选行区间(序列化直出,agent 据此读取对应片段)。
|
|
7185
|
-
* @author ddj 2026年08月25号 */
|
|
7186
|
-
function refOf(mention, range) {
|
|
7187
|
-
if (!range || range.endLine < range.startLine) return mention;
|
|
7188
|
-
return range.startLine === range.endLine ? `${mention} L${range.startLine}` : `${mention} L${range.startLine}-${range.endLine}`;
|
|
7189
|
-
}
|
|
7190
|
-
/** 构造 DSH 文件引用插入载荷(source=reference,发送序列化为 mention(+range) 文本)。 */
|
|
7191
|
-
function buildFileRef(path, cwd, range) {
|
|
7192
|
-
const mention = mentionOf(path, cwd);
|
|
7193
|
-
return {
|
|
7194
|
-
mention,
|
|
7195
|
-
reference: {
|
|
7196
|
-
source: REF_SOURCE,
|
|
7197
|
-
ref: refOf(mention, range),
|
|
7198
|
-
label: labelOf(path, range),
|
|
7199
|
-
appearance: "file",
|
|
7200
|
-
clipboardText: mention
|
|
7201
|
-
}
|
|
7202
|
-
};
|
|
7203
|
-
}
|
|
7204
|
-
/**
|
|
7205
|
-
* 解析会话输入门面;缺失时返回 undefined(动作侧守卫降级)。
|
|
7206
|
-
* @author ddj 2026年08月25号
|
|
7207
|
-
* @param ctx 客户端服务上下文
|
|
7208
|
-
* @param sessionId 会话 id
|
|
7209
|
-
* @returns SessionInput 或 undefined
|
|
7210
|
-
*/
|
|
7211
|
-
function inputFor(ctx, sessionId) {
|
|
7212
|
-
if (!sessionId) return void 0;
|
|
7213
|
-
try {
|
|
7214
|
-
const sessions = ctx.get("sessions");
|
|
7215
|
-
const conversation = ctx.get("conversation");
|
|
7216
|
-
const actx = sessions?.scope?.(sessionId);
|
|
7217
|
-
const shell = actx && conversation?.input?.for?.(actx);
|
|
7218
|
-
return shell && typeof shell.setDraft === "function" ? shell : void 0;
|
|
7219
|
-
} catch {
|
|
7220
|
-
return;
|
|
7221
|
-
}
|
|
7222
|
-
}
|
|
7223
|
-
/** 取当前草稿长度与版本;无输入门面时返回 null。
|
|
7224
|
-
* @author ddj 2026年08月25号 */
|
|
7225
|
-
function draftCursor(input) {
|
|
7226
|
-
try {
|
|
7227
|
-
const s = input?.state?.getSnapshot?.();
|
|
7228
|
-
if (!s) return null;
|
|
7229
|
-
return {
|
|
7230
|
-
draft: String(s.draft ?? ""),
|
|
7231
|
-
draftRev: Number(s.draftRev ?? 0)
|
|
7232
|
-
};
|
|
7233
|
-
} catch {
|
|
7234
|
-
return null;
|
|
7235
|
-
}
|
|
7236
|
-
}
|
|
7237
|
-
/** 从 sessions 列表快照读会话工作区目录(与 index.ts 既有读取方式一致)。
|
|
7238
|
-
* @author ddj 2026年08月25号 */
|
|
7239
|
-
function cwdOf(ctx, sessionId) {
|
|
7240
|
-
if (!sessionId) return void 0;
|
|
7241
|
-
try {
|
|
7242
|
-
return ctx.get("sessions")?.list?.getSnapshot?.()?.byId?.[sessionId]?.cwd;
|
|
7243
|
-
} catch {
|
|
7244
|
-
return;
|
|
7245
|
-
}
|
|
7246
|
-
}
|
|
7247
|
-
/** 写入成功后的 composer 内联提示(门面可用时;失败静默)。
|
|
7248
|
-
* @author ddj 2026年08月25号 */
|
|
7249
|
-
function notify(input, text) {
|
|
7250
|
-
try {
|
|
7251
|
-
input.notify?.("info", text);
|
|
7252
|
-
} catch {}
|
|
7253
|
-
}
|
|
7254
|
-
/** 输入门面插入引用(拦截异常视作未应用,走降级)。
|
|
7255
|
-
* @author ddj 2026年08月25号 */
|
|
7256
|
-
function safeInsert(input, reference, span) {
|
|
7257
|
-
try {
|
|
7258
|
-
return input.insertReference(reference, span) === true;
|
|
7259
|
-
} catch {
|
|
7260
|
-
return false;
|
|
7261
|
-
}
|
|
7262
|
-
}
|
|
7263
|
-
/**
|
|
7264
|
-
* 创建「添加到对话」动作集(apply 阶段构建一次,随 props 传给 EditorView)。
|
|
7265
|
-
* @author ddj 2026年08月25号
|
|
7266
|
-
* @param ctx 客户端服务上下文(sessions + conversation)
|
|
7267
|
-
* @returns 动作集
|
|
7268
|
-
*/
|
|
7269
|
-
function createAddToConversation(ctx) {
|
|
7270
|
-
const appendReference = async (sessionId, path, range) => {
|
|
7271
|
-
const input = inputFor(ctx, sessionId);
|
|
7272
|
-
if (!input) return "unavailable";
|
|
7273
|
-
const cur = draftCursor(input);
|
|
7274
|
-
if (!cur) return "unavailable";
|
|
7275
|
-
const { reference, mention } = buildFileRef(path, cwdOf(ctx, sessionId), range);
|
|
7276
|
-
if (!safeInsert(input, reference, {
|
|
7277
|
-
start: cur.draft.length,
|
|
7278
|
-
end: cur.draft.length,
|
|
7279
|
-
draftRev: cur.draftRev
|
|
7280
|
-
})) {
|
|
7281
|
-
const gap = cur.draft.length > 0 && !/\s$/.test(cur.draft) ? " " : "";
|
|
7282
|
-
input.setDraft(cur.draft + gap + mention + " ");
|
|
7283
|
-
notify(input, "已添加文件引用 " + mention);
|
|
7284
|
-
return "busy";
|
|
7285
|
-
}
|
|
7286
|
-
notify(input, "已添加文件引用 " + mention);
|
|
7287
|
-
return "ok";
|
|
7288
|
-
};
|
|
7289
|
-
return { appendReference };
|
|
7290
|
-
}
|
|
7291
|
-
//#endregion
|
|
7292
7375
|
//#region src/client/sidebar/registry.ts
|
|
7293
7376
|
/** 校验面板定义并写入注册表(缺 id/render 抛 TypeError)。 */
|
|
7294
7377
|
function panelRegister(entries, notify, panel) {
|
|
@@ -7346,24 +7429,6 @@ window.__ModuleLoader__.load({
|
|
|
7346
7429
|
top: Math.max(4, Math.min(y, maxTop))
|
|
7347
7430
|
};
|
|
7348
7431
|
}
|
|
7349
|
-
/**
|
|
7350
|
-
* 将文件树行右键菜单锚定到目标行右侧;无法测量时回退到鼠标坐标。
|
|
7351
|
-
* @author ddj 2026年08月28号
|
|
7352
|
-
* @param rect 目标行的布局矩形
|
|
7353
|
-
* @param fallbackX 鼠标 viewport x 坐标
|
|
7354
|
-
* @param fallbackY 鼠标 viewport y 坐标
|
|
7355
|
-
* @returns 菜单初始位置
|
|
7356
|
-
*/
|
|
7357
|
-
function rowMenuPosition(rect, fallbackX, fallbackY) {
|
|
7358
|
-
if (!rect || rect.width <= 0 || rect.height <= 0) return {
|
|
7359
|
-
left: fallbackX,
|
|
7360
|
-
top: fallbackY
|
|
7361
|
-
};
|
|
7362
|
-
return {
|
|
7363
|
-
left: rect.right + 4,
|
|
7364
|
-
top: rect.top
|
|
7365
|
-
};
|
|
7366
|
-
}
|
|
7367
7432
|
//#endregion
|
|
7368
7433
|
//#region src/client/ui/ContextMenu.ts
|
|
7369
7434
|
/**
|
|
@@ -7957,11 +8022,9 @@ window.__ModuleLoader__.load({
|
|
|
7957
8022
|
onContextMenu: (ev) => {
|
|
7958
8023
|
ev.preventDefault();
|
|
7959
8024
|
ev.stopPropagation();
|
|
7960
|
-
const rect = ev.currentTarget?.getBoundingClientRect?.();
|
|
7961
|
-
const position = rowMenuPosition(rect, ev.clientX, ev.clientY);
|
|
7962
8025
|
setMenu({
|
|
7963
|
-
x:
|
|
7964
|
-
y:
|
|
8026
|
+
x: ev.clientX,
|
|
8027
|
+
y: ev.clientY,
|
|
7965
8028
|
target: {
|
|
7966
8029
|
path: e.path,
|
|
7967
8030
|
type: e.type
|
|
@@ -8414,9 +8477,10 @@ window.__ModuleLoader__.load({
|
|
|
8414
8477
|
//#region src/client/sidebar/menuItems.ts
|
|
8415
8478
|
/**
|
|
8416
8479
|
* dsh-vscode-mode client — 文件管理右键菜单内置项。
|
|
8417
|
-
*
|
|
8480
|
+
* 内置项:「在文件浏览器中打开」(文件→OS Explorer 定位选中、目录→打开目录);
|
|
8481
|
+
* 「添加引用到对话」(文件/文件夹引用注入当前会话对话输入框)。
|
|
8418
8482
|
* 反馈统一走 ctx.notify(由 EditorView 提供,落到编辑区路径栏状态)。
|
|
8419
|
-
* 作者 ddj 2026-08-27
|
|
8483
|
+
* 作者 ddj 2026-08-27 / 2026-09-03
|
|
8420
8484
|
*/
|
|
8421
8485
|
/**
|
|
8422
8486
|
* 构造内置右键菜单项列表(后续内置项直接追加)。
|
|
@@ -8433,6 +8497,23 @@ window.__ModuleLoader__.load({
|
|
|
8433
8497
|
ctx.notify?.(outcome.ok ? "已在文件浏览器中打开" : "打开失败:" + (outcome.error ?? "未知错误"));
|
|
8434
8498
|
});
|
|
8435
8499
|
}
|
|
8500
|
+
}, {
|
|
8501
|
+
id: "add-to-conversation",
|
|
8502
|
+
label: "添加引用到对话",
|
|
8503
|
+
order: 1,
|
|
8504
|
+
visible: (target, ctx) => Boolean(target.path && ctx.sessionId && ctx.addToConversation),
|
|
8505
|
+
run: (target, ctx) => {
|
|
8506
|
+
const isDir = target.type === "directory";
|
|
8507
|
+
const add = ctx.addToConversation;
|
|
8508
|
+
if (!add) {
|
|
8509
|
+
ctx.notify?.("添加到对话不可用");
|
|
8510
|
+
return;
|
|
8511
|
+
}
|
|
8512
|
+
const okText = isDir ? "已添加文件夹引用" : "已添加文件引用";
|
|
8513
|
+
add.appendReference(ctx.sessionId, target.path, void 0, isDir ? "folder" : "file").then((outcome) => {
|
|
8514
|
+
ctx.notify?.(statusOfAdd(outcome, okText));
|
|
8515
|
+
});
|
|
8516
|
+
}
|
|
8436
8517
|
}];
|
|
8437
8518
|
}
|
|
8438
8519
|
//#endregion
|