dsh-vscode-mode 0.4.6 → 0.5.1
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 +38 -2
- package/lib/client.js +9316 -4038
- package/lib/client.js.map +1 -1
- package/lib/index.js +1852 -68
- package/lib/index.js.map +1 -1
- package/package.json +8 -8
- package/src/client/ai/inlineProvider.ts +42 -1
- package/src/client/index.ts +50 -45
- package/src/client/monaco/lsp/index.ts +33 -3
- package/src/client/monaco/lsp/lspClient.ts +3 -1
- package/src/client/monaco/lsp/providers.ts +60 -6
- package/src/client/navHighlight.ts +96 -0
- package/src/client/officialSidebar.ts +67 -3
- package/src/client/paths.ts +2 -0
- package/src/client/rpc.ts +32 -12
- package/src/client/sessionScope.ts +183 -0
- package/src/client/sidebar/SidebarView.ts +6 -2
- package/src/client/sidebar/menuItems.ts +93 -2
- package/src/client/sidebar/panels/FileExplorer.ts +18 -1
- package/src/client/sidebar/panels/SvnPanel.ts +303 -0
- package/src/client/sidebar/panels/index.ts +44 -3
- package/src/client/sidebar/types.ts +20 -0
- package/src/client/snippets/provider.ts +26 -4
- package/src/client/styles/editor.css +177 -0
- package/src/client/svnActions.ts +153 -0
- package/src/client/svnLog.ts +356 -0
- package/src/client/svnStatus.ts +363 -0
- package/src/client/svnStore.ts +150 -0
- package/src/client/tabActions.ts +30 -5
- package/src/client/tabMenu.ts +51 -0
- package/src/client/ui/AiSettings.ts +8 -0
- package/src/client/ui/EditorView.ts +632 -35
- package/src/client/ui/LogDialog.ts +333 -0
- package/src/client/ui/McpSettings.ts +98 -0
- package/src/client/ui/ModalShell.ts +79 -0
- package/src/client/ui/SideBySideDiff.tsx +139 -0
- package/src/client/ui/SnippetsPicker.ts +70 -58
- package/src/client/ui/SvnDiffPanel.ts +50 -0
- package/src/client/ui/SvnLogDialog.ts +1141 -0
- package/src/client/ui/SvnLogStats.ts +243 -0
- package/src/client/ui/commandCatalog.ts +72 -0
- package/src/compat.ts +1 -1
- package/src/debugLog.ts +81 -12
- package/src/dshVersion.ts +8 -1
- package/src/fileOpenSettings.ts +40 -3
- package/src/index.ts +5 -1
- package/src/rpc.ts +41 -4
- package/src/shared/ai.ts +25 -0
- package/src/shared/rpc.ts +53 -1
- package/src/shared/svn.ts +377 -0
- package/src/shared/svnActions.ts +296 -0
- package/src/svn.ts +870 -0
- package/src/svnLog.ts +315 -0
- package/src/svnText.ts +175 -0
- package/src/svnXml.ts +110 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
3
|
+
"version": "0.5.1",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
|
@@ -45,12 +45,12 @@
|
|
|
45
45
|
"@vscode/ripgrep": "^1.18.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
49
|
-
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0",
|
|
50
|
-
"@deepseek-ai/dsh-client-
|
|
51
|
-
"@deepseek-ai/dsh-client-ui-
|
|
52
|
-
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
53
|
-
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0",
|
|
48
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
49
|
+
"@deepseek-ai/dsh-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
51
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
52
|
+
"@deepseek-ai/dsh-llm": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
53
|
+
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0 || >=0.1.1-0 <0.2.0-0 || >=0.1.2-0 <0.2.0-0 || >=0.1.3-0 <0.2.0-0 || >=0.1.4-0 <0.2.0-0 || >=0.1.5-0 <0.2.0-0 || >=0.1.6-0 <0.2.0-0",
|
|
54
54
|
"cordis": ">=4.0.0-rc <5",
|
|
55
55
|
"react": "^18.2.0",
|
|
56
56
|
"react-dom": "^18.2.0",
|
|
@@ -38,6 +38,17 @@ let inFlight = null
|
|
|
38
38
|
let debounceTimer = null
|
|
39
39
|
const cache = new Map()
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* 补全 provider 注销器的全局挂点名。
|
|
43
|
+
*
|
|
44
|
+
* 为什么挂 window 而不是只靠模块级 registered:DSH 0.1.6-alpha.2 起支持插件运行时
|
|
45
|
+
* 卸载/重载,而 `window.monaco` 由 loader 注入后**跨重载存活**,模块级状态却会随
|
|
46
|
+
* bundle 重新求值而复位 —— 只判 registered 会在每次重载后再注册一次 provider,
|
|
47
|
+
* 导致同一编辑器出现重复补全。故注销器落 window,重载时命中即跳过注册。
|
|
48
|
+
* @author ddj 2026年09月18号
|
|
49
|
+
*/
|
|
50
|
+
const AI_INLINE_GLOBAL = '__edrvAiInlineDisposer__'
|
|
51
|
+
|
|
41
52
|
/** 上次开关状态(configUpdate 后经事件刷新,关闭时立即静默)。 */
|
|
42
53
|
let enabled = false
|
|
43
54
|
|
|
@@ -96,8 +107,15 @@ function diffPending(editor) {
|
|
|
96
107
|
*/
|
|
97
108
|
export function registerAiInline(monaco) {
|
|
98
109
|
if (registered || !monaco?.languages?.registerInlineCompletionsProvider) return
|
|
110
|
+
// 跨重载守卫:上一代 bundle 注册的 provider 仍挂在存活的 window.monaco 上,
|
|
111
|
+
// 此时直接认领为「已注册」,避免重复注册(注销器在首次注册时已落 window)。
|
|
112
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
113
|
+
if (host && host[AI_INLINE_GLOBAL]) {
|
|
114
|
+
registered = true
|
|
115
|
+
return
|
|
116
|
+
}
|
|
99
117
|
registered = true
|
|
100
|
-
monaco.languages.registerInlineCompletionsProvider('*', {
|
|
118
|
+
const inlineDisposer = monaco.languages.registerInlineCompletionsProvider('*', {
|
|
101
119
|
async provideInlineCompletions(model, position, context, token) {
|
|
102
120
|
const t0 = Date.now()
|
|
103
121
|
if (!enabled || token?.isCancellationRequested) return { items: [] }
|
|
@@ -166,6 +184,29 @@ export function registerAiInline(monaco) {
|
|
|
166
184
|
},
|
|
167
185
|
freeInlineCompletions() { /* 无资源需释放 */ },
|
|
168
186
|
})
|
|
187
|
+
if (host) host[AI_INLINE_GLOBAL] = inlineDisposer
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* 卸载 AI 内联补全:注销 provider 并复位模块状态(插件重载/卸载时调用)。
|
|
192
|
+
*
|
|
193
|
+
* 复位 registered 让重装后能重新注册;清 window 挂点避免新实例被旧注销器误判为「已注册」。
|
|
194
|
+
* 同时作废在飞请求与去抖定时器,防止卸载后仍有回调写状态。
|
|
195
|
+
* @author ddj 2026年09月18号
|
|
196
|
+
*/
|
|
197
|
+
export function disposeAiInline() {
|
|
198
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
199
|
+
const inlineDisposer = host ? host[AI_INLINE_GLOBAL] : undefined
|
|
200
|
+
if (inlineDisposer && typeof inlineDisposer.dispose === 'function') {
|
|
201
|
+
try { inlineDisposer.dispose() } catch { /* 已注销 */ }
|
|
202
|
+
}
|
|
203
|
+
if (host) delete host[AI_INLINE_GLOBAL]
|
|
204
|
+
registered = false
|
|
205
|
+
seq += 1 // 作废在飞响应
|
|
206
|
+
if (debounceTimer) { clearTimeout(debounceTimer); debounceTimer = null }
|
|
207
|
+
if (inFlight) { try { inFlight.abort() } catch { /* 已结束 */ } inFlight = null }
|
|
208
|
+
cache.clear()
|
|
209
|
+
monacoRef = null
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
/** 缓存写入(超限整体清空,LRU 简化)。 */
|
package/src/client/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
|
40
40
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
41
41
|
import { createSearchPanel } from './sidebar/panels/index.js'
|
|
42
42
|
import { createRulesPanel } from './sidebar/panels/index.js'
|
|
43
|
+
import { createSvnPanel } from './sidebar/panels/index.js'
|
|
43
44
|
import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
|
|
44
45
|
import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
45
46
|
import { createOutlinePanel } from './outline/index.js'
|
|
@@ -50,7 +51,10 @@ import { createCommandBridge } from './commandBridge.js'
|
|
|
50
51
|
import { REGISTRY_GLOBAL } from './commandGlobals.js'
|
|
51
52
|
import { sidebarMinApply } from './sidebarMin.js'
|
|
52
53
|
import { log } from './log.js'
|
|
53
|
-
import { setupLsp, setSession } from './monaco/lsp/index.js'
|
|
54
|
+
import { setupLsp, setSession, disposeLsp } from './monaco/lsp/index.js'
|
|
55
|
+
import { disposeSnippets } from './snippets/provider.js'
|
|
56
|
+
import { disposeAiInline } from './ai/inlineProvider.js'
|
|
57
|
+
import { readSessionScope, subscribeScope } from './sessionScope.js'
|
|
54
58
|
import type { CompatAdapter } from '../shared/compat.js'
|
|
55
59
|
|
|
56
60
|
// ⚠️ inject 只列必需服务:webUiSettings 是 @linxin666/dsh-client-ui-web-ui-settings 提供的
|
|
@@ -104,52 +108,48 @@ export function apply(ctx: any): void {
|
|
|
104
108
|
// 外部深链落地:Windows 右键菜单 / Unity 外部编辑器 → 浏览器 URL 参数 → 打开规则路由
|
|
105
109
|
setupExtOpen(ctx)
|
|
106
110
|
|
|
107
|
-
// Monaco 加载时机:不再 DSH
|
|
111
|
+
// Monaco 加载时机:不再 DSH 启动即预热,改为进入会话界面(当前会话出现)后再后台加载,
|
|
108
112
|
// 用户点开「文件编辑」页签即用;空闲时仍由 EditorView 挂载兜底加载。
|
|
109
113
|
// 模块级 promise 去重,重复触发只首次真正加载;requestIdleCallback 让出会话首屏带宽,缺省回退延时调度;
|
|
110
114
|
// 预热失败静默吞掉(loader 失败会重置 promise,页签打开时仍走原有加载/重试路径)。
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
115
|
+
//
|
|
116
|
+
// 会话取值经 readSessionScope 收敛(跨版本三级链,见 sessionScope.ts):
|
|
117
|
+
// DSH 0.1.6-alpha.2 随多实例共存移除了 sessions.list.current,旧版则没有 uiSession;
|
|
118
|
+
// 原先直读 list.getSnapshot().current 在新版恒为 undefined,会让预热/LSP 同步/编辑 Tab 恢复静默失效。
|
|
119
|
+
// 会话切换 → LSP 文档归属更新 + 状态刷新(原先为两条订阅,取值源一致,此处合流为一条)。
|
|
120
|
+
if (typeof window !== 'undefined') {
|
|
114
121
|
const schedulePreload = () => {
|
|
115
122
|
const preload = () => loadMonaco(() => {}).then((m: unknown) => setupLsp(m)).catch(() => {})
|
|
116
123
|
if (typeof window.requestIdleCallback === 'function') window.requestIdleCallback(preload, { timeout: 2000 })
|
|
117
124
|
else schedule(preload, 300)
|
|
118
125
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
126
|
+
let preloadedFor: string | undefined
|
|
127
|
+
const onScopeChange = () => {
|
|
128
|
+
const scope = readSessionScope(ctx)
|
|
129
|
+
// 传 undefined = 清除绑定(保留原 LSP 同步语义:无当前会话时不残留旧归属)
|
|
130
|
+
setSession(scope.sessionId)
|
|
131
|
+
if (!scope.sessionId || scope.sessionId === preloadedFor) return
|
|
132
|
+
preloadedFor = scope.sessionId
|
|
123
133
|
schedulePreload()
|
|
124
134
|
}
|
|
125
|
-
ctx.effect(() =>
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
// 会话切换 → LSP 文档归属更新 + 状态刷新
|
|
129
|
-
if (monacoList && typeof monacoList.subscribe === 'function') {
|
|
130
|
-
const list = monacoList as { getSnapshot: () => { current?: string }; subscribe: (listener: () => void) => () => void }
|
|
131
|
-
ctx.effect(() => list.subscribe(() => {
|
|
132
|
-
setSession(list.getSnapshot()?.current)
|
|
133
|
-
}), 'vscode-mode: lsp session sync')
|
|
135
|
+
ctx.effect(() => subscribeScope(ctx, onScopeChange), 'vscode-mode: monaco session trigger')
|
|
136
|
+
onScopeChange()
|
|
134
137
|
}
|
|
135
138
|
// 会话切换 → 编辑 Tab 跨会话自动恢复:上一会话编辑区激活时,新会话自动展开并激活
|
|
136
139
|
//(官方停靠面按会话隔离,新会话默认收起;先试 openTabIn 直写新会话面,未 adopt 走有界重试)。
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
return list.subscribe(() => schedule(check, 0))
|
|
151
|
-
}, 'vscode-mode: editor tab restore')
|
|
152
|
-
}
|
|
140
|
+
// 注意:首次打开某会话时通知早于会话字段落定,故通知只调度延迟任务,执行时再读最新作用域
|
|
141
|
+
ctx.effect(() => {
|
|
142
|
+
let lastCurrent = readSessionScope(ctx).sessionId
|
|
143
|
+
const check = () => {
|
|
144
|
+
const current = readSessionScope(ctx).sessionId
|
|
145
|
+
if (!current || current === lastCurrent) return
|
|
146
|
+
lastCurrent = current
|
|
147
|
+
if (!isEditorTabActive() || officialService === undefined) return
|
|
148
|
+
try { officialService.service.openTabIn?.(current, OFFICIAL_TAB_KIND, {}) } catch { /* 未 adopt 等 seat 就绪走重试 */ }
|
|
149
|
+
restoreEditorTab(officialService.service, schedule)
|
|
150
|
+
}
|
|
151
|
+
return subscribeScope(ctx, () => schedule(check, 0))
|
|
152
|
+
}, 'vscode-mode: editor tab restore')
|
|
153
153
|
const originalOpenPath = workspaces?.openPath
|
|
154
154
|
const binder = pickSettingsBinder(ctx)
|
|
155
155
|
const settings = binder.scope
|
|
@@ -158,13 +158,8 @@ export function apply(ctx: any): void {
|
|
|
158
158
|
let remoteOpenInstalled = false
|
|
159
159
|
/** 两条文件链接路由共用的路由日志(openPathRouter / remoteOpenRouter;统一走插件日志器)。 */
|
|
160
160
|
const routeLogger = (message: string): void => log.warn(message)
|
|
161
|
-
/** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd
|
|
162
|
-
const openContext = (): FileOpenContext =>
|
|
163
|
-
const current = sessions?.list?.getSnapshot?.()
|
|
164
|
-
const sessionId = current?.current
|
|
165
|
-
const summary = sessionId ? current?.byId?.[sessionId] : undefined
|
|
166
|
-
return { sessionId, cwd: summary?.cwd } as FileOpenContext
|
|
167
|
-
}
|
|
161
|
+
/** 两条路由共用的 FileOpenContext:当前会话 id 与工作区 cwd(跨版本取值链)。 */
|
|
162
|
+
const openContext = (): FileOpenContext => readSessionScope(ctx) as FileOpenContext
|
|
168
163
|
/** 可选探测 betterSidebar 服务(不进 inject:缺失会让插件停靠等待,杀死回退路径)。 */
|
|
169
164
|
let sideService = detectSidebarService(ctx)
|
|
170
165
|
/** 可选探测官方右侧 Sidebar(DSH 0.1.5-alpha.1+;探测命中 ≡ 版本判定,不进 inject 同上)。 */
|
|
@@ -196,6 +191,8 @@ export function apply(ctx: any): void {
|
|
|
196
191
|
ctx.effect(() => sidebarPanels.register(createSearchPanel()), 'vscode-mode: sidebar panel search')
|
|
197
192
|
// 规则面板:活动栏「规则」页签(Codebuddy 规则管理形态:用户/项目规则 + 启用开关,host 注入生效)
|
|
198
193
|
ctx.effect(() => sidebarPanels.register(createRulesPanel()), 'vscode-mode: sidebar panel rules')
|
|
194
|
+
// SVN 变更面板:活动栏「SVN 变更」页签(工作副本状态列表;visible 守卫使非 SVN 工作区不出现)
|
|
195
|
+
ctx.effect(() => sidebarPanels.register(createSvnPanel()), 'vscode-mode: sidebar panel svn')
|
|
199
196
|
// 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
|
|
200
197
|
const fileMenuItems = createTreeMenuRegistry()
|
|
201
198
|
ctx.provide('edrvFileContextMenuItems', fileMenuItems)
|
|
@@ -378,10 +375,9 @@ export function apply(ctx: any): void {
|
|
|
378
375
|
service: sideService as NonNullable<typeof sideService>,
|
|
379
376
|
renderTab: (props: Record<string, unknown>) => React.createElement(SideEditorTab, Object.assign({}, props, { schedule, addToConversation, sidebarPanels, outlineSources, fileMenuItems, sessions })),
|
|
380
377
|
activeSession: () => {
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
return { sessionId, cwd: snapshot?.byId?.[sessionId]?.cwd }
|
|
378
|
+
const scope = readSessionScope(ctx)
|
|
379
|
+
if (!scope.sessionId) return undefined
|
|
380
|
+
return { sessionId: scope.sessionId, cwd: scope.cwd }
|
|
385
381
|
},
|
|
386
382
|
registerLegacyFallback: registerLegacyTab,
|
|
387
383
|
}), 'vscode-mode: sidebar editor tab')
|
|
@@ -442,4 +438,13 @@ export function apply(ctx: any): void {
|
|
|
442
438
|
order: 30,
|
|
443
439
|
label: 'VSCodeMode',
|
|
444
440
|
}, () => React.createElement(SettingsContext.Provider, { value: settings }, React.createElement(McpSettings, { openerRegistry: registry, compatSummary })))
|
|
441
|
+
|
|
442
|
+
// 卸载收尾(G4,DSH 0.1.6-alpha.2 起支持运行时卸载/重载):
|
|
443
|
+
// 注销挂在存活的 window.monaco 上的全部 Monaco provider 并复位模块状态。
|
|
444
|
+
// 不做会导致重载后重复注册(补全/跳转/hover 各翻倍)与陈旧开关残留。
|
|
445
|
+
ctx.effect(() => () => {
|
|
446
|
+
try { disposeSnippets() } catch { /* 卸载异常不得阻断其余清理 */ }
|
|
447
|
+
try { disposeAiInline() } catch { /* 同上 */ }
|
|
448
|
+
try { disposeLsp() } catch { /* 同上 */ }
|
|
449
|
+
}, 'vscode-mode: monaco providers teardown')
|
|
445
450
|
}
|
|
@@ -5,20 +5,33 @@
|
|
|
5
5
|
* setSession(id):会话切换时同步(provider 与文档同步共用)。
|
|
6
6
|
* 作者 ddj 2026-08-27
|
|
7
7
|
*/
|
|
8
|
-
import { registerLspProviders, hideReferencesOverlay } from './providers.js'
|
|
8
|
+
import { registerLspProviders, disposeLspProviders, hideReferencesOverlay } from './providers.js'
|
|
9
9
|
import { setLspSession, refreshStatus, lspStatusFor, onLspProgress, bindLspSession } from './lspClient.js'
|
|
10
10
|
|
|
11
11
|
let monacoRef = null
|
|
12
12
|
let sessionBound = false
|
|
13
|
+
/** 会话广播取消订阅函数(卸载时调用;落 window 以便跨重载清理上一代监听)。 */
|
|
14
|
+
let sessionUnbind = null
|
|
15
|
+
|
|
16
|
+
/** 会话广播订阅标记的全局挂点名(跨插件重载认领;见 disposeLsp)。 */
|
|
17
|
+
const LSP_SESSION_GLOBAL = '__edrvLspSessionUnbind__'
|
|
13
18
|
|
|
14
19
|
/** Monaco 加载后装配(幂等;重复调用仅刷新会话)。 */
|
|
15
20
|
export function setupLsp(monaco) {
|
|
16
21
|
monacoRef = monaco
|
|
17
22
|
registerLspProviders(monaco)
|
|
18
|
-
//
|
|
23
|
+
// 会话广播订阅只装一次:跨重载时上一代已订阅(取消函数落 window,模块级状态会复位),
|
|
24
|
+
// 只判 sessionBound 会重复订阅 → 同一 LSP 会话事件被处理多次。
|
|
25
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
26
|
+
if (host && typeof host[LSP_SESSION_GLOBAL] === 'function') {
|
|
27
|
+
sessionBound = true
|
|
28
|
+
sessionUnbind = host[LSP_SESSION_GLOBAL]
|
|
29
|
+
return
|
|
30
|
+
}
|
|
19
31
|
if (!sessionBound) {
|
|
20
32
|
sessionBound = true
|
|
21
|
-
bindLspSession()
|
|
33
|
+
sessionUnbind = bindLspSession()
|
|
34
|
+
if (host) host[LSP_SESSION_GLOBAL] = sessionUnbind
|
|
22
35
|
}
|
|
23
36
|
}
|
|
24
37
|
|
|
@@ -33,4 +46,21 @@ export function disposeLspOverlay() {
|
|
|
33
46
|
hideReferencesOverlay()
|
|
34
47
|
}
|
|
35
48
|
|
|
49
|
+
/**
|
|
50
|
+
* 卸载 LSP 装配(插件重载/卸载时调用):注销全部 provider、解绑会话广播并复位标记。
|
|
51
|
+
* @author ddj 2026年09月18号
|
|
52
|
+
*/
|
|
53
|
+
export function disposeLsp() {
|
|
54
|
+
disposeLspProviders()
|
|
55
|
+
const unbind = sessionUnbind
|
|
56
|
+
if (typeof unbind === 'function') {
|
|
57
|
+
try { unbind() } catch { /* 已解绑 */ }
|
|
58
|
+
}
|
|
59
|
+
sessionUnbind = null
|
|
60
|
+
sessionBound = false
|
|
61
|
+
monacoRef = null
|
|
62
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
63
|
+
if (host) delete host[LSP_SESSION_GLOBAL]
|
|
64
|
+
}
|
|
65
|
+
|
|
36
66
|
export { lspStatusFor, refreshStatus, onLspProgress }
|
|
@@ -54,7 +54,9 @@ export function setLspSession(id) {
|
|
|
54
54
|
/**
|
|
55
55
|
* 订阅窗口会话广播(幂等):EditorView 是 sessionId 的权威来源,挂载与会话切换时广播。
|
|
56
56
|
* 独立于服务订阅,避免服务形状变化时 LSP 整体失联。
|
|
57
|
-
*
|
|
57
|
+
*
|
|
58
|
+
* 返回值是可重入的取消订阅函数;调用方应保存并在卸载时调用(否则重载后会累积监听)。
|
|
59
|
+
* @author ddj 2026年09月11号 / 2026年09月18号
|
|
58
60
|
* @returns 取消订阅函数
|
|
59
61
|
*/
|
|
60
62
|
export function bindLspSession() {
|
|
@@ -23,14 +23,48 @@ const SEMANTIC_LEGEND = {
|
|
|
23
23
|
let registered = false
|
|
24
24
|
const disposables = []
|
|
25
25
|
|
|
26
|
+
/** LSP provider 注册标记的全局挂点名(跨插件重载认领;见 disposeLspProviders)。 */
|
|
27
|
+
const LSP_PROVIDERS_GLOBAL = '__edrvLspProvidersRegistered__'
|
|
28
|
+
|
|
26
29
|
/** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
|
|
27
|
-
function openAt(path,
|
|
28
|
-
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path
|
|
30
|
+
function openAt(path, target) {
|
|
31
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: Object.assign({ path }, target) }))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 解析 Monaco opener 第三参(目标位置)为 1-based 区间。
|
|
36
|
+
*
|
|
37
|
+
* 为什么要兼容多形态:Monaco 经官方桥 `registerEditorOpener` 传出的是
|
|
38
|
+
* `options.selection`(Range,含 getStartPosition)或其**降级形态**裸
|
|
39
|
+
* `{ lineNumber, column }`(官方桥在 Range 缺 endLineNumber/endColumn 时降级),
|
|
40
|
+
* 另有 `editor.action.goToLocations` 完全不传 options(第三参 undefined)。
|
|
41
|
+
* 三种形态都出现过,逐形态取值并对缺字段兜底,避免落点退化成第 1 行。
|
|
42
|
+
* @author ddj 2026年09月17号
|
|
43
|
+
* @param selectionOrPosition opener 第三参(Range / 裸位置 / undefined)
|
|
44
|
+
* @returns 1-based 目标区间(end 缺省等于 start,且保证 end >= start)
|
|
45
|
+
*/
|
|
46
|
+
export function navTargetOf(selectionOrPosition) {
|
|
47
|
+
const src = selectionOrPosition ?? {}
|
|
48
|
+
const start = typeof src.getStartPosition === 'function' ? src.getStartPosition() : src
|
|
49
|
+
const end = typeof src.getEndPosition === 'function' ? src.getEndPosition() : null
|
|
50
|
+
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
51
|
+
const column = Math.max(1, start?.column ?? 1)
|
|
52
|
+
const rawEndLine = end?.lineNumber ?? src.endLineNumber
|
|
53
|
+
const rawEndColumn = end?.column ?? src.endColumn
|
|
54
|
+
const endLine = Math.max(line, rawEndLine != null ? Math.max(1, rawEndLine) : line)
|
|
55
|
+
const endColumn = rawEndColumn != null ? Math.max(1, rawEndColumn) : undefined
|
|
56
|
+
return endColumn === undefined ? { line, column, endLine } : { line, column, endLine, endColumn }
|
|
29
57
|
}
|
|
30
58
|
|
|
31
59
|
/** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
|
|
32
60
|
export function registerLspProviders(monaco) {
|
|
33
61
|
if (registered) return
|
|
62
|
+
// 跨重载守卫:window.monaco 存活的上一代已注册过整套 provider(标记落 window)
|
|
63
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
64
|
+
if (host && host[LSP_PROVIDERS_GLOBAL]) {
|
|
65
|
+
registered = true
|
|
66
|
+
return
|
|
67
|
+
}
|
|
34
68
|
registered = true
|
|
35
69
|
|
|
36
70
|
// —— 文档跟踪:model 生命周期 → host 文档同步(含未保存编辑)——
|
|
@@ -74,10 +108,7 @@ export function registerLspProviders(monaco) {
|
|
|
74
108
|
openCodeEditor: (source, resource, selectionOrPosition) => {
|
|
75
109
|
const path = lspUriToAbs(resource)
|
|
76
110
|
if (!path) return false
|
|
77
|
-
|
|
78
|
-
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
79
|
-
const column = Math.max(1, start?.column ?? 1)
|
|
80
|
-
openAt(path, line, column)
|
|
111
|
+
openAt(path, navTargetOf(selectionOrPosition))
|
|
81
112
|
return true
|
|
82
113
|
},
|
|
83
114
|
}),
|
|
@@ -130,6 +161,29 @@ export function registerLspProviders(monaco) {
|
|
|
130
161
|
releaseDocumentSemanticTokens: () => {},
|
|
131
162
|
}),
|
|
132
163
|
)
|
|
164
|
+
// 注销器落 window:window.monaco 跨插件重载存活,模块级 registered 会复位,
|
|
165
|
+
// 只判 registered 会在重载后重复注册全部 LSP provider(见 disposeLspProviders)。
|
|
166
|
+
if (host) host[LSP_PROVIDERS_GLOBAL] = true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 卸载 LSP provider 集合:注销全部注册并复位状态(插件重载/卸载时调用)。
|
|
171
|
+
*
|
|
172
|
+
* 必须存在的原因:DSH 0.1.6-alpha.2 起支持插件运行时卸载/重载,而 `window.monaco`
|
|
173
|
+
* 由 loader 注入后**跨重载存活**;模块级 `registered`/`disposables` 却随 bundle 重新
|
|
174
|
+
* 求值清空 —— 不注销则每次重载都重复叠加一整套 provider(跳转/hover/语义高亮翻倍)。
|
|
175
|
+
* @author ddj 2026年09月18号
|
|
176
|
+
*/
|
|
177
|
+
export function disposeLspProviders() {
|
|
178
|
+
for (const dispose of disposables.splice(0)) {
|
|
179
|
+
try {
|
|
180
|
+
if (typeof dispose === 'function') dispose()
|
|
181
|
+
else if (dispose && typeof dispose.dispose === 'function') dispose.dispose()
|
|
182
|
+
} catch { /* 单个注销异常不影响其余清理 */ }
|
|
183
|
+
}
|
|
184
|
+
registered = false
|
|
185
|
+
const host = /* @__PURE__ */ (typeof window === 'undefined' ? undefined : window)
|
|
186
|
+
if (host) delete host[LSP_PROVIDERS_GLOBAL]
|
|
133
187
|
}
|
|
134
188
|
|
|
135
189
|
/** LSP SymbolInfo(host 归一化后)→ Monaco DocumentSymbol。 */
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 跳转目标高亮区间推导(纯函数,不依赖 monaco/DOM)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:Monaco 原生跳转在 resolve 出定义后会把选区 `collapseToStart` 再交给 opener,
|
|
5
|
+
* 于是插件拿到的是**零宽区间**(start === end)。零宽 range 挂装饰渲染不出任何可见
|
|
6
|
+
* 高亮(实测 Ctrl+点击枚举成员后 `.edrv-nav-target` 存在但无视觉效果)。因此必须把
|
|
7
|
+
* 零宽识别为「无区间」,并逐级回落:opener 区间 → 光标处单词 → 整行。
|
|
8
|
+
* 抽成纯函数便于单测(EditorView 是 React 组件,node 侧不可直接引入)。
|
|
9
|
+
* 作者 ddj 2026-09-17
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 推导所需的最小 model 能力(Monaco ITextModel 子集,测试可桩)。 */
|
|
13
|
+
export interface NavFlashModel {
|
|
14
|
+
getLineCount(): number
|
|
15
|
+
getLineMaxColumn(lineNumber: number): number
|
|
16
|
+
/**
|
|
17
|
+
* Monaco 实返 `{ word, startColumn, endColumn }` —— **不含** startLineNumber/endLineNumber。
|
|
18
|
+
* 类型里把行号标为可选,正是为了强制调用方用当前行补齐(曾因直接透传 undefined
|
|
19
|
+
* 行号产出非法装饰区间,装饰已挂但渲染不出任何高亮)。
|
|
20
|
+
*/
|
|
21
|
+
getWordAtPosition(pos: { lineNumber: number; column: number }):
|
|
22
|
+
| {
|
|
23
|
+
word?: string
|
|
24
|
+
startColumn: number
|
|
25
|
+
endColumn: number
|
|
26
|
+
startLineNumber?: number
|
|
27
|
+
endLineNumber?: number
|
|
28
|
+
}
|
|
29
|
+
| null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 跳转目标(1-based;endLine/endColumn 缺省表示未提供区间)。 */
|
|
33
|
+
export interface NavFlashTarget {
|
|
34
|
+
line?: number
|
|
35
|
+
column?: number
|
|
36
|
+
endLine?: number | null
|
|
37
|
+
endColumn?: number | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 高亮区间(Monaco IRange 形状,1-based)。 */
|
|
41
|
+
export interface NavFlashRange {
|
|
42
|
+
startLineNumber: number
|
|
43
|
+
startColumn: number
|
|
44
|
+
endLineNumber: number
|
|
45
|
+
endColumn: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 推导跳转目标的高亮区间。
|
|
50
|
+
*
|
|
51
|
+
* 逐级回落:① opener 提供的**非零宽**区间(原样夹到文档范围);
|
|
52
|
+
* ② 光标处单词(LSP 折叠选区的常态,高亮标识符本身最易辨认);
|
|
53
|
+
* ③ 整行(符号位置无单词可取时的兜底,保证一定可见)。
|
|
54
|
+
* @author ddj 2026年09月17号
|
|
55
|
+
* @param model 文本模型(或等价桩)
|
|
56
|
+
* @param target 跳转目标
|
|
57
|
+
* @returns 可渲染的高亮区间;model 不可用时返回 null
|
|
58
|
+
*/
|
|
59
|
+
export function navFlashRangeOf(model: NavFlashModel | null | undefined, target: NavFlashTarget | null | undefined): NavFlashRange | null {
|
|
60
|
+
if (!model || !target) return null
|
|
61
|
+
const lineCount = model.getLineCount()
|
|
62
|
+
if (!Number.isFinite(lineCount) || lineCount < 1) return null
|
|
63
|
+
const startLine = Math.max(1, Math.min(lineCount, Math.floor(target.line ?? 1) || 1))
|
|
64
|
+
const startColumn = Math.min(Math.max(1, Math.floor(target.column ?? 1) || 1), model.getLineMaxColumn(startLine))
|
|
65
|
+
|
|
66
|
+
const endLineRaw = target.endLine == null ? null : Math.floor(target.endLine)
|
|
67
|
+
const endColumnRaw = target.endColumn == null ? null : Math.floor(target.endColumn)
|
|
68
|
+
// 零宽(start === end)与缺字段一并视为「无区间」:零宽装饰不可见,必须回落
|
|
69
|
+
const hasRange = endLineRaw != null && endColumnRaw != null &&
|
|
70
|
+
(endLineRaw > startLine || endColumnRaw > startColumn)
|
|
71
|
+
if (hasRange) {
|
|
72
|
+
const endLineNumber = Math.max(startLine, Math.min(lineCount, endLineRaw))
|
|
73
|
+
const endColumn = Math.max(1, Math.min(model.getLineMaxColumn(endLineNumber), endColumnRaw))
|
|
74
|
+
return { startLineNumber: startLine, startColumn, endLineNumber, endColumn }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const word = model.getWordAtPosition({ lineNumber: startLine, column: startColumn })
|
|
78
|
+
if (word && word.endColumn > word.startColumn) {
|
|
79
|
+
// ⚠️ Monaco getWordAtPosition 不返回行号,必须用当前行补齐;直接透传 undefined
|
|
80
|
+
// 会产出 startLineNumber=undefined 的非法区间——装饰挂上了却渲染不出高亮(实测踩坑)。
|
|
81
|
+
const wordStartLine = word.startLineNumber ?? startLine
|
|
82
|
+
const wordEndLine = word.endLineNumber ?? startLine
|
|
83
|
+
return {
|
|
84
|
+
startLineNumber: wordStartLine,
|
|
85
|
+
startColumn: word.startColumn,
|
|
86
|
+
endLineNumber: wordEndLine,
|
|
87
|
+
endColumn: word.endColumn,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
startLineNumber: startLine,
|
|
92
|
+
startColumn: 1,
|
|
93
|
+
endLineNumber: startLine,
|
|
94
|
+
endColumn: model.getLineMaxColumn(startLine),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -33,6 +33,63 @@ export const OFFICIAL_FILE_TAB_ID = 'dsh-vscode-mode/file'
|
|
|
33
33
|
/** file 资源类型的 kind(与页类型 edrvEditor 区分:同 kind 只允许一份 extension)。 */
|
|
34
34
|
export const OFFICIAL_FILE_KIND = 'edrvEditorFile'
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Office 文档后缀:官方 `dsh-client-ui-sidebar-documentpreview` 的 Office 渲染器
|
|
38
|
+
* 声明 `doc/docx/xls/xlsx/ppt/pptx`,并在 alpha.2 起提供侧栏 Office 预览。
|
|
39
|
+
* 本插件让位官方(不认领),否则会把这些文件路由进 Monaco 而丢掉 Office 预览。
|
|
40
|
+
*/
|
|
41
|
+
export const OFFICE_EXT: readonly string[] = ['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'pages', 'numbers']
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 官方「不可预览二进制容器」清单(`UNVIEWABLE_BINARY_EXTENSIONS`,逐项取自
|
|
45
|
+
* `dsh-client-ui-sidebar-documentpreview/lib/client.js` 的 `UNVIEWABLE_BINARY_EXTENSIONS`)。
|
|
46
|
+
* 这些后缀官方会给出「无法预览」提示;本插件让位官方,避免把二进制当文本读成乱码。
|
|
47
|
+
*
|
|
48
|
+
* ⚠️ 刻意例外:官方清单含 `avif`,但本插件 {@link IMAGE_MIME} 支持 avif 图片预览
|
|
49
|
+
* (浏览器原生解码),故从本表**移除** `avif` —— 保留本插件的图片预览优于官方的「不可预览」。
|
|
50
|
+
* @author ddj 2026年09月18号
|
|
51
|
+
*/
|
|
52
|
+
export const BLIND_EXT: readonly string[] = [
|
|
53
|
+
// 媒体
|
|
54
|
+
'mp4', 'mov', 'avi', 'mkv', 'webm', 'flv', 'wmv', 'm4v', 'mp3', 'wav', 'flac', 'ogg', 'm4a', 'aac', 'wma', 'opus',
|
|
55
|
+
// 归档
|
|
56
|
+
'zip', 'gz', 'tgz', 'bz2', 'xz', 'zst', '7z', 'rar', 'tar', 'jar',
|
|
57
|
+
// 可执行与库
|
|
58
|
+
'exe', 'dll', 'so', 'dylib', 'bin', 'o', 'class', 'pyc', 'wasm',
|
|
59
|
+
// 字体
|
|
60
|
+
'ttf', 'otf', 'woff', 'woff2', 'eot',
|
|
61
|
+
// 磁盘映像
|
|
62
|
+
'dmg', 'iso', 'img',
|
|
63
|
+
// 数据库与设计文件
|
|
64
|
+
'sqlite', 'db', 'psd', 'ai', 'sketch', 'tiff', 'tif', 'heic', 'heif',
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
/** 让位判定用后缀集合(模块级构建一次;小写、无前导点)。 */
|
|
68
|
+
const DEFER_EXT_SET: Set<string> = new Set([...OFFICE_EXT, ...BLIND_EXT])
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 取 path 的 basename 后缀(小写、无点);无后缀/隐藏文件返回 ''。
|
|
72
|
+
* @author ddj 2026年09月18号
|
|
73
|
+
* @param path 文件路径(`/` 或 `\` 分隔均可)
|
|
74
|
+
* @returns 小写后缀或 ''
|
|
75
|
+
*/
|
|
76
|
+
function suffixOf(path: unknown): string {
|
|
77
|
+
const base = String(path ?? '').replace(/\\/g, '/').split('/').pop() ?? ''
|
|
78
|
+
const dot = base.lastIndexOf('.')
|
|
79
|
+
return dot > 0 ? base.slice(dot + 1).toLowerCase() : ''
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 该路径是否应让位官方查看器(Office 文档 + 官方不可预览的二进制容器)。
|
|
84
|
+
* 命中即不认领 `dsh-resource://file/**`,回落官方预览/提示。
|
|
85
|
+
* @author ddj 2026年09月18号
|
|
86
|
+
* @param path 文件路径(工作区相对或绝对)
|
|
87
|
+
* @returns 是否让位官方
|
|
88
|
+
*/
|
|
89
|
+
export function deferToOfficial(path: unknown): boolean {
|
|
90
|
+
return DEFER_EXT_SET.has(suffixOf(path))
|
|
91
|
+
}
|
|
92
|
+
|
|
36
93
|
/** 官方 Tab 打开参数(官方运行时不校验的 JSON,正文经 navigation.params 读回)。 */
|
|
37
94
|
export interface OfficialTabParams {
|
|
38
95
|
openPath?: string
|
|
@@ -297,7 +354,10 @@ export function installOfficial(options: {
|
|
|
297
354
|
* 注册官方 file 资源地址认领(extension 档接管 `dsh-resource://file/**`,
|
|
298
355
|
* 聊天文件链接/文件树打开改落本插件编辑器;注销后官方 textpreview 自动恢复)。
|
|
299
356
|
* 认领与否由「文件链接使用工具」设置驱动(见 fileOpeners.shouldClaimFiles)。
|
|
300
|
-
*
|
|
357
|
+
*
|
|
358
|
+
* 让位规则(DSH 0.1.6-alpha.2 起):Office 文档与官方「不可预览」二进制容器不认领,
|
|
359
|
+
* 回落官方查看器(Office 侧栏预览 / 「无法预览」提示),见 {@link deferToOfficial}。
|
|
360
|
+
* @author ddj 2026年09月09号 / 2026年09月18号
|
|
301
361
|
* @param options 装配参数(探测命中的注册表 + slots + 正文组件)
|
|
302
362
|
* @returns 卸载器(反注册类型与正文);类型注册抛错返回 null(回落官方查看器)
|
|
303
363
|
*/
|
|
@@ -314,8 +374,12 @@ export function registerOfficialFileClaim(options: {
|
|
|
314
374
|
kind: OFFICIAL_FILE_KIND,
|
|
315
375
|
patterns: [OFFICIAL_FILE_PATTERN],
|
|
316
376
|
priority: 'extension',
|
|
317
|
-
//
|
|
318
|
-
canOpen: (address: string) =>
|
|
377
|
+
// 畸形/解码失败地址否决 + Office/不可预览后缀让位官方:回落官方查看器,不产生白屏 Tab
|
|
378
|
+
canOpen: (address: string) => {
|
|
379
|
+
const parsed = parseOfficialFileAddress(address)
|
|
380
|
+
if (parsed === null) return false
|
|
381
|
+
return !deferToOfficial(parsed.path)
|
|
382
|
+
},
|
|
319
383
|
title: (address: string) => officialFileTitle(address),
|
|
320
384
|
})
|
|
321
385
|
} catch (error) {
|
package/src/client/paths.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const CACHE_KEY = {
|
|
|
28
28
|
workspaceFold: 'edrv.ws-fold.v1.',
|
|
29
29
|
/** 规则面板 UI 状态(RulesPanel v1:Tab 与项目工作区选择,全局不按会话)。 */
|
|
30
30
|
rules: 'edrv.rules.v1.',
|
|
31
|
+
/** SVN 变更面板 UI 状态(SvnPanel v1:未版本控制/忽略项显示开关,按作用域隔离)。 */
|
|
32
|
+
svn: 'edrv.svn.v1.',
|
|
31
33
|
} as const
|
|
32
34
|
|
|
33
35
|
/** 按前缀拼作用域键(scopeStore 产出作用域;侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|