dsh-vscode-mode 0.15.8 → 0.16.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 +6 -4
- package/lib/client.js +312 -69
- package/lib/client.js.map +1 -1
- package/lib/index.js +262 -17
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/index.ts +4 -0
- package/src/client/state/editorFindState.ts +15 -10
- package/src/client/state/editorViewState.ts +7 -4
- package/src/client/styles/editor.css +3 -0
- package/src/client/ui/EditorPane.ts +23 -3
- package/src/client/ui/EditorView.ts +117 -15
- package/src/client/ui/McpSettings.ts +36 -0
- package/src/client/updateNotice.ts +64 -0
- package/src/dap/discovery.ts +5 -3
- package/src/dap/provider.ts +26 -0
- package/src/lsp/providers.ts +9 -5
- package/src/lsp/server.ts +26 -1
- package/src/rpc.ts +9 -0
- package/src/selfUpdate.ts +188 -0
- package/src/shared/rpc.ts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.0",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车,界面状态(页签/光标/侧栏/编辑 Tab/导航历史)镜像到 DSH home 跨重启恢复",
|
|
5
5
|
"icon": "assets/icon.svg",
|
|
6
6
|
"keywords": [
|
package/src/client/index.ts
CHANGED
|
@@ -67,6 +67,7 @@ import { disposeAiInline } from './ai/inlineProvider.js'
|
|
|
67
67
|
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
68
68
|
import { installPlanBridge } from './planBridge.js'
|
|
69
69
|
import { installPageReload } from './pageReload.js'
|
|
70
|
+
import { installUpdateNotice } from './updateNotice.js'
|
|
70
71
|
import { flushPageState, flushUiState, hydrateUiState, installUiStatePersist, onUiStateReady, reportUiStateEvent, uiStateSettled } from './state/uiStatePersist.js'
|
|
71
72
|
import { workspaceScopeOf } from './state/scopeStore.js'
|
|
72
73
|
import { dapStore } from './dap/store.js'
|
|
@@ -195,6 +196,9 @@ export async function apply(ctx: any): Promise<void> {
|
|
|
195
196
|
}
|
|
196
197
|
ctx.effect(() => installPageReload(flushPageState, notifyReload), 'vscode-mode: guarded page reload')
|
|
197
198
|
|
|
199
|
+
// 可用更新主动提醒:启动延迟 + 抖动后检查 registry,有新版本且未提醒过时 toast(失败静默)
|
|
200
|
+
installUpdateNotice(ctx, schedule)
|
|
201
|
+
|
|
198
202
|
const registry: FileOpenerRegistry = createFileOpenerRegistry()
|
|
199
203
|
const workspaces = ctx.get('workspaces')
|
|
200
204
|
const sessions = ctx.get('sessions')
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Workspace-local Ctrl+F state bridge; ddj 2026年10月09号.
|
|
3
|
+
*
|
|
4
|
+
* 快照键语义(ddj 2026年10月16号):键由调用方传入(findKey),不再限定为工作区 scope——
|
|
5
|
+
* 编辑器按分组隔离搜索框时传「scope + 分组 id」组合键;旧调用传 scope 时行为完全不变。
|
|
6
|
+
*/
|
|
2
7
|
export interface FindSnapshot {
|
|
3
8
|
searchString: string
|
|
4
9
|
isRevealed: boolean
|
|
@@ -58,17 +63,17 @@ function applyFind(controller: FindController, saved: FindSnapshot): Promise<unk
|
|
|
58
63
|
/** @public @author ddj 2026年10月09号 @param editor Monaco editor. @returns Model-switch bridge; dispose releases the native listener. */
|
|
59
64
|
export function createFindBridge(editor: FindEditor) {
|
|
60
65
|
const controller = controllerOf(editor)
|
|
61
|
-
let
|
|
66
|
+
let findKey: string | null = null
|
|
62
67
|
let paused = true
|
|
63
68
|
let disposed = false
|
|
64
69
|
|
|
65
|
-
/** @private @author ddj 2026年10月
|
|
70
|
+
/** @private @author ddj 2026年10月16号 Capture only user changes into the current findKey bucket, not model-reset or restoration events. */
|
|
66
71
|
function capture(): void {
|
|
67
|
-
if (disposed || paused || !
|
|
72
|
+
if (disposed || paused || !findKey || !controller) return
|
|
68
73
|
try {
|
|
69
74
|
const saved = snapshotOf(controller.getState())
|
|
70
|
-
snapshots.delete(
|
|
71
|
-
snapshots.set(
|
|
75
|
+
snapshots.delete(findKey)
|
|
76
|
+
snapshots.set(findKey, saved)
|
|
72
77
|
if (snapshots.size > 20) snapshots.delete(snapshots.keys().next().value!)
|
|
73
78
|
} catch { /* A disposed contribution must not interrupt view capture. */ }
|
|
74
79
|
}
|
|
@@ -77,11 +82,11 @@ export function createFindBridge(editor: FindEditor) {
|
|
|
77
82
|
/** @public @author ddj 2026年10月09号 Save before setModel; suppress Monaco's per-model option resets. */
|
|
78
83
|
function pause(): void { capture(); paused = true }
|
|
79
84
|
|
|
80
|
-
/** @public @author ddj 2026年10月
|
|
81
|
-
function resume(
|
|
85
|
+
/** @public @author ddj 2026年10月16号 @param nextKey Bucket owning the bound view(scope 或 scope+分组 id 组合键). Restore with moveCursor=false; no per-file range or match index. */
|
|
86
|
+
function resume(nextKey: string): void {
|
|
82
87
|
if (disposed || !controller) return
|
|
83
|
-
|
|
84
|
-
const saved = snapshots.get(
|
|
88
|
+
findKey = nextKey
|
|
89
|
+
const saved = snapshots.get(findKey) ?? {
|
|
85
90
|
searchString: '', isRevealed: false, matchCase: false, wholeWord: false, isRegex: false,
|
|
86
91
|
}
|
|
87
92
|
try {
|
|
@@ -14,6 +14,8 @@ export interface ViewEditor extends FindEditor {
|
|
|
14
14
|
export function createViewKeeper(editor: ViewEditor) {
|
|
15
15
|
const find = createFindBridge(editor)
|
|
16
16
|
let bound: { scope: string; path: string; model: unknown } | null = null
|
|
17
|
+
// Ctrl+F 快照键跟随最近一次 bind(默认回退 scope);编辑器跨分组复用时按组存取搜索框状态
|
|
18
|
+
let lastFindKey: string | null = null
|
|
17
19
|
let disposed = false
|
|
18
20
|
|
|
19
21
|
/** @public @author ddj 2026年10月09号 @param path Optional expected path; mismatches never write a snapshot under the wrong file. Persist viewport even if cursor never moved. */
|
|
@@ -27,20 +29,21 @@ export function createViewKeeper(editor: ViewEditor) {
|
|
|
27
29
|
} catch { /* A missing/disposed model must not overwrite a valid snapshot. */ }
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
/** @public @author ddj 2026年10月
|
|
31
|
-
function bind(scope: string, path: string, model: unknown): void {
|
|
32
|
+
/** @public @author ddj 2026年10月16号 @param scope Model workspace. @param path Cache path. @param model Ready text model. @param findKey Optional Ctrl+F snapshot bucket(scope+分组 id);缺省沿用 scope. Restore only on identity change, before explicit navigation effects. */
|
|
33
|
+
function bind(scope: string, path: string, model: unknown, findKey?: string): void {
|
|
32
34
|
if (disposed || !path || !model) return
|
|
33
35
|
if (bound?.scope === scope && bound.path === path && bound.model === model
|
|
34
|
-
&& editor.getModel() === model) return
|
|
36
|
+
&& editor.getModel() === model && (findKey === undefined || findKey === lastFindKey)) return
|
|
35
37
|
save()
|
|
36
38
|
find.pause()
|
|
37
39
|
if (editor.getModel() !== model) editor.setModel(model)
|
|
38
40
|
bound = { scope, path, model }
|
|
41
|
+
lastFindKey = findKey ?? scope
|
|
39
42
|
const state = viewStatesLoad(scope)[path]
|
|
40
43
|
if (state) {
|
|
41
44
|
try { editor.restoreViewState(state) } catch { /* Stale fold ranges may be invalid. */ }
|
|
42
45
|
}
|
|
43
|
-
find.resume(
|
|
46
|
+
find.resume(lastFindKey)
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
/** @public @author ddj 2026年10月09号 Save before dispose/ref clearing; repeated cleanup is a no-op. */
|
|
@@ -23,6 +23,9 @@
|
|
|
23
23
|
[data-edrv-view] .edrv-pathbar .edrv-pb-full { flex: 1; min-width: 0; color: var(--dsw-alias-label-tertiary, #9aa5b1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; direction: rtl; text-align: left; }
|
|
24
24
|
[data-edrv-view] .edrv-pathbar .edrv-pb-meta { flex-shrink: 0; font-size: 11px; color: var(--dsw-alias-label-tertiary, #9aa5b1); font-variant-numeric: tabular-nums; }
|
|
25
25
|
[data-edrv-view] .edrv-pathbar .edrv-pb-status { color: var(--dsw-alias-label-secondary, #52606d); }
|
|
26
|
+
/* 非焦点分组的静态路径条(GroupPathbar):可点击聚焦所属组,悬停给反馈 @author ddj 2026-10-16 */
|
|
27
|
+
[data-edrv-view] .edrv-pathbar[data-edrv-pb-static] { cursor: pointer; }
|
|
28
|
+
[data-edrv-view] .edrv-pathbar[data-edrv-pb-static]:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.06)); }
|
|
26
29
|
[data-edrv-view] .edrv-pb-update { display: inline-flex; align-items: center; gap: 4px; height: 20px; flex: none; padding: 0 6px; border: 1px solid var(--dsw-alias-border-l1, #d9e0e4); border-radius: 4px; background: var(--dsw-alias-bg-layer-2, #f1f4f5); color: var(--dsw-alias-label-secondary, #485762); font-size: 11px; cursor: pointer; }
|
|
27
30
|
[data-edrv-view] .edrv-pb-update:hover { background: var(--dsw-alias-interactive-bg-hover, #e7edf0); }
|
|
28
31
|
[data-edrv-view] .edrv-pb-update:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary, #2869b4); outline-offset: 1px; }
|
|
@@ -61,14 +61,34 @@ export function bindPaneView(editor: ViewEditor, scope: string, path: string, mo
|
|
|
61
61
|
|
|
62
62
|
export interface PassiveProps {
|
|
63
63
|
path: string | null
|
|
64
|
-
|
|
64
|
+
groupId?: string
|
|
65
|
+
mount(host: HTMLDivElement, path: string, groupId?: string): (() => void) | undefined
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 非焦点分组的静态路径条:让路径条在每个编辑分组常驻(分组高度不随焦点切换变化)。
|
|
70
|
+
* 只读展示文件名 + 完整路径,点击聚焦所属组(与 passive 编辑器 pointerdown 同语义)。
|
|
71
|
+
* @public @author ddj 2026年10月16号
|
|
72
|
+
* @param props path 当前组活动文件(null 显示占位);onFocus 点击回调。
|
|
73
|
+
* @returns 恒定 26px 高的路径条元素。
|
|
74
|
+
*/
|
|
75
|
+
export function GroupPathbar(props: { path: string | null; onFocus?(): void }): React.ReactElement {
|
|
76
|
+
const name = props.path ? props.path.split(/[\\/]/).pop() : '未打开文件'
|
|
77
|
+
return React.createElement('div', {
|
|
78
|
+
className: 'edrv-pathbar', 'data-edrv-pb-static': '1',
|
|
79
|
+
title: props.path || '此分页未打开文件', onClick: props.onFocus,
|
|
80
|
+
},
|
|
81
|
+
React.createElement('span', { className: 'edrv-pb-name' }, name),
|
|
82
|
+
React.createElement('span', { className: 'edrv-pb-full' }, props.path || ''))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @public @author ddj 2026年10月16号 @param props Workbench-owned mounting contract. @returns Independent visible file view without global command listeners. */
|
|
68
86
|
export function PassiveEditor(props: PassiveProps): React.ReactElement {
|
|
69
87
|
const host = React.useRef<HTMLDivElement>(null)
|
|
88
|
+
const groupRef = React.useRef(props.groupId)
|
|
89
|
+
groupRef.current = props.groupId
|
|
70
90
|
React.useLayoutEffect(() => {
|
|
71
|
-
if (host.current && props.path) return props.mount(host.current, props.path)
|
|
91
|
+
if (host.current && props.path) return props.mount(host.current, props.path, groupRef.current)
|
|
72
92
|
}, [props.path, props.mount])
|
|
73
93
|
return React.createElement('div', { ref: host, className: 'edrv-passive-view' })
|
|
74
94
|
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { createPortal } from 'react-dom'
|
|
12
|
-
import { EditorPane, EditorSplit, PassiveEditor } from './EditorPane.js'
|
|
12
|
+
import { EditorPane, EditorSplit, PassiveEditor, GroupPathbar } from './EditorPane.js'
|
|
13
13
|
import { FloatingEditor } from './FloatingEditor.js'
|
|
14
14
|
import { FloatingWindow } from './FloatingWindow.js'
|
|
15
15
|
import { createLayout, parseLayout, syncTabs, ownerOf, focusGroup, moveTab, returnGroup, GROUP_LIMIT, MAIN_GROUP } from '../state/editorLayout.js'
|
|
@@ -1898,6 +1898,37 @@ export function EditorView(props) {
|
|
|
1898
1898
|
loadContent(active, sessionId)
|
|
1899
1899
|
}, [active, sessionId])
|
|
1900
1900
|
|
|
1901
|
+
/**
|
|
1902
|
+
* 切换焦点分组时同步重置内容状态(与 setWorkbench 同批渲染)。
|
|
1903
|
+
*
|
|
1904
|
+
* 为什么不能只靠上面的 active effect:它在本轮 commit **之后**才执行——而 mountFull 在 commit 阶段
|
|
1905
|
+
* 就把 fullHost 搬进新焦点组,导致新组先 paint 一帧旧组文件内容(用户所见「内容串组」闪烁)。
|
|
1906
|
+
*
|
|
1907
|
+
* 两个分支(ddj 2026年10月16号):
|
|
1908
|
+
* - 新组文件 model 已缓存 → 直接用缓存内容 seed(同批渲染即最终内容,无 loading 帧 → 切组零空白);
|
|
1909
|
+
* - 未缓存 → setContent(null)(loading 帧),避免旧内容串组。
|
|
1910
|
+
* 之后 active effect 的 loadContent 照常校验/接管(缓存分支 value 相同,React 不会多渲染)。
|
|
1911
|
+
*/
|
|
1912
|
+
const resetContentForSwitch = (nextPath) => {
|
|
1913
|
+
const cached = nextPath ? modelsRef.current.get(nextPath) : null
|
|
1914
|
+
if (cached) {
|
|
1915
|
+
setContent(cached.getValue())
|
|
1916
|
+
setContentPath(nextPath)
|
|
1917
|
+
setLoadStage((prev) => ({ progress: Math.max(prev.progress, 84), message: '文件已读取,准备创建编辑器…' }))
|
|
1918
|
+
setStatus('已加载')
|
|
1919
|
+
return
|
|
1920
|
+
}
|
|
1921
|
+
setContent(null)
|
|
1922
|
+
setContentPath(null)
|
|
1923
|
+
setLoadStage({ progress: 10, message: '准备读取文件…' })
|
|
1924
|
+
setStatus('加载中…')
|
|
1925
|
+
setImageSrc(null)
|
|
1926
|
+
setImgSize(null)
|
|
1927
|
+
setImgBroken(false)
|
|
1928
|
+
setPdfBytes(null)
|
|
1929
|
+
setLoadError(null)
|
|
1930
|
+
}
|
|
1931
|
+
|
|
1901
1932
|
// 页签右键菜单的 Esc 关闭由 ContextMenu 组件自己处理(capture 监听),此处不再重复挂监听
|
|
1902
1933
|
|
|
1903
1934
|
React.useEffect(() => {
|
|
@@ -2191,7 +2222,7 @@ export function EditorView(props) {
|
|
|
2191
2222
|
if (!monaco || !editorRef.current || !active || !contentReady) return
|
|
2192
2223
|
const ed = editorRef.current
|
|
2193
2224
|
const model = getModel(active, content)
|
|
2194
|
-
viewKeeperRef.current?.bind(String(scope), active, model)
|
|
2225
|
+
viewKeeperRef.current?.bind(String(scope), active, model, findKeyOf(scope, focusedGroupOf(active)))
|
|
2195
2226
|
syncTypeSuggest(ed, monaco)
|
|
2196
2227
|
setLoadStage((prev) => ({ progress: Math.max(96, prev.progress), message: '创建编辑器视图…' }))
|
|
2197
2228
|
}, [monaco, scope, active, content, contentReady, mdPreviewing, svnDiff])
|
|
@@ -2291,8 +2322,9 @@ export function EditorView(props) {
|
|
|
2291
2322
|
language: 'plaintext',
|
|
2292
2323
|
theme: themeNameOf(),
|
|
2293
2324
|
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
2294
|
-
|
|
2295
|
-
|
|
2325
|
+
// 16px/21px:与 md 预览渲染态同字号,切分页/切组时观感不再跳变(ddj 2026-10-16)
|
|
2326
|
+
fontSize: 16,
|
|
2327
|
+
lineHeight: 21,
|
|
2296
2328
|
minimap: { enabled: !side, scale: 1 },
|
|
2297
2329
|
glyphMargin: true,
|
|
2298
2330
|
lineDecorationsWidth: 16,
|
|
@@ -3704,16 +3736,21 @@ export function EditorView(props) {
|
|
|
3704
3736
|
saveViewState()
|
|
3705
3737
|
recordNav()
|
|
3706
3738
|
const next = focusGroup(benchRef.current, group, path)
|
|
3739
|
+
const nextActive = next.groups.find((item) => item.id === group)?.active ?? null
|
|
3740
|
+
// 新焦点组文件与当前 contentPath 不同才重置:同文件聚焦无内容切换;缓存命中同步 seed(零空白),未命中清空防串组
|
|
3741
|
+
if (nextActive && nextActive !== activeRef.current) resetContentForSwitch(nextActive)
|
|
3707
3742
|
setWorkbench(next)
|
|
3708
3743
|
setPlanActive(null)
|
|
3709
|
-
setDiskActive(
|
|
3744
|
+
setDiskActive(nextActive)
|
|
3710
3745
|
}
|
|
3711
3746
|
/** @private @author ddj 2026年10月14号 @param next Atomic layout. Accept ownership without close/save side effects. */
|
|
3712
3747
|
function acceptLayout(next) {
|
|
3713
3748
|
saveViewState()
|
|
3749
|
+
const nextActive = next.groups.find((group) => group.id === next.focused)?.active ?? null
|
|
3750
|
+
if (nextActive && nextActive !== activeRef.current) resetContentForSwitch(nextActive)
|
|
3714
3751
|
setWorkbench(next)
|
|
3715
3752
|
setPlanActive(null)
|
|
3716
|
-
setDiskActive(
|
|
3753
|
+
setDiskActive(nextActive)
|
|
3717
3754
|
}
|
|
3718
3755
|
/** @private @author ddj 2026年10月14号 @param path File. @param edge Split direction. Move the file into a new group. */
|
|
3719
3756
|
function splitTab(path, edge) {
|
|
@@ -3765,9 +3802,62 @@ export function EditorView(props) {
|
|
|
3765
3802
|
}
|
|
3766
3803
|
/** @private @author ddj 2026年10月14号 @param host Group body. Keep the full editor subtree stable while changing group/window ownership. */
|
|
3767
3804
|
function mountFull(host) { if (host && fullHost.parentElement !== host) { host.appendChild(fullHost); editorRef.current?.layout() } }
|
|
3768
|
-
/** @private @author ddj 2026年10月
|
|
3769
|
-
function
|
|
3770
|
-
|
|
3805
|
+
/** @private @author ddj 2026年10月16号 @param editor Passive editor instance. @param host Passive view container. @param path Canonical file of this group. @returns Gutter cleanup(断点装饰订阅 + 右键捕获解除). 断点 glyph 列每分页可用:装饰/左键切换/hover 预览/右键菜单与焦点编辑器同语义。 */
|
|
3806
|
+
function bindBpGutter(editor, host, path) {
|
|
3807
|
+
const m = window.monaco
|
|
3808
|
+
const GUTTER = m.editor?.MouseTargetType?.GUTTER_GLYPH_MARGIN ?? 2
|
|
3809
|
+
const bpListOf = () => (dapStore.bpMapOf(dapScopeRef.current)[path] ?? [])
|
|
3810
|
+
|
|
3811
|
+
const applyBp = () => {
|
|
3812
|
+
const map = dapStore.bpMapOf(dapScopeRef.current)
|
|
3813
|
+
const unverified = bpLinesOf(map, path).filter((line) => dapStore.ackOf(path, line)?.verified === false)
|
|
3814
|
+
applyBpDecorations(editor, bpLinesOf(map, path), bpDisabledLinesOf(map, path), unverified)
|
|
3815
|
+
}
|
|
3816
|
+
applyBp()
|
|
3817
|
+
const unsubscribeBp = dapStore.subscribe(applyBp)
|
|
3818
|
+
|
|
3819
|
+
editor.onMouseDown((e) => {
|
|
3820
|
+
if (!e?.event?.leftButton) return
|
|
3821
|
+
const target = e?.target
|
|
3822
|
+
if (!target || target.type !== GUTTER) return
|
|
3823
|
+
const line = target.position?.lineNumber
|
|
3824
|
+
if (line) dapToggleRef.current?.(path, line)
|
|
3825
|
+
})
|
|
3826
|
+
editor.onMouseMove((e) => {
|
|
3827
|
+
const target = e?.target
|
|
3828
|
+
const line = hintLineFor(target?.type, target?.position?.lineNumber, bpListOf().map((it) => it.line))
|
|
3829
|
+
setHintLine(editor, line || null)
|
|
3830
|
+
})
|
|
3831
|
+
editor.onMouseLeave(() => setHintLine(editor, null))
|
|
3832
|
+
|
|
3833
|
+
const glyphHit = (ev) => {
|
|
3834
|
+
if (!editor.getTargetAtClientPoint) return null
|
|
3835
|
+
const rect = editor.getDomNode?.()?.getBoundingClientRect?.()
|
|
3836
|
+
const layout = editor.getLayoutInfo?.()
|
|
3837
|
+
if (!rect || !layout) return null
|
|
3838
|
+
const relX = ev.clientX - rect.left
|
|
3839
|
+
if (relX < layout.glyphMarginLeft || relX > layout.glyphMarginLeft + layout.glyphMarginWidth) return null
|
|
3840
|
+
const target = editor.getTargetAtClientPoint(ev.clientX, ev.clientY)
|
|
3841
|
+
if (!target || target.type !== GUTTER) return null
|
|
3842
|
+
return target.position?.lineNumber ?? null
|
|
3843
|
+
}
|
|
3844
|
+
const onCtx = (ev) => {
|
|
3845
|
+
const line = glyphHit(ev)
|
|
3846
|
+
if (!line) return
|
|
3847
|
+
const entry = bpListOf().find((it) => it.line === line)
|
|
3848
|
+
ev.preventDefault()
|
|
3849
|
+
ev.stopPropagation()
|
|
3850
|
+
setDapBpMenu({
|
|
3851
|
+
x: ev.clientX, y: ev.clientY, path, line,
|
|
3852
|
+
hasBp: !!entry, enabled: entry ? entry.enabled !== false : true, isLogpoint: !!(entry && entry.logMessage),
|
|
3853
|
+
})
|
|
3854
|
+
}
|
|
3855
|
+
host.addEventListener('contextmenu', onCtx, true)
|
|
3856
|
+
return () => { unsubscribeBp?.(); host.removeEventListener('contextmenu', onCtx) }
|
|
3857
|
+
}
|
|
3858
|
+
/** @private @author ddj 2026年10月16号 @param host Passive body. @param path Canonical file. @param groupId Owning group id(find 快照按组隔离). @returns View-only cleanup, models remain workbench-owned. */
|
|
3859
|
+
function mountPassive(host, path, groupId) {
|
|
3860
|
+
let disposed = false, keeper = null, subscription = null, observer = null, unbindGutter = null
|
|
3771
3861
|
const focus = () => { const owner = ownerOf(benchRef.current, path); if (owner) selectGroup(owner.id, path) }
|
|
3772
3862
|
host.addEventListener('pointerdown', focus)
|
|
3773
3863
|
if (isPdfPath(path) && pdfNodes.current.has(path)) {
|
|
@@ -3792,9 +3882,13 @@ export function EditorView(props) {
|
|
|
3792
3882
|
if (!window.monaco) { host.textContent = '编辑器正在加载'; return }
|
|
3793
3883
|
const model = cached || getModel(path, result.content)
|
|
3794
3884
|
if (result.version) { revRef.current[path] = result.version; recordBaseline(scope, path, result.version) }
|
|
3795
|
-
const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false
|
|
3885
|
+
const editor = window.monaco.editor.create(host, { model, automaticLayout: true, minimap: { enabled: false }, scrollBeyondLastLine: false, glyphMargin: true,
|
|
3886
|
+
// 与焦点编辑器(ensureEditor)同观感参数:字号/行高/顶部留白(side 形态 6,tab 形态 8)/装饰宽度,切分页行号位置不跳变(ddj 2026-10-16)
|
|
3887
|
+
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)', fontSize: 16, lineHeight: 21,
|
|
3888
|
+
lineDecorationsWidth: 16, padding: { top: layoutRef.current === 'side' ? 6 : 8 } })
|
|
3889
|
+
const unbindGutter = bindBpGutter(editor, host, path)
|
|
3796
3890
|
keeper = createViewKeeper(editor)
|
|
3797
|
-
keeper.bind(scope, path, model)
|
|
3891
|
+
keeper.bind(scope, path, model, groupId ? findKeyOf(scope, groupId) : undefined)
|
|
3798
3892
|
paneEditors.current.set(path, editor)
|
|
3799
3893
|
const store = documents()
|
|
3800
3894
|
store.bind(path, model, revRef.current[path])
|
|
@@ -3802,11 +3896,15 @@ export function EditorView(props) {
|
|
|
3802
3896
|
observer = new ResizeObserver(() => editor.layout())
|
|
3803
3897
|
observer.observe(host)
|
|
3804
3898
|
}).catch((failure) => { if (!disposed) host.textContent = '文件加载失败:' + String(failure) })
|
|
3805
|
-
return () => { disposed = true; host.removeEventListener('pointerdown', focus); observer?.disconnect(); subscription?.dispose(); keeper?.dispose(); paneEditors.current.delete(path) }
|
|
3899
|
+
return () => { disposed = true; host.removeEventListener('pointerdown', focus); unbindGutter?.(); observer?.disconnect(); subscription?.dispose(); keeper?.dispose(); paneEditors.current.delete(path) }
|
|
3806
3900
|
}
|
|
3807
3901
|
const passiveOps = React.useRef(mountPassive)
|
|
3808
3902
|
passiveOps.current = mountPassive
|
|
3809
|
-
const passiveMount = React.useCallback((host, path) => passiveOps.current(host, path), [])
|
|
3903
|
+
const passiveMount = React.useCallback((host, path, groupId) => passiveOps.current(host, path, groupId), [])
|
|
3904
|
+
/** @private @author ddj 2026年10月16号 @param scope Workspace scope. @param groupId Editor group id. @returns Ctrl+F snapshot bucket key isolating find state per group. */
|
|
3905
|
+
function findKeyOf(scope, groupId) { return String(scope) + '\u0000' + String(groupId) }
|
|
3906
|
+
/** @private @author ddj 2026年10月16号 @param active Active file path. @returns Owning group id, falling back to the focused group. */
|
|
3907
|
+
function focusedGroupOf(active) { return ownerOf(benchRef.current, active)?.id ?? benchRef.current.focused }
|
|
3810
3908
|
/** @private @author ddj 2026年10月14号 @param node Split branch. @param ratio Updated branch ratio. */
|
|
3811
3909
|
function changeRatio(node, ratio) {
|
|
3812
3910
|
const replace = (current) => current === node ? { ...current, ratio } : 'group' in current ? current : { ...current, first: replace(current.first), second: replace(current.second) }
|
|
@@ -3818,7 +3916,9 @@ export function EditorView(props) {
|
|
|
3818
3916
|
if (!group) return null
|
|
3819
3917
|
const focused = id === workbench.focused
|
|
3820
3918
|
return React.createElement(EditorPane, { key: id, group, focused, dirty: dirtyMap, onSelect: selectGroup, onClose: closeTab, onMenu: paneMenu, onDrag: dragTab, consumeClick: consumeDrag },
|
|
3821
|
-
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(
|
|
3919
|
+
focused ? React.createElement('div', { className: 'edrv-full-slot', ref: mountFull }) : React.createElement(React.Fragment, null,
|
|
3920
|
+
React.createElement(GroupPathbar, { path: group.active, onFocus: () => selectGroup(id) }),
|
|
3921
|
+
React.createElement(PassiveEditor, { key: String(scope) + ':' + group.active, path: group.active, groupId: id, mount: passiveMount })))
|
|
3822
3922
|
}
|
|
3823
3923
|
/** @private @author ddj 2026年10月14号 @param node Layout subtree. @returns Recursive resizable panes. */
|
|
3824
3924
|
function renderTree(node) {
|
|
@@ -4359,10 +4459,12 @@ export function EditorView(props) {
|
|
|
4359
4459
|
React.createElement('button', { className: 'edrv-pill edrv-pill-ghost', title: '复制安装命令', onClick: copyInstallCmd }, '复制命令'),
|
|
4360
4460
|
React.createElement('button', { className: 'edrv-side-hint-close', title: '关闭提示', 'aria-label': '关闭提示', onClick: dismissHint }, '×'))
|
|
4361
4461
|
: null
|
|
4362
|
-
|
|
4462
|
+
// 底部状态栏共用一整条:挂在 mainCol 层级(所有分组之下),不随焦点分组搬家(ddj 2026-10-16)
|
|
4463
|
+
const fullView = createPortal(React.createElement(React.Fragment, null, pathBar, sideHintEl, diskBanner(), editorArea), fullHost, 'edrv-full-editor')
|
|
4363
4464
|
const mainCol = React.createElement('div', { className: 'edrv-main-col edrv-workbench', style: { flex: 1, minWidth: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' } },
|
|
4364
4465
|
React.createElement('div', { className: 'edrv-workbench-tools' }, tabRow),
|
|
4365
4466
|
renderTree(workbench.tree), fullView,
|
|
4467
|
+
statusBar,
|
|
4366
4468
|
workbench.floats.map((item, layer) => React.createElement(FloatingEditor, { key: item.group, owner: String(scope), group: item.group, rect: item, layer, focused: item.group === workbench.focused, onBounds: floatBounds, onFocus: selectGroup, onReturn: returnPane }, renderGroup(item.group))),
|
|
4367
4469
|
dragPreview ? createPortal(React.createElement(React.Fragment, null,
|
|
4368
4470
|
dragPreview.rect ? React.createElement('div', { className: 'edrv-drag-overlay', style: dragPreview.rect }, React.createElement('span', { className: 'edrv-drop-label' }, dragPreview.label)) : null,
|
|
@@ -115,6 +115,9 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
115
115
|
const [devForm, setDevForm] = React.useState(null)
|
|
116
116
|
const [devBusy, setDevBusy] = React.useState(false)
|
|
117
117
|
const [devMessage, setDevMessage] = React.useState('')
|
|
118
|
+
const [updateState, setUpdateState] = React.useState(null) // { current, latest, hasUpdate }
|
|
119
|
+
const [updateBusy, setUpdateBusy] = React.useState(false)
|
|
120
|
+
const [updateMessage, setUpdateMessage] = React.useState('')
|
|
118
121
|
const [minW, setMinW] = React.useState(SIDEBAR_MIN_DEFAULT)
|
|
119
122
|
const [minDraft, setMinDraft] = React.useState(null) // 输入草稿(null=跟随已保存值;blur/Enter 提交)
|
|
120
123
|
const [limit, setLimit] = React.useState(EDITOR_LIMIT_DEFAULT)
|
|
@@ -198,6 +201,27 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
198
201
|
setDevMessage(result.restart ? '已切换为正式版,重启 DSH 后生效。' : '已关闭开发形态。')
|
|
199
202
|
}).catch((e) => setDevMessage('关闭失败:' + String(e))).finally(() => setDevBusy(false))
|
|
200
203
|
}
|
|
204
|
+
/** 检查 npm registry 是否有比当前版本更新的发布(结果落 updateState)。 */
|
|
205
|
+
const runUpdateCheck = () => {
|
|
206
|
+
setUpdateBusy(true)
|
|
207
|
+
setUpdateMessage('')
|
|
208
|
+
rpc('vscode.updateCheck', {}).then((result) => {
|
|
209
|
+
if (!result.ok) { setUpdateMessage('检查失败:' + String(result.error)); return }
|
|
210
|
+
setUpdateState({ current: result.current ?? '', latest: result.latest ?? '', hasUpdate: result.hasUpdate === true })
|
|
211
|
+
setUpdateMessage(result.hasUpdate ? '发现新版本 v' + result.latest : '已是最新版本。')
|
|
212
|
+
}).catch((e) => setUpdateMessage('检查失败:' + String(e))).finally(() => setUpdateBusy(false))
|
|
213
|
+
}
|
|
214
|
+
/** 触发官方 pluginManager「先删后装」自我更新(页面会断开,成功与否均不再回报本页)。 */
|
|
215
|
+
const runSelfUpdate = () => {
|
|
216
|
+
const target = updateState?.latest ?? ''
|
|
217
|
+
if (!window.confirm('更新到 v' + target + ':更新中插件会先卸载再安装,页面将短暂断开,完成后请刷新页面或重启 DSH。\n注意:若安装失败(如断网),插件会保持未安装状态,可在官方插件管理页重新安装恢复。确认更新?')) return
|
|
218
|
+
setUpdateBusy(true)
|
|
219
|
+
setUpdateMessage('')
|
|
220
|
+
rpc('vscode.updateSelf', {}).then((result) => {
|
|
221
|
+
if (!result.ok) { setUpdateMessage('更新失败:' + String(result.error)); setUpdateBusy(false); return }
|
|
222
|
+
setUpdateMessage('更新已开始,页面即将断开;完成后请刷新页面或重启 DSH。')
|
|
223
|
+
}).catch((e) => { setUpdateMessage('更新失败:' + String(e)); setUpdateBusy(false) })
|
|
224
|
+
}
|
|
201
225
|
/**
|
|
202
226
|
* 唤起官方快捷键弹窗(键位统一归官方机制;入口不可用时展示手动打开方式)。
|
|
203
227
|
* @author ddj 2026年09月28号
|
|
@@ -215,6 +239,17 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
215
239
|
React.createElement('button', { className: 'vsm-devform-close', disabled: devBusy, onClick: closeDevForm }, devBusy ? '切换中…' : '关闭开发形态'),
|
|
216
240
|
),
|
|
217
241
|
) : null
|
|
242
|
+
const updateRow = React.createElement('div', { className: 'vsm-general-row' },
|
|
243
|
+
React.createElement('span', null, '插件更新',
|
|
244
|
+
React.createElement('small', { className: 'vsm-devform-note' },
|
|
245
|
+
'当前版本 v' + (updateState?.current ?? '') + (devForm?.enabled ? ';开发形态下请走工作区同步流程' : ''))),
|
|
246
|
+
React.createElement('div', { className: 'vsm-devform-actions' },
|
|
247
|
+
updateMessage && React.createElement('small', { className: 'vsm-devform-msg' }, updateMessage),
|
|
248
|
+
updateState?.hasUpdate
|
|
249
|
+
? React.createElement('button', { disabled: updateBusy || devForm?.enabled === true, onClick: runSelfUpdate, title: devForm?.enabled ? '开发形态下请走工作区同步流程' : undefined }, updateBusy ? '更新中…' : '更新到 v' + (updateState.latest ?? ''))
|
|
250
|
+
: React.createElement('button', { disabled: updateBusy || devForm?.enabled === true, onClick: runUpdateCheck, title: devForm?.enabled ? '开发形态下请走工作区同步流程' : undefined }, updateBusy ? '检查中…' : '检查更新'),
|
|
251
|
+
),
|
|
252
|
+
)
|
|
218
253
|
return React.createElement('section', { className: 'vsm-general-page' },
|
|
219
254
|
React.createElement('h2', null, '通用'),
|
|
220
255
|
React.createElement('p', null, '配置 DSH 对话中文件链接的打开工具。工具由当前运行时自动扫描。'),
|
|
@@ -225,6 +260,7 @@ function GeneralSettings({ registry, getShortcuts }) {
|
|
|
225
260
|
React.createElement('div', { className: 'vsm-panel-body' },
|
|
226
261
|
React.createElement('label', { className: 'vsm-general-row' }, React.createElement('span', null, '文件链接使用工具'), React.createElement('select', { value: currentOption, disabled: loading || unavailable || notReady || busy || snapshot?.writable === false, onChange: (event) => save(event.target.value) }, options.map((item) => React.createElement('option', { key: item.id, value: item.id }, item.label))), loading && React.createElement('small', null, '读取中…')),
|
|
227
262
|
devFormRow,
|
|
263
|
+
updateRow,
|
|
228
264
|
),
|
|
229
265
|
),
|
|
230
266
|
React.createElement('section', { className: 'vsm-panel' },
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 可用更新主动提醒:启动延迟 + 抖动后检查 registry 新版本,
|
|
3
|
+
* 有更新且该版本未提醒过时经 toastDom 弹提示(每版本一次,localStorage 去重)。
|
|
4
|
+
* 设计:检查失败静默(不打扰);开发形态(link: 安装)下不提醒(应走工作区同步)。
|
|
5
|
+
* 作者 ddj 2026-10-16
|
|
6
|
+
*/
|
|
7
|
+
import { rpc } from './rpc.js'
|
|
8
|
+
import { toastDom } from './externalOpen.js'
|
|
9
|
+
import { PLUGIN_NAME } from '../compat.js'
|
|
10
|
+
|
|
11
|
+
/** localStorage 去重键:值为已提醒过的最新版本号。 */
|
|
12
|
+
const NOTICE_KEY = 'vsm-update-notice'
|
|
13
|
+
|
|
14
|
+
/** 启动后首次检查的延迟(毫秒)。 */
|
|
15
|
+
const NOTICE_DELAY_MS = 30000
|
|
16
|
+
|
|
17
|
+
/** 抖动上限(毫秒):避免多客户端同时请求 registry。 */
|
|
18
|
+
const NOTICE_JITTER_MS = 30000
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 读取开发形态状态(客户端侧经 RPC 查询;失败按未开启处理)。
|
|
22
|
+
* @private
|
|
23
|
+
* @author ddj 2026年10月16号
|
|
24
|
+
* @returns devForm.enabled 是否为 true
|
|
25
|
+
*/
|
|
26
|
+
async function devFormEnabled(): Promise<boolean> {
|
|
27
|
+
try {
|
|
28
|
+
const result = await rpc('vscode.devFormGet', {})
|
|
29
|
+
return result.ok === true && (result.devForm as { enabled?: boolean } | undefined)?.enabled === true
|
|
30
|
+
} catch {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 执行一次可用更新检查并在需要时弹 toast 提醒(同版本只提醒一次)。
|
|
37
|
+
* @private
|
|
38
|
+
* @author ddj 2026年10月16号
|
|
39
|
+
*/
|
|
40
|
+
async function checkAndNotify(): Promise<void> {
|
|
41
|
+
if (await devFormEnabled()) return
|
|
42
|
+
const result = await rpc('vscode.updateCheck', {})
|
|
43
|
+
if (result.ok !== true || result.hasUpdate !== true) return
|
|
44
|
+
const latest = String(result.latest ?? '')
|
|
45
|
+
if (!latest) return
|
|
46
|
+
let storage: Storage | null = null
|
|
47
|
+
try { storage = window.localStorage } catch { /* 隐私模式等场景静默降级 */ }
|
|
48
|
+
if (storage?.getItem(NOTICE_KEY) === latest) return
|
|
49
|
+
storage?.setItem(NOTICE_KEY, latest)
|
|
50
|
+
toastDom(PLUGIN_NAME + ' 有可用更新:v' + latest + '(设置页「文件与安装」可一键更新)')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 装配更新提醒:启动 30s + 0~30s 抖动后检查一次;失败静默。
|
|
55
|
+
* @public
|
|
56
|
+
* @author ddj 2026年10月16号
|
|
57
|
+
* @param ctx DSH client 上下文
|
|
58
|
+
* @param schedule 定时器注入(ctx.timeout),便于测试与生命周期统一
|
|
59
|
+
*/
|
|
60
|
+
export function installUpdateNotice(ctx: { timeout(fn: () => void, ms: number): unknown }, schedule: (fn: () => void, ms: number) => void): void {
|
|
61
|
+
schedule(() => {
|
|
62
|
+
void checkAndNotify().catch(() => { /* 检查失败静默,不打扰 */ })
|
|
63
|
+
}, NOTICE_DELAY_MS + Math.floor(Math.random() * NOTICE_JITTER_MS))
|
|
64
|
+
}
|
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/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
|
}
|