dsh-vscode-mode 0.4.3 → 0.4.5

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
5
5
  "keywords": [
6
6
  "dsh",
@@ -54,6 +54,17 @@ export function emitRefresh(): void {
54
54
  window.dispatchEvent(new CustomEvent('edrv:refresh'))
55
55
  }
56
56
 
57
+ /**
58
+ * 磁盘文件变化(外部写入/删除):文件树按路径失效对应目录并强制重列。
59
+ * 与 edrv:refresh 分开:这里不触发差异记录重算与 stale 清理,只动目录缓存。
60
+ * @author ddj 2026年09月15号
61
+ * @param path 发生变化的文件路径
62
+ */
63
+ export function emitFileChanged(path: string): void {
64
+ if (!path) return
65
+ window.dispatchEvent(new CustomEvent('edrv:file-changed', { detail: { path } }))
66
+ }
67
+
57
68
  /** 打开指定路径到编辑区页签。 */
58
69
  export function emitOpenEditor(path: string): void {
59
70
  window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path } }))
@@ -107,7 +107,7 @@ export function snippetLanguageOf(path) {
107
107
 
108
108
  /**
109
109
  * 加载 Monaco Editor(AMD 构建,随插件包离线分发):注入 loader.js → require.config → editor.main。
110
- * @author ddj 2026年08月20号
110
+ * @author ddj 2026年08月20号 / 2026年09月22号
111
111
  * @returns Promise<object> window.monaco
112
112
  */
113
113
  export function loadMonaco(onProgress) {
@@ -115,13 +115,58 @@ export function loadMonaco(onProgress) {
115
115
  if (!monacoPromise) {
116
116
  publishStage('loader', MONACO_STAGES.loader.progress, MONACO_STAGES.loader.message)
117
117
  monacoPromise = new Promise((resolve, reject) => {
118
+ // 注入期间临时屏蔽全局 module/exports:Monaco loader.js 的 Environment._detect 用
119
+ // `typeof module < 'u' && !!module.exports` 判运行环境,其他插件(如 dsh-backup)注入的
120
+ // 全局 module 会让它误判为 Node 环境 → 只写 module.exports、不挂 window.require →
121
+ // 后续 window.require.config 抛 TypeError(issue #3 根因之一)。onload/onerror 后还原。
122
+ const hasModule = Object.prototype.hasOwnProperty.call(globalThis, 'module')
123
+ const hasExports = Object.prototype.hasOwnProperty.call(globalThis, 'exports')
124
+ const savedModule = globalThis.module
125
+ const savedExports = globalThis.exports
126
+ const hideNodeGlobals = () => {
127
+ try { delete globalThis.module } catch (error) { /* 只读/不可删忽略 */ }
128
+ try { delete globalThis.exports } catch (error) { /* 只读/不可删忽略 */ }
129
+ }
130
+ const restoreNodeGlobals = () => {
131
+ if (hasModule) globalThis.module = savedModule
132
+ else { try { delete globalThis.module } catch (error) { /* 只读/不可删忽略 */ } }
133
+ if (hasExports) globalThis.exports = savedExports
134
+ else { try { delete globalThis.exports } catch (error) { /* 只读/不可删忽略 */ } }
135
+ }
136
+ // 移除已注入的 loader 标签:失败/残留时防止下次命中残留分支同步 boot()(require 未就绪)
137
+ const removeLoaderTag = () => {
138
+ const existing = document.querySelector('script[data-edrv-monaco-loader]')
139
+ if (existing?.parentNode) existing.parentNode.removeChild(existing)
140
+ }
118
141
  const fail = (error) => {
119
142
  monacoPromise = null
143
+ removeLoaderTag()
144
+ restoreNodeGlobals()
120
145
  publishStage('error', MONACO_STAGES.error.progress, MONACO_STAGES.error.message)
121
146
  reject(error)
122
147
  }
148
+ const inject = () => {
149
+ const s = document.createElement('script')
150
+ s.src = MONACO_BASE + '/loader.js'
151
+ s.dataset.edrvMonacoLoader = '1'
152
+ s.onload = boot
153
+ s.onerror = (event) => {
154
+ // 保留首次失败真实原因(网络/HTTP 层事件),不让后续 TypeError 覆盖
155
+ const hint = event && event.type ? '(' + event.type + ')' : ''
156
+ fail(new Error('Monaco loader 加载失败' + hint))
157
+ }
158
+ hideNodeGlobals()
159
+ document.head.appendChild(s)
160
+ }
123
161
  const boot = () => {
162
+ restoreNodeGlobals()
124
163
  try {
164
+ // loader 已执行但未挂载 require(全局 module 污染残留/文件异常):报真实原因,
165
+ // 不再重注入——残留标签场景已在注入前分支处理,此处重试只会无限循环
166
+ if (typeof window.require !== 'function' || typeof window.require.config !== 'function') {
167
+ fail(new Error('Monaco loader 未挂载 window.require(可能被其他脚本注入的全局 module 干扰)'))
168
+ return
169
+ }
125
170
  window.require.config({ paths: { vs: MONACO_BASE } })
126
171
  publishStage('core', MONACO_STAGES.core.progress, MONACO_STAGES.core.message)
127
172
  window.require(['vs/editor/editor.main'], () => {
@@ -138,14 +183,15 @@ export function loadMonaco(onProgress) {
138
183
  }
139
184
  }
140
185
  const existing = document.querySelector('script[data-edrv-monaco-loader]')
141
- if (existing) boot()
142
- else {
143
- const s = document.createElement('script')
144
- s.src = MONACO_BASE + '/loader.js'
145
- s.dataset.edrvMonacoLoader = '1'
146
- s.onload = boot
147
- s.onerror = () => fail(new Error('Monaco loader 加载失败'))
148
- document.head.appendChild(s)
186
+ if (existing && typeof window.require === 'function' && typeof window.require.config === 'function') {
187
+ // 残留标签但 require 已就绪(上次注入已生效):直接 boot,不重复注入
188
+ boot()
189
+ } else if (existing) {
190
+ // 残留标签但 require 缺失(上次失败未清掉):移除后重新注入,避免同步 boot() 二次踩坑
191
+ removeLoaderTag()
192
+ inject()
193
+ } else {
194
+ inject()
149
195
  }
150
196
  })
151
197
  }
@@ -224,7 +224,11 @@ export function toEdrvUri(uri, root) {
224
224
  if (!uri) return ''
225
225
  if (uri.startsWith('edrv://')) return uri
226
226
  const path = relativeLspPath(uri, root)
227
- return 'edrv:///' + encodeURI(path)
227
+ // root 不匹配时 relativeLspPath 返回完整绝对路径(/home/x 或 D:/x):
228
+ // 直接拼 'edrv:///' 会得 edrv:////home/x(空 authority + // 开头)→ Uri.parse 抛
229
+ // UriError(issue #5/#6 同源隐患,LSP 定义/引用跳转工作区外文件时触发);
230
+ // 剥前导 / 后与编辑器 model URI 的相对路径约定一致(Windows 盘符形态不受影响)
231
+ return 'edrv:///' + encodeURI(String(path ?? '').replace(/^\/+/, ''))
228
232
  }
229
233
 
230
234
  /** 将 LSP 目标解析为当前工作区相对路径(跨文件跳转与 model URI 对齐)。 */
@@ -28,6 +28,8 @@ const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm
28
28
  const REVEAL_HIGHLIGHT_MS = 2000
29
29
  const REVEAL_RETRY_MAX = 6
30
30
  const REVEAL_RETRY_MS = 120
31
+ /** 外部文件变化重列去抖:一轮外部批量写入(如 agent 连写多文件)合并为一次重列。 */
32
+ const FILE_CHANGE_DEBOUNCE_MS = 400
31
33
 
32
34
  // --region 行图标(官方原语:目录文件夹图标 + 文件类型图标;缺失时回落纯文本)
33
35
 
@@ -125,6 +127,11 @@ export function FileExplorer(props) {
125
127
  const revealTryRef = React.useRef(0) // 当前定位的重试计数(行渲染需等目录加载)
126
128
  const revealInTreeRef = React.useRef(null) // 定位动作最新闭包(窗口监听读取)
127
129
  const treeRef = React.useRef(null) // 目录树容器(定位时按 data-edrv-path 查行)
130
+ const reloadDirRef = React.useRef(null) // loadDir 最新闭包(文件变化监听读取,防陈旧闭包)
131
+ const changedTimerRef = React.useRef(null) // 文件变化合并去抖计时器
132
+ const changedRelRef = React.useRef(new Set()) // 待重列的相对目录集合(去抖窗口内合并)
133
+ const dirsMapRef = React.useRef(null) // loadDir 最新闭包(文件变化监听读取,防陈旧闭包)
134
+ dirsMapRef.current = loadDir
128
135
 
129
136
  /** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
130
137
  const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(scope, rel) ?? null
@@ -261,6 +268,46 @@ export function FileExplorer(props) {
261
268
  void loadDir('', { force: true, prefetch: true })
262
269
  }
263
270
  refreshRef.current = refresh
271
+ reloadDirRef.current = loadDir
272
+
273
+ /**
274
+ * 磁盘文件变化(外部写入/删除/改名):把变化路径的父目录并入待重列集合,
275
+ * 去抖合并后对「已展开且存在」的目录强制重列(force 跳过 host 索引命中)。
276
+ * host 侧 fileVersions 已顺手失效目录树缓存,这里补上「已渲染行」的即时刷新。
277
+ * @author ddj 2026年09月15号
278
+ * @param path 发生变化的文件路径(工作区相对;绝对路径按最长已展开祖先匹配)
279
+ */
280
+ const onFileChanged = (path) => {
281
+ if (typeof path !== 'string' || !path) return
282
+ const rel = path.replace(/\\/g, '/').replace(/^\.\//, '')
283
+ const parts = rel.split('/').filter(Boolean)
284
+ if (parts.length > 1) changedRelRef.current.add(parts.slice(0, -1).join('/'))
285
+ // 绝对路径(或已在更深层目录):补上所有已展开的祖先目录
286
+ for (const dir of ancestorDirsOf(rel)) {
287
+ if (expandedRef.current[dir] === true) changedRelRef.current.add(dir)
288
+ }
289
+ if (changedTimerRef.current) return
290
+ changedTimerRef.current = window.setTimeout(() => {
291
+ changedTimerRef.current = null
292
+ const targets = [...changedRelRef.current]
293
+ changedRelRef.current.clear()
294
+ for (const dir of targets) {
295
+ if (dir !== '' && expandedRef.current[dir] !== true) continue
296
+ void reloadDirRef.current?.(dir, { force: true, prefetch: false })
297
+ }
298
+ }, FILE_CHANGE_DEBOUNCE_MS)
299
+ }
300
+
301
+ // edrv:file-changed(外部改动同步):按变化路径强制重列对应目录(去抖合并)
302
+ React.useEffect(() => {
303
+ const handler = (event) => onFileChanged(event?.detail?.path)
304
+ window.addEventListener('edrv:file-changed', handler)
305
+ return () => {
306
+ window.removeEventListener('edrv:file-changed', handler)
307
+ if (changedTimerRef.current) { clearTimeout(changedTimerRef.current); changedTimerRef.current = null }
308
+ }
309
+ // eslint-disable-next-line react-hooks/exhaustive-deps
310
+ }, [])
264
311
 
265
312
  React.useEffect(() => {
266
313
  tokensRef.current = {}