dsh-vscode-mode 0.15.9 → 0.17.0
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 +14 -9
- package/lib/client.js +376 -84
- package/lib/client.js.map +1 -1
- package/lib/index.js +4545 -4454
- package/lib/index.js.map +1 -1
- package/locale/en.json +4 -2
- package/locale/zh.json +4 -2
- package/package.json +4 -2
- package/src/capture.ts +2 -2
- package/src/client/compat.ts +2 -1
- package/src/client/index.ts +22 -7
- package/src/client/localeClient.ts +97 -0
- package/src/client/officialSidebar.ts +4 -1
- package/src/client/state/recordsFeed.ts +241 -0
- package/src/client/ui/ConversationDiffDock.ts +10 -19
- package/src/client/ui/DiffBadge.ts +12 -19
- package/src/client/ui/EditorView.ts +38 -33
- package/src/client/ui/PerfSizeBadge.ts +7 -4
- package/src/client/ui/PluginDetailSection.ts +82 -0
- package/src/dap/discovery.ts +5 -3
- package/src/dap/provider.ts +26 -0
- package/src/dshVersion.ts +4 -2
- package/src/fileVersions.ts +2 -2
- package/src/lsp/providers.ts +9 -5
- package/src/lsp/rpc.ts +2 -2
- package/src/lsp/server.ts +26 -1
- package/src/mcpRuntime.ts +2 -1
- package/src/registry.ts +15 -2
- package/src/rpc.ts +2 -2
- package/src/rules.ts +9 -4
- package/src/search/fallback.ts +2 -2
- package/src/search/ripgrep.ts +2 -2
- package/src/store.ts +26 -1
- package/src/svn.ts +2 -2
|
@@ -19,6 +19,7 @@ import { useOpenReceipt } from '../useOpenReceipt.js'
|
|
|
19
19
|
import { acceptsLoad, receiptErrorFor, receiptPath, receiptReadyFor, receiptTabPath } from '../receiptState.js'
|
|
20
20
|
import { bindPanelCommand } from '../panelCommands.js'
|
|
21
21
|
import { dbg, isDebug, rpc } from '../rpc.js'
|
|
22
|
+
import { refreshFeed, subscribeErrors, subscribeRecords } from '../state/recordsFeed.js'
|
|
22
23
|
import { emitFileChanged, emitRefresh } from '../events.js'
|
|
23
24
|
import { langOf, loadMonaco, snippetLanguageOf } from '../monaco/loader.js'
|
|
24
25
|
import { bindEditActions, runEditAction } from '../monaco/editActions.js'
|
|
@@ -870,38 +871,43 @@ export function EditorView(props) {
|
|
|
870
871
|
return true
|
|
871
872
|
}
|
|
872
873
|
|
|
873
|
-
|
|
874
|
-
|
|
874
|
+
/**
|
|
875
|
+
* 把共享订阅拿到的记录落进组件状态(内容一致时保持引用不变,避免整树重渲染)。
|
|
876
|
+
* @private @author ddj 2026年10月19号
|
|
877
|
+
* @param list 差异记录数组
|
|
878
|
+
*/
|
|
879
|
+
const applyRecords = (list) => {
|
|
875
880
|
// #region debug log
|
|
876
881
|
const trace = isDebug()
|
|
877
|
-
const
|
|
882
|
+
const compareStart = trace ? performance.now() : 0
|
|
878
883
|
// #endregion
|
|
879
|
-
|
|
884
|
+
const map = {}
|
|
885
|
+
for (const r of list) {
|
|
886
|
+
if (r.archived === true) continue
|
|
887
|
+
map[r.callId] = r
|
|
888
|
+
}
|
|
889
|
+
setRecords((prev) => {
|
|
890
|
+
const equal = sameRecords(prev, map)
|
|
880
891
|
// #region debug log
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
if (r.archived === true) continue
|
|
887
|
-
map[r.callId] = r
|
|
892
|
+
if (trace) {
|
|
893
|
+
try {
|
|
894
|
+
const compareMs = performance.now() - compareStart
|
|
895
|
+
if (compareMs > 20) dbg(sessionId, `[DEBUG diffList] count=${list.length} compareMs=${compareMs.toFixed(1)} changed=${!equal}`)
|
|
896
|
+
} catch (e) { /* 诊断不可影响轮询 */ }
|
|
888
897
|
}
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
return equal ? prev : map
|
|
903
|
-
})
|
|
904
|
-
}).catch((e) => setError('list异常:' + String(e)))
|
|
898
|
+
// #endregion
|
|
899
|
+
return equal ? prev : map
|
|
900
|
+
})
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* 主动刷新一次差异记录(走会话级共享通道,同会话多消费方共享一次 RPC)。
|
|
905
|
+
* @private @author ddj 2026年10月19号
|
|
906
|
+
* @param skipStale 是否跳过过期判定
|
|
907
|
+
*/
|
|
908
|
+
const refreshRecords = (skipStale) => {
|
|
909
|
+
if (!sessionId) return
|
|
910
|
+
void refreshFeed(sessionId, skipStale === true)
|
|
905
911
|
}
|
|
906
912
|
|
|
907
913
|
/**
|
|
@@ -1185,14 +1191,13 @@ export function EditorView(props) {
|
|
|
1185
1191
|
})
|
|
1186
1192
|
}
|
|
1187
1193
|
|
|
1188
|
-
//
|
|
1194
|
+
// 挂载:接入会话级共享订阅(单定时器由 recordsFeed 持有,本组件不再自建 interval)
|
|
1189
1195
|
React.useEffect(() => {
|
|
1190
|
-
if (!sessionId) return
|
|
1196
|
+
if (!sessionId) return undefined
|
|
1197
|
+
const offRecords = subscribeRecords(sessionId, applyRecords)
|
|
1198
|
+
const offErrors = subscribeErrors(sessionId, (message) => setError('list异常:' + message))
|
|
1191
1199
|
refreshRecords()
|
|
1192
|
-
|
|
1193
|
-
const onRefresh = () => refreshRecords()
|
|
1194
|
-
window.addEventListener('edrv:refresh', onRefresh)
|
|
1195
|
-
return () => { clearInterval(t); window.removeEventListener('edrv:refresh', onRefresh) }
|
|
1200
|
+
return () => { offRecords(); offErrors() }
|
|
1196
1201
|
}, [sessionId])
|
|
1197
1202
|
|
|
1198
1203
|
/**
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — PerfSizeBadge:对话 header 会话体积指示器。
|
|
4
|
-
*
|
|
4
|
+
* 定时器复用 state/recordsFeed.ts 的会话级心跳(单会话只保留 1 个定时器),
|
|
5
|
+
* 轮询当前会话持久化体积(host 仅 stat,近零成本),≥1MB 显示、超阈值变色,
|
|
5
6
|
* 引导"一次任务一个短会话":会话过大时建议 /compact 或新开会话。
|
|
6
7
|
* 作者 ddj 2026-09-02
|
|
7
8
|
*/
|
|
8
9
|
import React from 'react'
|
|
9
10
|
import { rpc } from '../rpc.js'
|
|
11
|
+
import { subscribeTick } from '../state/recordsFeed.js'
|
|
10
12
|
|
|
11
13
|
/** 软阈值:≥ 此值琥珀色(默认 2MB)。 */
|
|
12
14
|
const SOFT = 2 * 1024 * 1024
|
|
@@ -38,10 +40,11 @@ export function PerfSizeBadge(props) {
|
|
|
38
40
|
}, [sessionId, sessions])
|
|
39
41
|
|
|
40
42
|
React.useEffect(() => {
|
|
43
|
+
if (!sessionId) return undefined
|
|
41
44
|
load()
|
|
42
|
-
|
|
43
|
-
return (
|
|
44
|
-
}, [load])
|
|
45
|
+
// 共享心跳:recordsFeed 每周期回调一次,避免本组件再自建 setInterval。
|
|
46
|
+
return subscribeTick(sessionId, load)
|
|
47
|
+
}, [load, sessionId])
|
|
45
48
|
|
|
46
49
|
if (!size || size < SHOW_MIN) return null
|
|
47
50
|
const label = size >= 1024 * 1024 ? (size / (1024 * 1024)).toFixed(1) + 'MB' : Math.round(size / 1024) + 'KB'
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — 官方插件管理器详情页的只读状态段。
|
|
4
|
+
* 挂在 DSH 0.2.1 新增的 `plugins.detail.section`(list / root)扩展位上,
|
|
5
|
+
* 只在「本插件自己」的详情页渲染,其余插件页返回 null。
|
|
6
|
+
* 作者 ddj 2026年10月19号
|
|
7
|
+
*/
|
|
8
|
+
import React from 'react'
|
|
9
|
+
import { rpc } from '../rpc.js'
|
|
10
|
+
|
|
11
|
+
/** 本插件标识(与 package.json name / bundle 行 id 一致)。 */
|
|
12
|
+
export const SELF_ID = 'dsh-vscode-mode'
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 判断详情页 subject 是否指向本插件。
|
|
16
|
+
* 官方有两种形状:官方内置项 `{kind:'item', id}`、安装包 `{kind:'bundle', pkg:{name}}`。
|
|
17
|
+
* @author ddj 2026年10月19号
|
|
18
|
+
* @param subject 详情页主题
|
|
19
|
+
* @returns 是否为本插件
|
|
20
|
+
*/
|
|
21
|
+
export function isSelfSubject(subject) {
|
|
22
|
+
if (!subject || typeof subject !== 'object') return false
|
|
23
|
+
if (subject.kind === 'item') return subject.id === SELF_ID
|
|
24
|
+
if (subject.kind === 'bundle') return subject.pkg?.name === SELF_ID
|
|
25
|
+
return false
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 把适配项列表压成「正常 / 未生效」计数文案。
|
|
30
|
+
* @author ddj 2026年10月19号
|
|
31
|
+
* @param list 适配项数组
|
|
32
|
+
* @returns 计数文案
|
|
33
|
+
*/
|
|
34
|
+
function countText(list) {
|
|
35
|
+
const items = Array.isArray(list) ? list : []
|
|
36
|
+
const ok = items.filter((item) => item?.active === true).length
|
|
37
|
+
return ok + ' / ' + items.length + ' 项生效'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 插件详情页的只读状态段。
|
|
42
|
+
* @author ddj 2026年10月19号
|
|
43
|
+
* @param props DSH 注入的 { subject }
|
|
44
|
+
* @returns 状态段元素或 null(非本插件页)
|
|
45
|
+
*/
|
|
46
|
+
export function PluginDetailSection(props) {
|
|
47
|
+
const subject = props?.subject
|
|
48
|
+
const isSelf = isSelfSubject(subject)
|
|
49
|
+
const [report, setReport] = React.useState(null)
|
|
50
|
+
|
|
51
|
+
React.useEffect(() => {
|
|
52
|
+
if (!isSelf) return undefined
|
|
53
|
+
let alive = true
|
|
54
|
+
rpc('compat', {}).then((result) => {
|
|
55
|
+
if (!alive || !result || !result.ok) return
|
|
56
|
+
setReport(result.report ?? null)
|
|
57
|
+
}).catch(() => { /* 状态段属锦上添花,取不到就不显示 */ })
|
|
58
|
+
return () => { alive = false }
|
|
59
|
+
}, [isSelf])
|
|
60
|
+
|
|
61
|
+
if (!isSelf) return null
|
|
62
|
+
|
|
63
|
+
const row = (name, note) => React.createElement('div', { key: name, className: 'vsm-compat-item ok' },
|
|
64
|
+
React.createElement('span', { className: 'vsm-compat-name' }, name),
|
|
65
|
+
React.createElement('span', { className: 'vsm-compat-note' }, note),
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
const versionText = report?.dshVersion ? 'DSH ' + report.dshVersion : '未探测到 DSH 版本'
|
|
69
|
+
const pluginText = report?.pluginVersion ? '插件 ' + report.pluginVersion : '插件版本未上报'
|
|
70
|
+
const warnCount = Array.isArray(report?.warnings) ? report.warnings.length : 0
|
|
71
|
+
|
|
72
|
+
return React.createElement('section', { className: 'vsm-panel', 'data-edrv-plugin-status': '1' },
|
|
73
|
+
React.createElement('h3', { className: 'vsm-panel-title' }, 'VSCodeMode 运行状态'),
|
|
74
|
+
React.createElement('div', { className: 'vsm-panel-body' },
|
|
75
|
+
row('版本', versionText + ' · ' + pluginText),
|
|
76
|
+
row('版本适配', countText(report?.adapters)),
|
|
77
|
+
row('外部插件与服务', countText(report?.external)),
|
|
78
|
+
row('护栏', countText(report?.guards)),
|
|
79
|
+
row('兼容性警告', warnCount ? warnCount + ' 条(设置页「兼容性」可查看详情)' : '未检测到'),
|
|
80
|
+
),
|
|
81
|
+
)
|
|
82
|
+
}
|
package/src/dap/discovery.ts
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* ~/.vscode-server/extensions、插件扩展目录),读各扩展 package.json 的
|
|
5
5
|
* contributes.debuggers[] 声明,解析为 DapAdapterDecl:
|
|
6
6
|
* - program 按平台取 windows/linux/osx 覆盖,相对扩展根解析;
|
|
7
|
-
* - runtime='node' → 由 node 跑 js
|
|
7
|
+
* - runtime='node' → 由 node 跑 js 入口;runtime='dotnet' → 由 dotnet 跑 dll 入口;
|
|
8
|
+
* 无 runtime → program 即可执行(args 为清单声明);
|
|
8
9
|
* - 同 type 多扩展并存 → 按「清单版本降序(numeric) → 路径升序」取唯一(与 LSP 候选口径一致);
|
|
9
10
|
* - 入口不存在 / runtime 不支持 → available=false + reason(如 DotRush 按需下载的 clrdbg,
|
|
10
11
|
* 诚实标注不假成功,调用方据此在下拉置灰并提示原因)。
|
|
@@ -128,12 +129,13 @@ function declOf(extDir: string, entry: DebuggerEntry): DapAdapterDecl | null {
|
|
|
128
129
|
const languages = stringsOf(entry.languages)
|
|
129
130
|
const resolved = resolveProgram(extDir, entry)
|
|
130
131
|
const runtime = typeof entry.runtime === 'string' && entry.runtime ? entry.runtime : undefined
|
|
131
|
-
|
|
132
|
+
// node = 扩展 js 入口;dotnet = dotnet 承载的 dll(如 DotRush monodbg),provider 层解析 dotnet 路径
|
|
133
|
+
const runtimeBad = runtime !== undefined && runtime !== 'node' && runtime !== 'dotnet'
|
|
132
134
|
const available = resolved.available && !runtimeBad
|
|
133
135
|
const reason = !resolved.available
|
|
134
136
|
? resolved.reason
|
|
135
137
|
: runtimeBad
|
|
136
|
-
? '暂不支持运行时 "' + runtime + '"(仅支持 node 与原生可执行)'
|
|
138
|
+
? '暂不支持运行时 "' + runtime + '"(仅支持 node、dotnet 与原生可执行)'
|
|
137
139
|
: undefined
|
|
138
140
|
return {
|
|
139
141
|
type,
|
package/src/dap/provider.ts
CHANGED
|
@@ -2,12 +2,17 @@
|
|
|
2
2
|
* dsh-vscode-mode host — 调试适配器规格解析:按调试类型从扩展清单声明(discovery)
|
|
3
3
|
* 解析 spawn 规格,通用不限定语言:
|
|
4
4
|
* - runtime='node' → node 跑 js 入口(emmylua 形态,等价旧 process.execPath 硬编码);
|
|
5
|
+
* - runtime='dotnet' → dotnet 跑 dll 入口(DotRush monodbg 形态;按 dll runtimeconfig
|
|
6
|
+
* 要求的运行时大版本找满足的 dotnet,对齐 LSP 侧 dotrushLaunch 口径);
|
|
5
7
|
* - 无 runtime → program 即原生可执行(clrdbg/monodbg 形态,args 为清单声明如 --interpreter=vscode);
|
|
6
8
|
* - 其它 runtime 明确不支持(decl.available=false 带原因,不做假启动)。
|
|
7
9
|
* 旧版仅探测 tangzx.emmylua 两型(attach/new),由清单驱动后自然覆盖并扩展到任意适配器。
|
|
8
10
|
* 作者 ddj 2026年09月29号 / 2026年09月21号
|
|
9
11
|
*/
|
|
10
12
|
import { debuggerDecls } from './discovery.js'
|
|
13
|
+
import { dirname, join } from 'node:path'
|
|
14
|
+
import { existsSync } from 'node:fs'
|
|
15
|
+
import { dotnetWithRuntime, findDotnet, runtimeMajorOf } from '../lsp/providers.js'
|
|
11
16
|
|
|
12
17
|
/** 适配器启动规格(spawn 参数)。 */
|
|
13
18
|
export interface DapAdapterSpec {
|
|
@@ -42,8 +47,29 @@ export function resolveAdapterSpec(type: string, force = false, home?: string):
|
|
|
42
47
|
if (decl.runtime === 'node') {
|
|
43
48
|
return { ...base, command: process.execPath, args: [decl.program, ...decl.args] }
|
|
44
49
|
}
|
|
50
|
+
if (decl.runtime === 'dotnet') {
|
|
51
|
+
const dotnet = dotnetLaunch(decl.extensionPath, decl.program)
|
|
52
|
+
if (!dotnet) return null
|
|
53
|
+
return { ...base, command: dotnet, args: [decl.program, ...decl.args] }
|
|
54
|
+
}
|
|
45
55
|
if (!decl.runtime) {
|
|
46
56
|
return { ...base, command: decl.program, args: [...decl.args] }
|
|
47
57
|
}
|
|
48
58
|
return null
|
|
49
59
|
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 为 dotnet 型适配器找启动用 dotnet:优先扩展内置 SDK,其次满足 dll runtimeconfig
|
|
63
|
+
* 要求的运行时大版本,兜底任意 dotnet(对齐 LSP 侧 dotrushLaunch 口径)。
|
|
64
|
+
* @author ddj 2026年10月19号
|
|
65
|
+
* @param extensionPath 扩展根目录
|
|
66
|
+
* @param program 适配器 dll 绝对路径
|
|
67
|
+
* @returns dotnet 可执行路径;找不到满足条件的返回 null
|
|
68
|
+
*/
|
|
69
|
+
function dotnetLaunch(extensionPath: string, program: string): string | null {
|
|
70
|
+
const sdkDotnet = join(extensionPath, 'extension', 'Sdk', 'dotnet' + (process.platform === 'win32' ? '.exe' : ''))
|
|
71
|
+
if (existsSync(sdkDotnet)) return sdkDotnet
|
|
72
|
+
const major = runtimeMajorOf(dirname(program))
|
|
73
|
+
if (major !== null) return dotnetWithRuntime(major) ?? findDotnet()
|
|
74
|
+
return findDotnet()
|
|
75
|
+
}
|
package/src/dshVersion.ts
CHANGED
|
@@ -84,14 +84,16 @@ export function inDshRange(version: DshVersion | null | undefined, range: DshRan
|
|
|
84
84
|
|
|
85
85
|
/**
|
|
86
86
|
* 版本线标签:报告与文档展示版本归属;不可解析返回 '未知'。
|
|
87
|
-
* 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2 精确标记。
|
|
88
|
-
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号
|
|
87
|
+
* 逐线自新到旧匹配,先命中先返回;仅已静态审计的 0.2.0-rc.1/rc.2、0.2.1-rc.2 精确标记。
|
|
88
|
+
* @author ddj 2026年09月02号 / 2026年09月18号 / 2026年09月22号 / 2026年09月29号 / 2026年10月08号 / 2026年10月19号
|
|
89
89
|
* @param input 版本串(如 '0.1.6-alpha.2')
|
|
90
90
|
* @returns 版本线标签
|
|
91
91
|
*/
|
|
92
92
|
export function familyLabel(input: string): string {
|
|
93
93
|
const version = parseDshVersion(input)
|
|
94
94
|
if (!version) return '未知'
|
|
95
|
+
if (inDshRange(version, { from: '0.2.1-rc.2', to: '0.2.1-rc.2' })) return '0.2.1-rc.2(设置=profile Config;workingDirectory 投影跟随 cd;plugins.detail 扩展位;按能力探测)'
|
|
96
|
+
if (inDshRange(version, { from: '0.2.1-0', to: '0.2.1' })) return '0.2.1 线(rc.2 已逐版适配,其余按能力探测)'
|
|
95
97
|
if (inDshRange(version, { from: '0.2.0-rc.2', to: '0.2.0-rc.2' })) return '0.2.0-rc.2(设置=profile Config;按能力探测)'
|
|
96
98
|
if (inDshRange(version, { from: '0.2.0-rc.1', to: '0.2.0-rc.1' })) return '0.2.0-rc.1(设置=profile Config;按能力探测)'
|
|
97
99
|
if (inDshRange(version, { from: '0.2.0-0' })) return '0.2.0 及更新(除 rc.1/rc.2 外未经逐版适配;按能力探测)'
|
package/src/fileVersions.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { FileVersionItem } from './shared/rpc.js'
|
|
22
22
|
import type { Ctx } from './store.js'
|
|
23
|
-
import {
|
|
23
|
+
import { cwdOfSession, sessionOf } from './registry.js'
|
|
24
24
|
import { invalidateIndex } from './treeIndex.js'
|
|
25
25
|
import { log } from './log.js'
|
|
26
26
|
|
|
@@ -150,7 +150,7 @@ export function createFileVersions(ctx: Ctx): FileVersions {
|
|
|
150
150
|
.slice(0, MAX_PATHS)
|
|
151
151
|
if (!fs || !paths.length) return { ok: true, items: [] }
|
|
152
152
|
const session = sessionOf(ctx, args.sessionId)
|
|
153
|
-
const cwd =
|
|
153
|
+
const cwd = cwdOfSession(ctx, session)
|
|
154
154
|
const items: FileVersionItem[] = []
|
|
155
155
|
for (const path of paths) items.push(await observe(fs, cwd, path))
|
|
156
156
|
return { ok: true, items }
|
package/src/lsp/providers.ts
CHANGED
|
@@ -398,8 +398,8 @@ export function resolveLuaProvider(manual?: { command?: string; path?: string },
|
|
|
398
398
|
}
|
|
399
399
|
|
|
400
400
|
/**
|
|
401
|
-
* 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;
|
|
402
|
-
*
|
|
401
|
+
* 解析 C# provider(优先级:手动 > 扩展源 > 自动发现;DotRush 优先——自带 sln 加载与
|
|
402
|
+
* 反编译跳转;Roslyn dll 需 dotnet,但无 C# Dev Kit 时不加载项目;或 OmniSharp)。
|
|
403
403
|
* @author ddj 2026年08月27号
|
|
404
404
|
* @param manual 手动配置
|
|
405
405
|
* @param home DSH home(缺省真实;测试可注入临时目录)
|
|
@@ -416,9 +416,10 @@ export function resolveCSharpProvider(manual?: { command?: string; path?: string
|
|
|
416
416
|
// SDK 环境注入:runtime-only 的 dotnet 会导致 MSBuild 求值/restore 退化
|
|
417
417
|
const sdkRoot = findSdkRoot()
|
|
418
418
|
const msbuildEnv = sdkRoot ? sdkEnvOf(sdkRoot) : undefined
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
419
|
+
// DotRush 优先:其服务器从 cwd 自动发现 sln 并自行加载(restore/compile/索引),
|
|
420
|
+
// 元数据符号可跳转反编译源码(实测 GameObject.Find → _decompiled_/UnityEngine.GameObject.cs)。
|
|
421
|
+
// Roslyn(ms-dotnettools.csharp)依赖 C# Dev Kit 的 serviceBroker 加载 sln;
|
|
422
|
+
// 无 Dev Kit 时即使发 solution/open 也不加载项目,references 不跨文件、元数据 definition 全空(实测 2026-10-19)。
|
|
422
423
|
let dotrushReason: string | null = null
|
|
423
424
|
let provisionMajor: number | undefined
|
|
424
425
|
if (discovered.dotrushDll) {
|
|
@@ -430,6 +431,9 @@ export function resolveCSharpProvider(manual?: { command?: string; path?: string
|
|
|
430
431
|
dotrushReason = launch.reason
|
|
431
432
|
provisionMajor = major ?? undefined
|
|
432
433
|
}
|
|
434
|
+
if (discovered.roslynDll && discovered.dotnet) {
|
|
435
|
+
return { ...base, kind: 'discover', argv: [discovered.dotnet, discovered.roslynDll, '--stdio'], env: msbuildEnv, ready: true }
|
|
436
|
+
}
|
|
433
437
|
if (discovered.omnisharpExe) {
|
|
434
438
|
return { ...base, kind: 'discover', argv: [discovered.omnisharpExe, '--stdio'], ready: true }
|
|
435
439
|
}
|
package/src/lsp/rpc.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import type { Ctx } from '../store.js'
|
|
9
9
|
import { searchRoot } from '../search/ripgrep.js'
|
|
10
|
-
import {
|
|
10
|
+
import { cwdOfSession, sessionOf } from '../registry.js'
|
|
11
11
|
import { resolveProviderSpec, langOfPath, configFromPlugin, mergeConfig, sanitizeLang, LSP_LANGUAGES, type LspConfig, type LspLangConfig } from './config.js'
|
|
12
12
|
import { clearProviderCache, candidatesFor } from './providers.js'
|
|
13
13
|
import { capCompletions } from './completionCap.js'
|
|
@@ -154,7 +154,7 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
154
154
|
const rootOf = async (sessionId: string | undefined): Promise<{ root: string; sessionId: string } | { err: string }> => {
|
|
155
155
|
const session = sessionOf(ctx, sessionId)
|
|
156
156
|
if (!session) return { err: SESSION_MISSING_ERROR }
|
|
157
|
-
const cwd =
|
|
157
|
+
const cwd = cwdOfSession(ctx, session)
|
|
158
158
|
if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
|
|
159
159
|
try {
|
|
160
160
|
const root = await searchRoot(ctx, session)
|
package/src/lsp/server.ts
CHANGED
|
@@ -8,7 +8,8 @@ import { spawnServer, type Transport } from './transport.js'
|
|
|
8
8
|
import { createLspClient, type LspClient } from './client.js'
|
|
9
9
|
import { pathToFileUri, toWorkspacePath, isAbsolutePath, fileUriToPath } from './uri.js'
|
|
10
10
|
import { deriveDefinitionFromLocations, deriveCrossFileDeclaration, isMisresolvedSameFileUsage } from './derive.js'
|
|
11
|
-
import { readFile } from 'node:fs/promises'
|
|
11
|
+
import { readFile, readdir } from 'node:fs/promises'
|
|
12
|
+
import { join } from 'node:path'
|
|
12
13
|
import type { LspProviderSpec } from './providers.js'
|
|
13
14
|
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../shared/lsp.js'
|
|
14
15
|
import type {
|
|
@@ -61,6 +62,26 @@ interface OpenDoc {
|
|
|
61
62
|
const RESTARTABLE = true
|
|
62
63
|
const MAX_DOC_VERSION = 2147483647
|
|
63
64
|
|
|
65
|
+
/**
|
|
66
|
+
* 找工作区根目录下的第一个 .sln(字典序)。
|
|
67
|
+
*
|
|
68
|
+
* 背景:Roslyn LSP 启动后若收不到 solution/project 通知,会停留在「杂项文件」模式——
|
|
69
|
+
* 工作区内自有符号可解析,但 Unity 引擎程序集 / NuGet 包等元数据符号的
|
|
70
|
+
* definition/hover 一律返回空(实测 2026-10-16,GameObject.Find 全空)。
|
|
71
|
+
* 需在握手后发 Roslyn 专有 `solution/open` 通知加载项目上下文。
|
|
72
|
+
* @author ddj 2026年10月19号
|
|
73
|
+
* @param root 工作区根(绝对路径)
|
|
74
|
+
* @returns sln 绝对路径;根目录无 sln 返回 null
|
|
75
|
+
*/
|
|
76
|
+
async function findSolutionFile(root: string): Promise<string | null> {
|
|
77
|
+
try {
|
|
78
|
+
const names = (await readdir(root)).filter((name) => name.toLowerCase().endsWith('.sln')).sort()
|
|
79
|
+
return names[0] ? join(root, names[0]) : null
|
|
80
|
+
} catch {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
64
85
|
/**
|
|
65
86
|
* 使用合法、递增的 Int32 文档版本;旧客户端的毫秒时间戳不能直发给 Roslyn。
|
|
66
87
|
* @author ddj 2026年09月24号
|
|
@@ -320,6 +341,10 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
320
341
|
// 对不认识的配置段按协议忽略,均安全
|
|
321
342
|
if (languageId === 'csharp') {
|
|
322
343
|
client.notify('workspace/didChangeConfiguration', { settings: { dotrush: { roslyn: {} } } })
|
|
344
|
+
// Roslyn 专有:加载解决方案后元数据符号(引擎/NuGet 程序集)才可跳转;
|
|
345
|
+
// 未知通知对其它 C# 服务器按协议安全忽略。无 sln 的纯脚本项目静默跳过。
|
|
346
|
+
const slnPath = await findSolutionFile(root)
|
|
347
|
+
if (slnPath) client.notify('solution/open', { solution: pathToFileUri(slnPath) })
|
|
323
348
|
}
|
|
324
349
|
capabilities = caps
|
|
325
350
|
return true
|
package/src/mcpRuntime.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { matchWorkspace } from './mcpIsolation.js'
|
|
|
6
6
|
import { listProjects, projectEntryId, reconcileProject } from './mcpProject.js'
|
|
7
7
|
import type { MpcConfig, MpcServer } from './shared/mcp.js'
|
|
8
8
|
import type { Ctx } from './store.js'
|
|
9
|
+
import { cwdOfSession } from './store.js'
|
|
9
10
|
|
|
10
11
|
export type ProjectDef = { config: MpcConfig; enabled: boolean }
|
|
11
12
|
type Instance = { fiber?: any; key: string; path: string; error?: string; stopping?: Promise<void> }
|
|
@@ -76,7 +77,7 @@ export class McpRuntime {
|
|
|
76
77
|
*/
|
|
77
78
|
workspace(agent: any): string | undefined {
|
|
78
79
|
const paths = (this.ctx.get('workspaceRegistry')?.list?.() ?? []).map((ws: { path: string }) => ws.path)
|
|
79
|
-
return matchWorkspace(agent?.session
|
|
80
|
+
return matchWorkspace(cwdOfSession(this.ctx, agent?.session) ?? undefined, paths)
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
/**
|
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import type { DiffRecord } from './shared/types.js'
|
|
7
7
|
import type { Ctx, Session } from './store.js'
|
|
8
|
-
import { loadBucket } from './store.js'
|
|
8
|
+
import { cwdOfSession, loadBucket } from './store.js'
|
|
9
9
|
|
|
10
10
|
/** cwd → 记录桶(内存缓存;随会话销毁清理)。 */
|
|
11
11
|
export type Registry = Map<string, Map<string, DiffRecord>>
|
|
@@ -19,7 +19,20 @@ export function sessionOf(ctx: Ctx, sessionId?: string): Session | undefined {
|
|
|
19
19
|
return live.length === 1 ? live[0] : undefined
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* 会话工作区 cwd(跟随会话内 cd;服务缺失时回落 header.cwd)。
|
|
24
|
+
* 定义在 store.ts 以避免 registry ↔ store 循环依赖,此处仅转出为会话助手入口。
|
|
25
|
+
* @author ddj 2026年10月19号
|
|
26
|
+
*/
|
|
27
|
+
export { cwdOfSession }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 会话头部的原始 cwd(不跟随 cd)。仅用于会话销毁等无上下文场景:
|
|
31
|
+
* 此时投影可能已随会话卸载,读 header 才能保证拿到建会话时的目录。
|
|
32
|
+
* @author ddj 2026年10月19号
|
|
33
|
+
* @param session 目标会话
|
|
34
|
+
* @returns 绝对路径或 null
|
|
35
|
+
*/
|
|
23
36
|
export function cwdOf(session: Session | undefined): string | null {
|
|
24
37
|
return session?.header?.cwd ?? null
|
|
25
38
|
}
|
package/src/rpc.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { archiveEntryFor, markDecision, recordResolved, reconstructOriginal } fr
|
|
|
25
25
|
import { clearRevertState, listArchives, pageArchives, readArchives, revertState, setRevertState, startReverts } from './archiveDb.js'
|
|
26
26
|
import { log } from './log.js'
|
|
27
27
|
import type { Registry } from './registry.js'
|
|
28
|
-
import { bucketOf,
|
|
28
|
+
import { bucketOf, cwdOfSession, sessionOf } from './registry.js'
|
|
29
29
|
import type { SearchOrchestrator } from './search/orchestrator.js'
|
|
30
30
|
import { newSearcher } from './search/orchestrator.js'
|
|
31
31
|
import type { ContentSearcher } from './search/content.js'
|
|
@@ -100,7 +100,7 @@ async function requireSession(ctx: Ctx, sessionId: string | undefined): Promise<
|
|
|
100
100
|
> {
|
|
101
101
|
const session = sessionOf(ctx, sessionId)
|
|
102
102
|
if (!session) return { err: SESSION_MISSING_ERROR }
|
|
103
|
-
const cwd =
|
|
103
|
+
const cwd = cwdOfSession(ctx, session)
|
|
104
104
|
if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
|
|
105
105
|
return { session, cwd }
|
|
106
106
|
}
|
package/src/rules.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { join, resolve } from 'node:path'
|
|
|
16
16
|
import { dshHome } from './paths.js'
|
|
17
17
|
import type { RuleInfo, RuleProject, RuleRefInput, RuleScope, RuleSaveInput, UserRuleOrigin } from './shared/rules.js'
|
|
18
18
|
import type { Ctx } from './store.js'
|
|
19
|
+
import { cwdOfSession } from './store.js'
|
|
19
20
|
|
|
20
21
|
// --region 常量与目录定位
|
|
21
22
|
/** 规则文件名白名单:字母数字开头,仅字母数字点横下划线,.mdc 后缀(天然拒绝路径分隔符)。 */
|
|
@@ -550,13 +551,17 @@ export function readUserRulesSync(dirs = userRuleDirs()): LoadedRule[] {
|
|
|
550
551
|
return mergeUserRules(current, legacy, (rule) => rule.info.file)
|
|
551
552
|
}
|
|
552
553
|
|
|
553
|
-
/**
|
|
554
|
+
/**
|
|
555
|
+
* 从装配上下文解析会话工作区 cwd(agent.id → sessions → 当前目录;缺链返回 null)。
|
|
556
|
+
* @author ddj 2026年10月19号
|
|
557
|
+
* @param ctx DSH 上下文
|
|
558
|
+
* @param asm 装配上下文(含 agent.id)
|
|
559
|
+
* @returns 项目规则根的绝对路径或 null
|
|
560
|
+
*/
|
|
554
561
|
function cwdFromAssemble(ctx: Ctx, asm: { agent?: { id?: unknown } } | undefined): string | null {
|
|
555
562
|
const id = asm?.agent?.id
|
|
556
563
|
if (typeof id !== 'string' || !id) return null
|
|
557
|
-
|
|
558
|
-
const cwd = session?.header?.cwd
|
|
559
|
-
return typeof cwd === 'string' && cwd ? cwd : null
|
|
564
|
+
return cwdOfSession(ctx, ctx.get('sessions')?.get?.(id))
|
|
560
565
|
}
|
|
561
566
|
|
|
562
567
|
/**
|
package/src/search/fallback.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 作者 ddj 2026年08月24号
|
|
5
5
|
*/
|
|
6
6
|
import type { Ctx, Session } from '../store.js'
|
|
7
|
-
import { policyOf } from '../store.js'
|
|
7
|
+
import { cwdOfSession, policyOf } from '../store.js'
|
|
8
8
|
import { pathText, pathMatch, prepareQuery } from './query.js'
|
|
9
9
|
import type { PreparedQuery, WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResult } from './types.js'
|
|
10
10
|
|
|
@@ -61,7 +61,7 @@ export async function searchFallback(input: WorkspaceSearchInput): Promise<Works
|
|
|
61
61
|
const fs = input.ctx.get('fs') as SearchFs | undefined
|
|
62
62
|
if (!fs) throw new Error('缺少 fs')
|
|
63
63
|
const policy = policyOf(input.ctx, input.session)
|
|
64
|
-
const cwd = input.session
|
|
64
|
+
const cwd = cwdOfSession(input.ctx, input.session)
|
|
65
65
|
const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', cwd ? { cwd, signal: input.signal } : { signal: input.signal })
|
|
66
66
|
const signal = input.signal ?? new AbortController().signal
|
|
67
67
|
const files: string[] = []
|
package/src/search/ripgrep.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs'
|
|
|
7
7
|
import { join } from 'node:path'
|
|
8
8
|
import { createRequire } from 'node:module'
|
|
9
9
|
import type { Ctx, Session } from '../store.js'
|
|
10
|
-
import { policyOf } from '../store.js'
|
|
10
|
+
import { cwdOfSession, policyOf } from '../store.js'
|
|
11
11
|
import { prepareQuery, pathText } from './query.js'
|
|
12
12
|
import type { WorkspaceSearchInput, WorkspaceSearchProvider, WorkspaceSearchResult } from './types.js'
|
|
13
13
|
|
|
@@ -127,7 +127,7 @@ export async function searchRoot(ctx: Ctx, session: Session): Promise<string> {
|
|
|
127
127
|
const fs = ctx.get('fs')
|
|
128
128
|
if (!fs) throw new Error('缺少 fs')
|
|
129
129
|
const policy = policyOf(ctx, session)
|
|
130
|
-
const cwd = session
|
|
130
|
+
const cwd = cwdOfSession(ctx, session)
|
|
131
131
|
const rootTarget = await fs.resolve(policy?.workspaceRoot ?? '.', cwd ? { cwd } : {})
|
|
132
132
|
return fs.processPath(rootTarget)
|
|
133
133
|
}
|
package/src/store.ts
CHANGED
|
@@ -33,10 +33,35 @@ export function policyOf(ctx: Ctx, session: Session | undefined): any {
|
|
|
33
33
|
return session ? svc.resolve({ session }) : svc.resolve()
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 读取会话当前工作目录:优先 workingDirectory 投影(DSH 0.2.1 起支持会话内 cd),
|
|
38
|
+
* 服务缺失或投影未就绪时回落到 header.cwd(会话创建时的原始目录)。
|
|
39
|
+
* 服务一律可选探测:不得写入模块 inject,否则旧版 DSH 会停等一个不存在的服务。
|
|
40
|
+
* @author ddj 2026年10月19号
|
|
41
|
+
* @param ctx DSH 上下文(用于可选探测 workingDirectory 服务)
|
|
42
|
+
* @param session 目标会话
|
|
43
|
+
* @returns 绝对路径;无法确定时 null
|
|
44
|
+
*/
|
|
45
|
+
export function cwdOfSession(ctx: Ctx, session: Session | undefined): string | null {
|
|
46
|
+
const headerCwd = session?.header?.cwd
|
|
47
|
+
const fallback = typeof headerCwd === 'string' && headerCwd ? headerCwd : null
|
|
48
|
+
if (!session) return null
|
|
49
|
+
|
|
50
|
+
const service = ctx?.get?.('workingDirectory')
|
|
51
|
+
if (!service || typeof service.get !== 'function') return fallback
|
|
52
|
+
try {
|
|
53
|
+
const current = service.get(session)
|
|
54
|
+
return typeof current === 'string' && current ? current : fallback
|
|
55
|
+
} catch {
|
|
56
|
+
// 会话已销毁或投影缺失:回落原始目录,不让新能力变成新的故障点
|
|
57
|
+
return fallback
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
36
61
|
/** 按会话 cwd 解析目标路径。 */
|
|
37
62
|
export async function resolveTarget(ctx: Ctx, session: Session | undefined, path: string): Promise<string> {
|
|
38
63
|
const fs = ctx.get('fs')
|
|
39
|
-
const cwd = session
|
|
64
|
+
const cwd = cwdOfSession(ctx, session)
|
|
40
65
|
return fs.resolve(path, cwd ? { cwd } : {})
|
|
41
66
|
}
|
|
42
67
|
|
package/src/svn.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { readdir, stat } from 'node:fs/promises'
|
|
|
10
10
|
import { basename, dirname, isAbsolute, join, relative, resolve } from 'node:path'
|
|
11
11
|
import { tmpdir } from 'node:os'
|
|
12
12
|
import type { Ctx, Session } from './store.js'
|
|
13
|
-
import {
|
|
13
|
+
import { cwdOfSession, sessionOf } from './registry.js'
|
|
14
14
|
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnFeature, SvnIgnoreItem, SvnItemStatus, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry } from './shared/svn.js'
|
|
15
15
|
import { SESSION_MISSING_ERROR, SESSION_NO_WORKSPACE_ERROR } from './shared/rpc.js'
|
|
16
16
|
import { AI_PLAN_DIFF_CAP, AI_PLAN_PATHS_CAP, BATCH_PATHS_CAP, FILE_SIZES_CAP, SVN_CLI_DEFAULT, TORTOISE_EXE, contentGuardOf, normalizeAiPlan, svnChangelistNameErrorOf, tortoiseLaunchArgv } from './shared/svn.js'
|
|
@@ -463,7 +463,7 @@ async function requireSvnSession(
|
|
|
463
463
|
): Promise<{ cwd: string } | { err: string }> {
|
|
464
464
|
const session: Session | undefined = sessionOf(ctx, sessionId)
|
|
465
465
|
if (!session) return { err: SESSION_MISSING_ERROR }
|
|
466
|
-
const cwd =
|
|
466
|
+
const cwd = cwdOfSession(ctx, session)
|
|
467
467
|
if (!cwd) return { err: SESSION_NO_WORKSPACE_ERROR }
|
|
468
468
|
return { cwd }
|
|
469
469
|
}
|