dsh-gufa-code 0.0.0-stage → 1.0.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.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -2
  3. package/client.js +11548 -0
  4. package/cordis.patch.yml +8 -0
  5. package/docs/icon-180.png +0 -0
  6. package/docs/icon.svg +18 -0
  7. package/docs/market-submission.md +32 -0
  8. package/docs/screenshot-editor.jpg +0 -0
  9. package/docs/screenshot-ide.jpg +0 -0
  10. package/host/assets.js +31 -0
  11. package/host/chat.js +523 -0
  12. package/host/context.js +438 -0
  13. package/host/dev.js +35 -0
  14. package/host/fs.js +306 -0
  15. package/host/git.js +532 -0
  16. package/host/project.js +305 -0
  17. package/host/ssh.js +54 -0
  18. package/host/terminal-pty.js +237 -0
  19. package/host/terminal.js +947 -0
  20. package/icon.svg +18 -0
  21. package/index.js +63 -0
  22. package/locale/en.json +121 -0
  23. package/locale/zh.json +121 -0
  24. package/package.json +66 -4
  25. package/scripts/build-client.mjs +38 -0
  26. package/scripts/selftest-client.mjs +457 -0
  27. package/scripts/selftest-host.mjs +197 -0
  28. package/scripts/selftest-http.mjs +284 -0
  29. package/scripts/selftest-router.mjs +101 -0
  30. package/scripts/selftest-terminal-client.mjs +314 -0
  31. package/scripts/selftest-terminal-pty.mjs +190 -0
  32. package/scripts/selftest-terminal.mjs +351 -0
  33. package/scripts/test-chat-render.mjs +702 -0
  34. package/scripts/test-chat.mjs +761 -0
  35. package/scripts/test-editor.mjs +516 -0
  36. package/scripts/verify-workspace-sessions.mjs +135 -0
  37. package/src/client/00-core.js +1686 -0
  38. package/src/client/10-icons.js +205 -0
  39. package/src/client/20-editor.js +1318 -0
  40. package/src/client/30-terminal.js +1510 -0
  41. package/src/client/40-chat.js +1184 -0
  42. package/src/client/46-gitgraph.js +278 -0
  43. package/src/client/50-panels.js +916 -0
  44. package/src/client/55-backdrop.js +508 -0
  45. package/src/client/60-dock.js +662 -0
  46. package/src/client/85-shortcuts.js +516 -0
  47. package/src/client/90-app.js +2272 -0
  48. package/src/client/99-main.js +449 -0
  49. package/vendor/monaco/LICENSE +21 -0
  50. package/vendor/monaco/ThirdPartyNotices.txt +448 -0
  51. package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  52. package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  53. package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  54. package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  55. package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  56. package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  57. package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  58. package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  59. package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  60. package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  61. package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  62. package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  63. package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  64. package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  65. package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  66. package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  67. package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  68. package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  69. package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  70. package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  71. package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  72. package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  73. package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  74. package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  75. package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  76. package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  77. package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  78. package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  79. package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  80. package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  81. package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  82. package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  83. package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  84. package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  85. package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  86. package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  87. package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  88. package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  89. package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  90. package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  91. package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  92. package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  93. package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  94. package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  95. package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  96. package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  97. package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  98. package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  99. package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  100. package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  101. package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  102. package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  103. package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  104. package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  105. package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  106. package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  107. package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  108. package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  109. package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  110. package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  111. package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  112. package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  113. package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  114. package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  115. package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  116. package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  117. package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  118. package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  119. package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  120. package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  121. package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  122. package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  123. package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  124. package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  125. package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  126. package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  127. package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  128. package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  129. package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  130. package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  131. package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  132. package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  133. package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  134. package/vendor/monaco/vs/editor/editor.main.css +8 -0
  135. package/vendor/monaco/vs/editor/editor.main.js +798 -0
  136. package/vendor/monaco/vs/language/css/cssMode.js +13 -0
  137. package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  138. package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  139. package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  140. package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  141. package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  142. package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  143. package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  144. package/vendor/monaco/vs/loader.js +11 -0
  145. package/vendor/monaco/vs/nls.messages.de.js +21 -0
  146. package/vendor/monaco/vs/nls.messages.es.js +21 -0
  147. package/vendor/monaco/vs/nls.messages.fr.js +19 -0
  148. package/vendor/monaco/vs/nls.messages.it.js +19 -0
  149. package/vendor/monaco/vs/nls.messages.ja.js +21 -0
  150. package/vendor/monaco/vs/nls.messages.ko.js +19 -0
  151. package/vendor/monaco/vs/nls.messages.ru.js +21 -0
  152. package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
  153. package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
  154. package/vendor/xterm/LICENSE +21 -0
  155. package/vendor/xterm/LICENSE.addon-fit +19 -0
  156. package/vendor/xterm/addon-fit.js +2 -0
  157. package/vendor/xterm/xterm.css +285 -0
  158. package/vendor/xterm/xterm.js +2 -0
@@ -0,0 +1,1318 @@
1
+ /**
2
+ * 20-editor:「古法编程」Monaco 多标签编辑器(Cursor / VS Code 风格)。
3
+ *
4
+ * 设计要点:
5
+ * - Monaco 走离线 AMD 资产:注入 /gufa-code/asset/monaco/loader.js →
6
+ * require.config({ paths: { vs: G.asset('monaco') } }) → require(['vs/editor/editor.main'])。
7
+ * loader Promise 是模块级单例(client factory 只执行一次),组件重挂载直接复用,
8
+ * 不会重复注入 <script>;加载成功后也不 dispose 全局 monaco 实例。
9
+ * - 每个 tab 对应一个 ITextModel(uri = file:///<root>/<path>):切换 tab 只做
10
+ * editor.setModel(model) + restoreViewState,因此撤销栈、光标与滚动位置都保留;
11
+ * tab 关闭(从 props.tabs 消失)时 dispose 对应 model。
12
+ * - 加载失败 / AMD loader 被占用 / 初始化异常时降级为 textarea(保留原占位实现),
13
+ * 顶部显示错误与「重试」按钮,不会白屏。
14
+ * - 语言服务 worker:离线资产没有标准的 `.../vs/` URL 段,Monaco 默认 AMD worker 工厂
15
+ * 反推 baseUrl 会算错,因此显式提供 MonacoEnvironment.getWorker(worker 内先加载
16
+ * loader.js、配置 paths.vs 再执行 workerMain),保证 JSON/CSS/HTML/TS 语言服务可用。
17
+ * - 只用 factory 内的 React/h/hooks/G 与 window 全局,不 import 任何 Harness Client 包。
18
+ *
19
+ * props: { tabs, activePath, theme, root, onSelectTab, onCloseTab, onChange, onSave, onCursor }
20
+ * tabs: [{ path, content, language, revision, dirty, binary, tooLarge }]
21
+ * onChange(path, content) 内容变化(不落盘;落盘由 App 的 onSave 处理)
22
+ * onSave(path) Ctrl/Cmd+S
23
+ * onCursor({ path, line, column, selectedText, lineCount })
24
+ * onSelectTab / onCloseTab 由 App 的标签栏使用;编辑器自身不渲染标签栏,接口保留以对齐设计契约。
25
+ */
26
+ ;(function installGufaEditorPane() {
27
+ 'use strict'
28
+
29
+ /** 标记 Monaco loader <script>,避免重复注入 / 失败重试时残留。 */
30
+ const LOADER_SCRIPT_ATTR = 'data-gufa-monaco-loader'
31
+
32
+ /** 模块级单例:加载成功后的 Promise;失败原因单独缓存,避免每次挂载都重新注入脚本。 */
33
+ let gufaMonacoPromise = null
34
+ let gufaMonacoFailure = null
35
+
36
+ function gufaMonacoTheme(theme) {
37
+ return theme === 'dark' || theme === 'vs-dark' ? 'vs-dark' : 'vs'
38
+ }
39
+
40
+ /* ── 皮肤动态主题:让代码区跟随「滑动变祖」的实时配色(背景/前景/选区/行号/控件) ── */
41
+
42
+ /** 解析 'rgb(r, g, b)' / 'rgba(...)' 为 [r,g,b,a]。 */
43
+ function gufaParseRgb(value) {
44
+ const match = /^rgba?\(\s*(\d+)[,\s]+(\d+)[,\s]+(\d+)(?:[,\s/]+([\d.]+))?\s*\)$/.exec(String(value ?? '').trim())
45
+ if (match === null) return null
46
+ return [Number(match[1]), Number(match[2]), Number(match[3]), match[4] === undefined ? 1 : Number(match[4])]
47
+ }
48
+
49
+ function gufaHex(color) {
50
+ const c = color ?? [0, 0, 0]
51
+ return `#${[c[0], c[1], c[2]].map((part) => Math.max(0, Math.min(255, Math.round(part))).toString(16).padStart(2, '0')).join('')}`
52
+ }
53
+
54
+ function gufaHexAlpha(color, alpha) {
55
+ const a = Math.max(0, Math.min(1, alpha))
56
+ return `${gufaHex(color)}${Math.round(a * 255).toString(16).padStart(2, '0')}`
57
+ }
58
+
59
+ let gufaSkinThemeVersion = 0
60
+
61
+ /**
62
+ * 用插件合成出的实色调色板生成 Monaco 主题(跟随滑动变祖档位实时变化)。
63
+ * @param {{bg?:string, text?:string, textDim?:string, layer?:string, layer2?:string, accent?:string, isDark?:boolean}|null} palette
64
+ * @returns {{name:string, data:object}|null}
65
+ */
66
+ function gufaSkinMonacoTheme(palette) {
67
+ if (palette === null || palette === undefined || typeof palette !== 'object') return null
68
+ const bg = gufaParseRgb(palette.bg)
69
+ const fg = gufaParseRgb(palette.text)
70
+ if (bg === null || fg === null) return null
71
+ const dim = gufaParseRgb(palette.textDim) ?? fg
72
+ const layer = gufaParseRgb(palette.layer) ?? bg
73
+ const layer2 = gufaParseRgb(palette.layer2) ?? layer
74
+ const accent = gufaParseRgb(palette.accent) ?? fg
75
+ const dark = palette.isDark !== false
76
+ gufaSkinThemeVersion += 1
77
+ const name = `gufa-skin-${dark ? 'dark' : 'light'}-${gufaSkinThemeVersion}`
78
+ const rules = [
79
+ { token: '', foreground: gufaHex(fg) },
80
+ { token: 'comment', foreground: dark ? '#8d939d' : '#79808a', fontStyle: 'italic' },
81
+ { token: 'keyword', foreground: dark ? '#c792ea' : '#9155c9' },
82
+ { token: 'keyword.control', foreground: dark ? '#c792ea' : '#9155c9' },
83
+ { token: 'string', foreground: dark ? '#a3d18b' : '#3f8f3f' },
84
+ { token: 'number', foreground: dark ? '#dfae7a' : '#a4691b' },
85
+ { token: 'constant', foreground: dark ? '#dfae7a' : '#a4691b' },
86
+ { token: 'regexp', foreground: dark ? '#7fc8d6' : '#0f7f92' },
87
+ { token: 'type', foreground: dark ? '#e6c179' : '#a8781c' },
88
+ { token: 'type.identifier', foreground: dark ? '#e6c179' : '#a8781c' },
89
+ { token: 'function', foreground: dark ? '#82c0f0' : '#2f6fbd' },
90
+ { token: 'variable', foreground: gufaHex(fg) },
91
+ { token: 'variable.predefined', foreground: dark ? '#82c0f0' : '#2f6fbd' },
92
+ { token: 'tag', foreground: dark ? '#e58f8f' : '#b03a3a' },
93
+ { token: 'attribute.name', foreground: dark ? '#e6c179' : '#a8781c' },
94
+ { token: 'attribute.value', foreground: dark ? '#a3d18b' : '#3f8f3f' },
95
+ { token: 'delimiter', foreground: gufaHex(dim) },
96
+ { token: 'operator', foreground: dark ? '#8d939d' : '#79808a' },
97
+ ]
98
+ const data = {
99
+ base: dark ? 'vs-dark' : 'vs',
100
+ inherit: true,
101
+ rules,
102
+ colors: {
103
+ 'editor.background': gufaHex(bg),
104
+ 'editor.foreground': gufaHex(fg),
105
+ 'editorLineNumber.foreground': gufaHexAlpha(dim, 0.72),
106
+ 'editorLineNumber.activeForeground': gufaHex(fg),
107
+ 'editorCursor.foreground': gufaHex(accent),
108
+ 'editor.selectionBackground': gufaHexAlpha(accent, 0.34),
109
+ 'editor.inactiveSelectionBackground': gufaHexAlpha(accent, 0.16),
110
+ 'editor.selectionHighlightBackground': gufaHexAlpha(accent, 0.18),
111
+ 'editor.wordHighlightBackground': gufaHexAlpha(accent, 0.18),
112
+ 'editor.wordHighlightStrongBackground': gufaHexAlpha(accent, 0.26),
113
+ 'editor.findMatchBackground': gufaHexAlpha(accent, 0.34),
114
+ 'editor.findMatchHighlightBackground': gufaHexAlpha(accent, 0.2),
115
+ 'editor.lineHighlightBackground': gufaHexAlpha(layer2, 0.42),
116
+ 'editor.lineHighlightBorder': '#00000000',
117
+ 'editorIndentGuide.background1': gufaHexAlpha(dim, 0.18),
118
+ 'editorIndentGuide.activeBackground1': gufaHexAlpha(dim, 0.4),
119
+ 'editorWhitespace.foreground': gufaHexAlpha(dim, 0.22),
120
+ 'editorGutter.background': gufaHex(bg),
121
+ 'editorWidget.background': gufaHex(layer),
122
+ 'editorWidget.border': gufaHexAlpha(fg, 0.12),
123
+ 'editorSuggestWidget.background': gufaHex(layer),
124
+ 'editorSuggestWidget.border': gufaHexAlpha(fg, 0.12),
125
+ 'editorSuggestWidget.selectedBackground': gufaHexAlpha(accent, 0.22),
126
+ 'editorHoverWidget.background': gufaHex(layer),
127
+ 'editorHoverWidget.border': gufaHexAlpha(fg, 0.12),
128
+ 'editorBracketMatch.background': gufaHexAlpha(accent, 0.18),
129
+ 'editorBracketMatch.border': gufaHexAlpha(accent, 0.5),
130
+ 'editorLink.activeForeground': gufaHex(accent),
131
+ 'editorInfo.foreground': gufaHex(accent),
132
+ 'editorOverviewRuler.border': '#00000000',
133
+ 'scrollbarSlider.background': gufaHexAlpha(fg, 0.16),
134
+ 'scrollbarSlider.hoverBackground': gufaHexAlpha(fg, 0.26),
135
+ 'scrollbarSlider.activeBackground': gufaHexAlpha(fg, 0.34),
136
+ 'minimap.background': gufaHex(bg),
137
+ 'diffEditor.insertedTextBackground': dark ? '#2ea04399' : '#3fb95099',
138
+ 'diffEditor.removedTextBackground': dark ? '#f8514999' : '#d1242f99',
139
+ 'diffEditor.insertedLineBackground': dark ? '#2ea04373' : '#3fb95073',
140
+ 'diffEditor.removedLineBackground': dark ? '#f8514973' : '#d1242f73',
141
+ 'diffEditorGutter.insertedLineBackground': dark ? '#3fb950cc' : '#3fb950cc',
142
+ 'diffEditorGutter.removedLineBackground': dark ? '#f85149cc' : '#d1242fcc',
143
+ 'diffEditor.insertedTextBorder': '#3fb950',
144
+ 'diffEditor.removedTextBorder': '#f85149',
145
+ 'diffEditor.diagonalFill': dark ? '#333733' : '#e4e4e4',
146
+ 'diffEditorOverview.insertedForeground': '#3fb950ee',
147
+ 'diffEditorOverview.removedForeground': '#f85149ee',
148
+ 'editorGutter.addedBackground': '#2ea043',
149
+ 'editorGutter.deletedBackground': '#f85149',
150
+ 'editorGutter.modifiedBackground': '#c19a49',
151
+ 'editorStickyScroll.background': gufaHex(bg),
152
+ 'editorStickyScrollHover.background': gufaHex(layer2),
153
+ },
154
+ }
155
+ return { name, data }
156
+ }
157
+
158
+ /** 已就绪的 monaco 实例:其他插件可能先加载过(AMD 模块与 window.monaco 是页面级全局)。 */
159
+ function gufaReadyMonaco() {
160
+ const monaco = window.monaco
161
+ if (monaco === undefined || monaco === null || monaco.editor === undefined) return null
162
+ if (typeof monaco.editor.create !== 'function') return null
163
+ return monaco
164
+ }
165
+
166
+ /** 判断 window.require 是否是 Monaco 的 AMD loader(RequireJS 等别的 loader 不能混用)。 */
167
+ function gufaIsMonacoLoader(candidate) {
168
+ return (
169
+ typeof candidate === 'function'
170
+ && typeof candidate.config === 'function'
171
+ && typeof candidate.reset === 'function'
172
+ && typeof candidate.getConfig === 'function'
173
+ )
174
+ }
175
+
176
+ /**
177
+ * Monaco loader.js 的 Environment._detect 用全局 `module.exports` 判断运行环境:
178
+ * 若页面其他脚本注入了全局 module,会被误判为 Node 环境,导致只写 module.exports
179
+ * 而不挂 window.require。注入期间临时隐藏,onload/onerror 后原样还原。
180
+ */
181
+ function gufaHideNodeGlobals() {
182
+ const saved = {
183
+ hasModule: Object.prototype.hasOwnProperty.call(globalThis, 'module'),
184
+ hasExports: Object.prototype.hasOwnProperty.call(globalThis, 'exports'),
185
+ module: globalThis.module,
186
+ exports: globalThis.exports,
187
+ }
188
+ try {
189
+ delete globalThis.module
190
+ } catch (error) {
191
+ /* 只读/不可删除的全局属性:忽略 */
192
+ }
193
+ try {
194
+ delete globalThis.exports
195
+ } catch (error) {
196
+ /* 同上 */
197
+ }
198
+ return saved
199
+ }
200
+
201
+ function gufaRestoreNodeGlobals(saved) {
202
+ if (saved.hasModule) globalThis.module = saved.module
203
+ else {
204
+ try {
205
+ delete globalThis.module
206
+ } catch (error) {
207
+ /* 忽略 */
208
+ }
209
+ }
210
+ if (saved.hasExports) globalThis.exports = saved.exports
211
+ else {
212
+ try {
213
+ delete globalThis.exports
214
+ } catch (error) {
215
+ /* 忽略 */
216
+ }
217
+ }
218
+ }
219
+
220
+ /**
221
+ * 离线资产布局是 `<assetRoot>/loader.js + <assetRoot>/editor/...`(没有标准的 `.../vs/` URL 段),
222
+ * 而 Monaco 默认 AMD worker 工厂会用 require.toUrl(...) 反推 worker 的 baseUrl,在本布局下
223
+ * 会少算两段路径导致 worker 404(JSON/CSS/HTML/TS 语言服务失效)。
224
+ * 这里显式提供 MonacoEnvironment.getWorker:worker 内先加载 loader.js、配置 paths.vs,
225
+ * 再 importScripts workerMain.js,worker 侧模块解析与主线程完全一致。
226
+ * 已有其他 Monaco 插件配置 getWorker/getWorkerUrl 时保持不变。
227
+ */
228
+ function gufaEnsureWorkerEnvironment() {
229
+ const existing = window.MonacoEnvironment
230
+ if (
231
+ existing !== undefined
232
+ && existing !== null
233
+ && (typeof existing.getWorker === 'function' || typeof existing.getWorkerUrl === 'function')
234
+ ) {
235
+ return
236
+ }
237
+ const getWorker = (moduleId, label) => gufaCreateMonacoWorker(G.asset('monaco'), label)
238
+ if (existing !== undefined && existing !== null && typeof existing === 'object') {
239
+ existing.getWorker = getWorker
240
+ return
241
+ }
242
+ window.MonacoEnvironment = { getWorker }
243
+ }
244
+
245
+ /** worker 内启动脚本:先有 AMD loader 与 paths.vs,再执行 workerMain(与默认工厂等价但路径适配本布局)。 */
246
+ function gufaWorkerScript(base) {
247
+ return [
248
+ `self.MonacoEnvironment = { baseUrl: ${JSON.stringify(`${base}/`)} };`,
249
+ `importScripts(${JSON.stringify(`${base}/loader.js`)});`,
250
+ `require.config({ paths: { vs: ${JSON.stringify(base)} } });`,
251
+ `importScripts(${JSON.stringify(`${base}/base/worker/workerMain.js`)});`,
252
+ ].join('\n')
253
+ }
254
+
255
+ function gufaCreateMonacoWorker(base, label) {
256
+ const blob = new Blob([gufaWorkerScript(base)], { type: 'text/javascript' })
257
+ const url = URL.createObjectURL(blob)
258
+ try {
259
+ return new Worker(url, { name: typeof label === 'string' && label !== '' ? label : 'gufa-monaco-worker' })
260
+ } finally {
261
+ /* Worker 已取到脚本;延迟 revoke,避免个别浏览器创建阶段就失效。 */
262
+ window.setTimeout(() => URL.revokeObjectURL(url), 10000)
263
+ }
264
+ }
265
+
266
+ /** 配置 vs 模块根路径(已有配置时沿用,避免覆盖其他 Monaco 插件正在使用的路径)。 */
267
+ function gufaConfigureLoader(loader) {
268
+ let configured = false
269
+ try {
270
+ const config = typeof loader.getConfig === 'function' ? loader.getConfig() : null
271
+ const paths = config !== null && typeof config === 'object' ? config.paths : null
272
+ if (paths !== null && typeof paths === 'object' && typeof paths.vs === 'string' && paths.vs !== '') {
273
+ configured = true
274
+ }
275
+ } catch (error) {
276
+ configured = false
277
+ }
278
+ if (!configured) loader.config({ paths: { vs: G.asset('monaco') } })
279
+ }
280
+
281
+ /**
282
+ * 加载离线 Monaco(单例)。成功后缓存 Promise;失败后缓存原因,只有显式「重试」
283
+ * (gufaResetMonacoFailure)才会再次尝试,因此重复挂载不会反复注入 loader 脚本。
284
+ * @returns {Promise<object>} window.monaco
285
+ */
286
+ function gufaLoadMonaco() {
287
+ const ready = gufaReadyMonaco()
288
+ if (ready !== null) return Promise.resolve(ready)
289
+ if (gufaMonacoPromise !== null) return gufaMonacoPromise
290
+ if (gufaMonacoFailure !== null) return Promise.reject(gufaMonacoFailure)
291
+
292
+ gufaMonacoPromise = new Promise((resolve, reject) => {
293
+ let settled = false
294
+
295
+ const fail = (error) => {
296
+ if (settled) return
297
+ settled = true
298
+ const failure = error instanceof Error ? error : new Error(String(error))
299
+ gufaMonacoPromise = null
300
+ gufaMonacoFailure = failure
301
+ reject(failure)
302
+ }
303
+
304
+ const succeed = () => {
305
+ if (settled) return
306
+ settled = true
307
+ const monaco = gufaReadyMonaco()
308
+ if (monaco === null) {
309
+ fail(new Error('Monaco 初始化失败:window.monaco 不可用'))
310
+ return
311
+ }
312
+ gufaMonacoFailure = null
313
+ resolve(monaco)
314
+ }
315
+
316
+ const boot = () => {
317
+ try {
318
+ const loader = window.require
319
+ if (!gufaIsMonacoLoader(loader)) {
320
+ fail(new Error('没有可用的 Monaco AMD loader(window.require 缺失)'))
321
+ return
322
+ }
323
+ gufaEnsureWorkerEnvironment()
324
+ gufaConfigureLoader(loader)
325
+ loader(['vs/editor/editor.main'], () => succeed(), (error) => {
326
+ fail(new Error(`Monaco 模块加载失败:${String(error)}`))
327
+ })
328
+ } catch (error) {
329
+ fail(error)
330
+ }
331
+ }
332
+
333
+ const existingLoader = window.require
334
+ if (typeof existingLoader === 'function' && !gufaIsMonacoLoader(existingLoader)) {
335
+ fail(new Error('页面已被另一个非 Monaco 的 AMD loader 占用,无法加载离线 Monaco'))
336
+ return
337
+ }
338
+ if (gufaIsMonacoLoader(existingLoader)) {
339
+ boot()
340
+ return
341
+ }
342
+
343
+ /* 没有 AMD loader:注入离线 loader.js(先清理上次失败留下的标签)。 */
344
+ const stale = document.querySelector(`script[${LOADER_SCRIPT_ATTR}]`)
345
+ if (stale !== null) stale.remove()
346
+ const savedGlobals = gufaHideNodeGlobals()
347
+ const script = document.createElement('script')
348
+ script.src = G.asset('monaco/loader.js')
349
+ script.async = true
350
+ script.setAttribute(LOADER_SCRIPT_ATTR, '1')
351
+ script.onload = () => {
352
+ gufaRestoreNodeGlobals(savedGlobals)
353
+ boot()
354
+ }
355
+ script.onerror = (event) => {
356
+ gufaRestoreNodeGlobals(savedGlobals)
357
+ script.remove()
358
+ const hint = event !== null && event !== undefined && typeof event.type === 'string' ? `(${event.type})` : ''
359
+ fail(new Error(`Monaco loader.js 加载失败${hint}:请检查静态资产 ${G.asset('monaco/loader.js')}`))
360
+ }
361
+ document.head.append(script)
362
+ })
363
+
364
+ return gufaMonacoPromise
365
+ }
366
+
367
+ /** 失败后允许「重试」:清空失败缓存并重新走加载流程(成功加载过的 monaco 不会被重复注入)。 */
368
+ function gufaResetMonacoFailure() {
369
+ gufaMonacoFailure = null
370
+ if (gufaReadyMonaco() === null) gufaMonacoPromise = null
371
+ }
372
+
373
+ /* ── model / URI ────────────────────────────────────────────────────── */
374
+
375
+ /**
376
+ * tab → Monaco model URI:file:///<root>/<path>(root 为项目绝对路径)。
377
+ * @param {object} monaco Monaco 命名空间
378
+ * @param {string} root 项目绝对路径(例:/Users/me/project)
379
+ * @param {string} path 项目内相对路径(例:src/a.ts)
380
+ */
381
+ function gufaModelUri(monaco, root, path) {
382
+ const normalizedRoot = String(root ?? '').replace(/\\/g, '/').replace(/\/+$/, '')
383
+ const relativePath = String(path ?? '').replace(/\\/g, '/').replace(/^\/+/, '')
384
+ const absolute = normalizedRoot === '' ? `/${relativePath}` : `${normalizedRoot}/${relativePath}`
385
+ try {
386
+ if (monaco.Uri !== undefined && monaco.Uri !== null && typeof monaco.Uri.file === 'function') {
387
+ return monaco.Uri.file(absolute)
388
+ }
389
+ return monaco.Uri.parse(`file://${absolute.startsWith('/') ? '' : '/'}${absolute}`)
390
+ } catch (error) {
391
+ /* 极端路径(非法字符等)兜底:仍然给出唯一 file:// uri,避免整个编辑器不可用 */
392
+ return monaco.Uri.parse(`file:///${relativePath}`)
393
+ }
394
+ }
395
+
396
+ /** 只把 Monaco 已注册的 language id 交给 createModel(Host 的扩展名表里有 makefile/cmake 等 Monaco 没有的 id)。 */
397
+ function gufaResolveLanguage(monaco, language) {
398
+ if (typeof language !== 'string' || language === '') return undefined
399
+ try {
400
+ const languages = typeof monaco.languages?.getLanguages === 'function' ? monaco.languages.getLanguages() : null
401
+ if (Array.isArray(languages) && languages.length > 0 && !languages.some((item) => item.id === language)) return undefined
402
+ } catch (error) {
403
+ return language
404
+ }
405
+ return language
406
+ }
407
+
408
+ /** 取(或创建)tab 对应的 model。已存在的同名 model 直接复用,避免重复注册 URI。 */
409
+ function gufaCreateModel(monaco, root, tab, content) {
410
+ const uri = gufaModelUri(monaco, root, tab.path)
411
+ const language = gufaResolveLanguage(monaco, tab.language)
412
+ const existing = typeof monaco.editor.getModel === 'function' ? monaco.editor.getModel(uri) : null
413
+ if (existing !== null && existing !== undefined) return existing
414
+ const model = monaco.editor.createModel(content, language, uri)
415
+ try {
416
+ // 按语言给默认缩进(Go/Java 等 4 空格、Makefile 用 Tab),已有文件由 detectIndentation 自动跟随
417
+ if (typeof G.indentForLanguage === 'function') model.updateOptions(G.indentForLanguage(language))
418
+ } catch {
419
+ /* ignore */
420
+ }
421
+ return model
422
+ }
423
+
424
+ /** Monaco 编辑器选项:关闭 minimap / overview ruler,automaticLayout 跟随容器尺寸。 */
425
+ function gufaEditorOptions(theme) {
426
+ return {
427
+ theme: gufaMonacoTheme(theme),
428
+ automaticLayout: true,
429
+ minimap: { enabled: false },
430
+ renderOverviewRuler: false,
431
+ overviewRulerLanes: 0,
432
+ overviewRulerBorder: false,
433
+ hideCursorInOverviewRuler: true,
434
+ scrollBeyondLastLine: false,
435
+ smoothScrolling: true,
436
+ cursorBlinking: 'smooth',
437
+ fontSize: 13,
438
+ lineHeight: 20,
439
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
440
+ autoIndent: 'full',
441
+ detectIndentation: true,
442
+ tabSize: 2,
443
+ insertSpaces: true,
444
+ wordWrap: 'off',
445
+ renderWhitespace: 'selection',
446
+ fixedOverflowWidgets: true,
447
+ padding: { top: 8, bottom: 8 },
448
+ scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10, useShadows: false },
449
+ mouseWheelZoom: false,
450
+ contextmenu: true,
451
+ }
452
+ }
453
+
454
+ /** 文本插入后的光标位置(lineColumn 语义与 Monaco 一致:lineNumber 从 1、column 从 1)。 */
455
+ function gufaAdvancePosition(position, text) {
456
+ const lines = String(text ?? '').split('\n')
457
+ if (lines.length <= 1) {
458
+ return { lineNumber: position.lineNumber, column: position.column + (lines[0] ?? '').length }
459
+ }
460
+ return {
461
+ lineNumber: position.lineNumber + lines.length - 1,
462
+ column: (lines[lines.length - 1] ?? '').length + 1,
463
+ }
464
+ }
465
+
466
+ /* ── 组件 ───────────────────────────────────────────────────────────── */
467
+
468
+ G.EditorPane = function EditorPane(props) {
469
+ const tabs = useMemo(() => (Array.isArray(props.tabs) ? props.tabs : []), [props.tabs])
470
+ const activePath = typeof props.activePath === 'string' ? props.activePath : null
471
+ const root = typeof props.root === 'string' ? props.root : ''
472
+ const theme = gufaMonacoTheme(props.theme)
473
+ const palette = props.palette !== null && props.palette !== undefined && typeof props.palette === 'object' ? props.palette : null
474
+ const paletteKey = palette === null
475
+ ? ''
476
+ : [palette.bg, palette.text, palette.textDim, palette.layer, palette.layer2, palette.accent, palette.isDark].join('|')
477
+
478
+ const activeTab = useMemo(() => {
479
+ if (activePath === null) return null
480
+ return tabs.find((tab) => tab !== null && typeof tab === 'object' && tab.path === activePath) ?? null
481
+ }, [tabs, activePath])
482
+ const activeIsDiff = activeTab !== null && activeTab.kind === 'diff'
483
+ const activeDiffKey = activeIsDiff ? String(activeTab.diffKey ?? '') : ''
484
+ const editableActiveTab =
485
+ activeTab !== null && activeTab.kind !== 'diff' && activeTab.binary !== true && activeTab.tooLarge !== true ? activeTab : null
486
+
487
+ const rootRef = useRef(null) /* 外层容器(键盘保存监听挂这里) */
488
+ const hostRef = useRef(null) /* Monaco 挂载点 */
489
+ const textareaRef = useRef(null) /* 降级 textarea */
490
+ const editorRef = useRef(null)
491
+ const diffHostRef = useRef(null)
492
+ const diffEditorRef = useRef(null)
493
+ const diffModelsRef = useRef([])
494
+ const diffSignatureRef = useRef(null)
495
+ const monacoRef = useRef(null)
496
+ const modelsRef = useRef(new Map()) /* path -> ITextModel */
497
+ const viewStatesRef = useRef(new Map()) /* path -> editor view state(滚动/光标) */
498
+ const lastPropContentRef = useRef(new Map()) /* path -> 最近一次同步过的 props content */
499
+ const modelsRootRef = useRef(root) /* model 所属项目根:root 变了必须重建 */
500
+ const modelPathRef = useRef(null) /* 当前挂在 editor 上的 tab path */
501
+ const suppressChangeRef = useRef(false) /* 程序化 setValue 时抑制 onChange 回环 */
502
+ const propsRef = useRef(props)
503
+ const activePathRef = useRef(activePath)
504
+ const statusRef = useRef('loading')
505
+
506
+ /** idle → loading → ready / error */
507
+ const [status, setStatus] = useState('loading')
508
+ const [monacoError, setMonacoError] = useState('')
509
+ const [attempt, setAttempt] = useState(0)
510
+
511
+ useEffect(() => {
512
+ propsRef.current = props
513
+ }, [props])
514
+ useEffect(() => {
515
+ activePathRef.current = activePath
516
+ }, [activePath])
517
+ useEffect(() => {
518
+ statusRef.current = status
519
+ }, [status])
520
+
521
+ const disposeModelAt = useCallback((path) => {
522
+ const model = modelsRef.current.get(path) ?? null
523
+ modelsRef.current.delete(path)
524
+ viewStatesRef.current.delete(path)
525
+ lastPropContentRef.current.delete(path)
526
+ if (model !== null && model !== undefined && typeof model.isDisposed === 'function' && model.isDisposed() !== true) {
527
+ try {
528
+ model.dispose()
529
+ } catch (error) {
530
+ console.error('[gufa-code] dispose model failed', error)
531
+ }
532
+ }
533
+ }, [])
534
+
535
+ const disposeAllModels = useCallback(() => {
536
+ for (const path of [...modelsRef.current.keys()]) disposeModelAt(path)
537
+ }, [disposeModelAt])
538
+
539
+ const reportCursor = useCallback(() => {
540
+ const editor = editorRef.current
541
+ const monaco = monacoRef.current
542
+ const path = modelPathRef.current
543
+ if (editor === null || monaco === null || path === null) return
544
+ const model = editor.getModel()
545
+ if (model === null || model === undefined) return
546
+ const selection = editor.getSelection()
547
+ const position = selection !== null && selection !== undefined ? selection.getPosition() : editor.getPosition()
548
+ if (position === null || position === undefined) return
549
+ const hasSelection = selection !== null && selection !== undefined && !selection.isEmpty()
550
+ const selectedText = hasSelection ? model.getValueInRange(selection) : ''
551
+ const onCursor = propsRef.current.onCursor
552
+ if (typeof onCursor === 'function') {
553
+ onCursor({
554
+ path,
555
+ line: position.lineNumber,
556
+ column: position.column,
557
+ selectedText,
558
+ lineCount: model.getLineCount(),
559
+ })
560
+ }
561
+ }, [])
562
+
563
+ /** 降级 textarea 的光标/选区计算(沿用占位实现的口径:line 从 1、column 从 1)。 */
564
+ const reportFallbackCursor = useCallback(() => {
565
+ const node = textareaRef.current
566
+ const path = activePathRef.current
567
+ if (node === null || path === null) return
568
+ const value = typeof node.value === 'string' ? node.value : ''
569
+ const start = typeof node.selectionStart === 'number' ? node.selectionStart : 0
570
+ const end = typeof node.selectionEnd === 'number' ? node.selectionEnd : start
571
+ const before = value.slice(0, start)
572
+ const line = before.split('\n').length
573
+ const column = start - before.lastIndexOf('\n')
574
+ const onCursor = propsRef.current.onCursor
575
+ if (typeof onCursor === 'function') {
576
+ onCursor({
577
+ path,
578
+ line,
579
+ column,
580
+ selectedText: start !== end ? value.slice(start, end) : '',
581
+ lineCount: value.split('\n').length,
582
+ })
583
+ }
584
+ }, [])
585
+
586
+ /** 在当前光标/选区插入文本(G.bus 'editor:insert',AI 生成代码「插入到编辑器」用)。 */
587
+ const insertText = useCallback((rawText) => {
588
+ const text = typeof rawText === 'string' ? rawText : ''
589
+ if (text === '') return
590
+ const editor = statusRef.current === 'ready' ? editorRef.current : null
591
+ const monaco = monacoRef.current
592
+ if (editor !== null && monaco !== null) {
593
+ const model = editor.getModel()
594
+ if (model === null || model === undefined) return
595
+ try {
596
+ const position = editor.getPosition() ?? { lineNumber: 1, column: 1 }
597
+ const selection = editor.getSelection()
598
+ const range = selection !== null && selection !== undefined
599
+ ? selection
600
+ : new monaco.Range(position.lineNumber, position.column, position.lineNumber, position.column)
601
+ const startPosition = range.getStartPosition()
602
+ editor.pushUndoStop()
603
+ editor.executeEdits('gufa-editor-insert', [{ range, text, forceMoveMarkers: true }])
604
+ editor.pushUndoStop()
605
+ editor.setPosition(gufaAdvancePosition(startPosition, text))
606
+ editor.revealPositionInCenterIfOutsideViewport(gufaAdvancePosition(startPosition, text))
607
+ editor.focus()
608
+ } catch (error) {
609
+ console.error('[gufa-code] 插入代码失败', error)
610
+ }
611
+ return
612
+ }
613
+ /* 降级 textarea:按当前 selection 插入,然后调 onChange(等同于用户输入)。 */
614
+ const node = textareaRef.current
615
+ const path = activePathRef.current
616
+ if (node === null || path === null) return
617
+ const value = typeof node.value === 'string' ? node.value : ''
618
+ const start = typeof node.selectionStart === 'number' ? node.selectionStart : value.length
619
+ const end = typeof node.selectionEnd === 'number' ? node.selectionEnd : start
620
+ const next = `${value.slice(0, start)}${text}${value.slice(end)}`
621
+ const onChange = propsRef.current.onChange
622
+ if (typeof onChange === 'function') onChange(path, next)
623
+ const caret = start + text.length
624
+ window.requestAnimationFrame(() => {
625
+ const current = textareaRef.current
626
+ if (current === null) return
627
+ current.focus()
628
+ try {
629
+ current.selectionStart = caret
630
+ current.selectionEnd = caret
631
+ } catch (error) {
632
+ /* 忽略 */
633
+ }
634
+ })
635
+ }, [])
636
+
637
+ /** 失败后重试加载 Monaco。 */
638
+ const retryMonaco = useCallback(() => {
639
+ gufaResetMonacoFailure()
640
+ setMonacoError('')
641
+ setAttempt((value) => value + 1)
642
+ }, [])
643
+
644
+ /* ── 加载 Monaco 并创建 editor(每个 attempt 一次) ─────────────── */
645
+ useEffect(() => {
646
+ const host = hostRef.current
647
+ if (host === null) return undefined
648
+ let cancelled = false
649
+ let editor = null
650
+ const subscriptions = []
651
+
652
+ setStatus('loading')
653
+ setMonacoError('')
654
+
655
+ const disposeEditor = () => {
656
+ for (const subscription of subscriptions.splice(0)) {
657
+ try {
658
+ subscription.dispose()
659
+ } catch (error) {
660
+ /* 忽略 */
661
+ }
662
+ }
663
+ if (editor !== null) {
664
+ const current = editor
665
+ editor = null
666
+ try {
667
+ current.dispose()
668
+ } catch (error) {
669
+ /* 忽略 */
670
+ }
671
+ }
672
+ editorRef.current = null
673
+ modelPathRef.current = null
674
+ }
675
+
676
+ gufaLoadMonaco()
677
+ .then((monaco) => {
678
+ if (cancelled) return
679
+ try {
680
+ const instance = monaco.editor.create(host, gufaEditorOptions(propsRef.current.theme))
681
+ editor = instance
682
+ editorRef.current = instance
683
+ try {
684
+ try {
685
+ G.__gufaEditorController = {
686
+ comment: () => {
687
+ try {
688
+ instance.trigger('gufa-shortcut', 'editor.action.commentLine', null)
689
+ return true
690
+ } catch {
691
+ return false
692
+ }
693
+ },
694
+ hasEditor: true,
695
+ }
696
+ if (typeof instance.onDidFocusEditorText === 'function') instance.onDidFocusEditorText(() => {
697
+ try {
698
+ G.bus.emit('editor:focus', true)
699
+ } catch {
700
+ /* ignore */
701
+ }
702
+ })
703
+ if (typeof instance.onDidBlurEditorText === 'function') instance.onDidBlurEditorText(() => {
704
+ try {
705
+ G.bus.emit('editor:focus', false)
706
+ } catch {
707
+ /* ignore */
708
+ }
709
+ })
710
+ if (typeof instance.onMouseDown === 'function') instance.onMouseDown(() => {
711
+ try {
712
+ G.bus.emit('editor:focus', true)
713
+ } catch {
714
+ /* ignore */
715
+ }
716
+ })
717
+ } catch {
718
+ /* ignore */
719
+ }
720
+ G.__gufaEditorDebug = {
721
+ focus: () => {
722
+ try {
723
+ instance.focus()
724
+ } catch {
725
+ /* ignore */
726
+ }
727
+ },
728
+ hasTextFocus: () => {
729
+ try {
730
+ return typeof instance.hasTextFocus === 'function' ? instance.hasTextFocus() === true : false
731
+ } catch {
732
+ return false
733
+ }
734
+ },
735
+ setSelectionAll: () => {
736
+ try {
737
+ instance.trigger('gufa-debug', 'editor.action.selectAll', null)
738
+ } catch {
739
+ /* ignore */
740
+ }
741
+ },
742
+ }
743
+ } catch {
744
+ /* ignore */
745
+ }
746
+ monacoRef.current = monaco
747
+ /* Alt+↑/↓ 移动行之后,按语言缩进规则重排当前行(移入大括号自动缩进) */
748
+ const reindentCurrent = () => {
749
+ if (instance === null || instance === undefined) return
750
+ try {
751
+ instance.trigger('gufa-reindent', 'editor.action.reindentselectedlines', null)
752
+ } catch {
753
+ /* ignore */
754
+ }
755
+ }
756
+ const onReindentKey = (event) => {
757
+ if (event.altKey !== true || event.metaKey === true || event.ctrlKey === true) return
758
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') return
759
+ window.setTimeout(reindentCurrent, 0)
760
+ }
761
+ host.addEventListener('keydown', onReindentKey, true)
762
+ subscriptions.push({ dispose: () => host.removeEventListener('keydown', onReindentKey, true) })
763
+ /* 内容变化:记录最新内容(供 props 回环判定)并上报 App。 */
764
+ subscriptions.push(instance.onDidChangeModelContent(() => {
765
+ if (suppressChangeRef.current) return
766
+ const model = instance.getModel()
767
+ const path = modelPathRef.current
768
+ if (model === null || model === undefined || path === null) return
769
+ const value = model.getValue()
770
+ lastPropContentRef.current.set(path, value)
771
+ const onChange = propsRef.current.onChange
772
+ if (typeof onChange === 'function') onChange(path, value)
773
+ }))
774
+ /* 光标 / 选区变化上报(状态栏行号列、AI 上下文选区)。 */
775
+ subscriptions.push(instance.onDidChangeCursorSelection(() => reportCursor()))
776
+ /* 语言服务(JS/TS/JSON/CSS/HTML)诊断 → 资源管理器 / 标签页 / 状态栏 */
777
+ const publishProblems = () => {
778
+ if (typeof G.problemsStore?.set !== 'function') return
779
+ const next = {}
780
+ try {
781
+ for (const [path, model] of modelsRef.current) {
782
+ if (model === null || model === undefined) continue
783
+ let markers = []
784
+ try {
785
+ markers = monaco.editor.getModelMarkers({ resource: model.uri })
786
+ } catch {
787
+ continue
788
+ }
789
+ let errors = 0
790
+ let warnings = 0
791
+ const errorLevel = monaco.MarkerSeverity?.Error ?? 8
792
+ const warningLevel = monaco.MarkerSeverity?.Warning ?? 4
793
+ for (const marker of markers) {
794
+ if (marker.severity === errorLevel) errors += 1
795
+ else if (marker.severity === warningLevel) warnings += 1
796
+ }
797
+ if (errors > 0 || warnings > 0) next[path] = { errors, warnings }
798
+ }
799
+ } catch {
800
+ /* ignore */
801
+ }
802
+ G.problemsStore.set(next)
803
+ }
804
+ try {
805
+ subscriptions.push(monaco.editor.onDidChangeMarkers(() => publishProblems()))
806
+ } catch {
807
+ /* ignore */
808
+ }
809
+ publishProblems()
810
+ setStatus('ready')
811
+ } catch (error) {
812
+ disposeEditor()
813
+ if (cancelled) return
814
+ setStatus('error')
815
+ setMonacoError(G.errorText(error))
816
+ }
817
+ })
818
+ .catch((error) => {
819
+ if (cancelled) return
820
+ setStatus('error')
821
+ setMonacoError(G.errorText(error))
822
+ })
823
+
824
+ return () => {
825
+ cancelled = true
826
+ disposeEditor()
827
+ /* 组件卸载:释放本组件创建的全部 model(Monaco 全局实例保留,供其他面板复用)。 */
828
+ disposeAllModels()
829
+ }
830
+ }, [attempt, reportCursor, disposeAllModels])
831
+
832
+ /* ── 同步 tabs ↔ models,并切换当前 model ──────────────────────── */
833
+ useEffect(() => {
834
+ if (status !== 'ready') return
835
+ const editor = editorRef.current
836
+ const monaco = monacoRef.current
837
+ if (editor === null || monaco === null) return
838
+
839
+ const editableTabs = []
840
+ for (const tab of tabs) {
841
+ if (tab === null || typeof tab !== 'object' || typeof tab.path !== 'string') continue
842
+ if (tab.binary === true || tab.tooLarge === true) continue
843
+ editableTabs.push(tab)
844
+ }
845
+ const editablePaths = new Set(editableTabs.map((tab) => tab.path))
846
+
847
+ /* 项目根变了:旧 model 的 file:// uri 失效,全部重建。 */
848
+ if (modelsRootRef.current !== root) {
849
+ try {
850
+ editor.setModel(null)
851
+ } catch (error) {
852
+ /* 忽略 */
853
+ }
854
+ modelPathRef.current = null
855
+ disposeAllModels()
856
+ modelsRootRef.current = root
857
+ }
858
+
859
+ /* 1) 已关闭的 tab:先 detach 再 dispose model(撤销栈随 model 释放)。 */
860
+ for (const path of [...modelsRef.current.keys()]) {
861
+ if (editablePaths.has(path)) continue
862
+ const model = modelsRef.current.get(path) ?? null
863
+ if (editor.getModel() === model) {
864
+ try {
865
+ editor.setModel(null)
866
+ } catch (error) {
867
+ /* 忽略 */
868
+ }
869
+ modelPathRef.current = null
870
+ }
871
+ disposeModelAt(path)
872
+ }
873
+
874
+ /* 2) 新建 / 同步 model(内容被外部修改时兜底 setValue,正常输入不会走到这里)。 */
875
+ for (const tab of editableTabs) {
876
+ const content = typeof tab.content === 'string' ? tab.content : ''
877
+ let model = modelsRef.current.get(tab.path) ?? null
878
+ if (model !== null && typeof model.isDisposed === 'function' && model.isDisposed()) model = null
879
+ if (model === null) {
880
+ try {
881
+ model = gufaCreateModel(monaco, root, tab, content)
882
+ } catch (error) {
883
+ setStatus('error')
884
+ setMonacoError(G.errorText(error))
885
+ return
886
+ }
887
+ modelsRef.current.set(tab.path, model)
888
+ lastPropContentRef.current.set(tab.path, content)
889
+ continue
890
+ }
891
+ if (lastPropContentRef.current.get(tab.path) !== content) {
892
+ lastPropContentRef.current.set(tab.path, content)
893
+ if (model.getValue() !== content) {
894
+ suppressChangeRef.current = true
895
+ try {
896
+ model.setValue(content)
897
+ } finally {
898
+ suppressChangeRef.current = false
899
+ }
900
+ }
901
+ }
902
+ }
903
+
904
+ /* 3) 当前 tab → 同一个 editor 实例换 model:撤销栈在 model 上,滚动/光标用 viewState 恢复。 */
905
+ const targetTab = editablePaths.has(activePath)
906
+ ? editableTabs.find((tab) => tab.path === activePath) ?? null
907
+ : null
908
+ const targetModel = targetTab === null ? null : modelsRef.current.get(targetTab.path) ?? null
909
+ if (editor.getModel() !== targetModel) {
910
+ const previousPath = modelPathRef.current
911
+ const previousModel = editor.getModel()
912
+ if (previousPath !== null && previousModel !== null && previousModel !== undefined) {
913
+ try {
914
+ const viewState = editor.saveViewState()
915
+ if (viewState !== null && viewState !== undefined) viewStatesRef.current.set(previousPath, viewState)
916
+ } catch (error) {
917
+ /* 忽略 */
918
+ }
919
+ }
920
+ modelPathRef.current = targetTab === null ? null : targetTab.path
921
+ editor.setModel(targetModel)
922
+ if (targetModel !== null && targetTab !== null) {
923
+ const viewState = viewStatesRef.current.get(targetTab.path)
924
+ if (viewState !== null && viewState !== undefined) {
925
+ try {
926
+ editor.restoreViewState(viewState)
927
+ } catch (error) {
928
+ /* 忽略 */
929
+ }
930
+ }
931
+ reportCursor()
932
+ /* 打开/切换文件不抢焦点:只有用户点击编辑区时才进入键盘编辑 */
933
+ }
934
+ } else if (targetTab !== null) {
935
+ modelPathRef.current = targetTab.path
936
+ }
937
+ }, [status, tabs, activePath, root, reportCursor, disposeAllModels, disposeModelAt])
938
+
939
+ /* ── Git diff 视图:Monaco DiffEditor(未暂存的一侧可编辑并保存) ── */
940
+ useEffect(() => {
941
+ const disposeDiff = () => {
942
+ if (diffEditorRef.current !== null) {
943
+ try {
944
+ diffEditorRef.current.dispose()
945
+ } catch {
946
+ /* ignore */
947
+ }
948
+ diffEditorRef.current = null
949
+ }
950
+ for (const model of diffModelsRef.current) {
951
+ try {
952
+ model.dispose()
953
+ } catch {
954
+ /* ignore */
955
+ }
956
+ }
957
+ diffModelsRef.current = []
958
+ diffSignatureRef.current = null
959
+ }
960
+ if (status !== 'ready' || activeIsDiff !== true) {
961
+ disposeDiff()
962
+ return undefined
963
+ }
964
+ const monaco = monacoRef.current
965
+ const host = diffHostRef.current
966
+ const tab = activeTab
967
+ if (monaco === null || host === null || tab === null || tab.diff === null || tab.diff === undefined) return undefined
968
+ monacoRef.current = monaco
969
+ const signature = `${tab.path}|${tab.diff.staged === true ? 'staged' : 'work'}|${activeDiffKey}|${String(tab.diff.left ?? '').length}|${paletteKey}`
970
+ if (diffSignatureRef.current === signature && diffEditorRef.current !== null) return undefined
971
+ disposeDiff()
972
+ let skin = null
973
+ try {
974
+ skin = gufaSkinMonacoTheme(palette)
975
+ if (skin !== null) monaco.editor.defineTheme(skin.name, skin.data)
976
+ } catch {
977
+ skin = null
978
+ }
979
+ const language = typeof tab.language === 'string' && tab.language !== '' ? tab.language : 'plaintext'
980
+ const key = encodeURIComponent(String(tab.path))
981
+ const suffix = String(Date.now())
982
+ const leftText = tab.diff.compare === true && typeof tab.compareLeft === 'string' ? tab.compareLeft : tab.diff.left
983
+ const originalModel = monaco.editor.createModel(String(leftText ?? ''), language, monaco.Uri.parse(`gufa-diff://original/${key}/${suffix}`))
984
+ const modifiedModel = monaco.editor.createModel(String(tab.content ?? ''), language, monaco.Uri.parse(`gufa-diff://modified/${key}/${suffix}`))
985
+ let instance
986
+ try {
987
+ instance = monaco.editor.createDiffEditor(host, {
988
+ theme: skin === null ? gufaMonacoTheme(props.theme) : skin.name,
989
+ readOnly: tab.diff.editable !== true,
990
+ originalEditable: tab.diff.compare === true,
991
+ automaticLayout: true,
992
+ renderSideBySide: true,
993
+ renderIndicators: true,
994
+ renderMarginRevertIcon: true,
995
+ useInlineViewWhenSpaceIsLimited: true,
996
+ ignoreTrimWhitespace: false,
997
+ minimap: { enabled: false },
998
+ scrollBeyondLastLine: false,
999
+ lineNumbers: 'on',
1000
+ renderOverviewRuler: false,
1001
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace',
1002
+ fontSize: 12.5,
1003
+ lineHeight: 1.15,
1004
+ scrollbar: { verticalScrollbarSize: 10, horizontalScrollbarSize: 10 },
1005
+ })
1006
+ instance.setModel({ original: originalModel, modified: modifiedModel })
1007
+ } catch (error) {
1008
+ try {
1009
+ originalModel.dispose()
1010
+ modifiedModel.dispose()
1011
+ } catch {
1012
+ /* ignore */
1013
+ }
1014
+ return undefined
1015
+ }
1016
+ diffEditorRef.current = instance
1017
+ diffModelsRef.current = [originalModel, modifiedModel]
1018
+ diffSignatureRef.current = signature
1019
+ // 自动把视图滚到第一处差异,避免「看起来没对比出来」
1020
+ const revealFirstDiff = () => {
1021
+ try {
1022
+ if (typeof instance.getLineChanges !== 'function') return
1023
+ const changes = instance.getLineChanges()
1024
+ if (changes !== null && changes !== undefined && changes.length > 0) instance.getModifiedEditor().revealLineInCenter(changes[0].modifiedStartLineNumber)
1025
+ } catch {
1026
+ /* ignore */
1027
+ }
1028
+ }
1029
+ try {
1030
+ if (typeof instance.onDidUpdateDiff === 'function') instance.onDidUpdateDiff(revealFirstDiff)
1031
+ } catch {
1032
+ /* ignore */
1033
+ }
1034
+ revealFirstDiff()
1035
+ const subOriginal = originalModel.onDidChangeContent(() => {
1036
+ if (tab.diff.compare !== true) return
1037
+ if (suppressChangeRef.current) return
1038
+ try {
1039
+ props.onDiffChange?.(tab.path, 'left', originalModel.getValue())
1040
+ } catch {
1041
+ /* ignore */
1042
+ }
1043
+ })
1044
+ const sub = modifiedModel.onDidChangeContent(() => {
1045
+ if (suppressChangeRef.current) return
1046
+ if (tab.diff.editable !== true) return
1047
+ try {
1048
+ if (tab.diff.compare === true) props.onDiffChange?.(tab.path, 'right', modifiedModel.getValue())
1049
+ else props.onChange?.(tab.path, modifiedModel.getValue())
1050
+ } catch {
1051
+ /* ignore */
1052
+ }
1053
+ })
1054
+ try {
1055
+ G.__gufaEditorController = {
1056
+ comment: () => {
1057
+ try {
1058
+ instance.getModifiedEditor().trigger('gufa-shortcut', 'editor.action.commentLine', null)
1059
+ return true
1060
+ } catch {
1061
+ return false
1062
+ }
1063
+ },
1064
+ hasEditor: true,
1065
+ }
1066
+ } catch {
1067
+ /* ignore */
1068
+ }
1069
+ return () => {
1070
+ try {
1071
+ sub.dispose()
1072
+ } catch {
1073
+ /* ignore */
1074
+ }
1075
+ try {
1076
+ subOriginal.dispose()
1077
+ } catch {
1078
+ /* ignore */
1079
+ }
1080
+ disposeDiff()
1081
+ }
1082
+ }, [status, activeIsDiff, activePath, activeDiffKey, paletteKey, theme])
1083
+
1084
+ /* ── 主题跟随 App / 滑动变祖配色(即时切换,不需要重建 editor) ──── */
1085
+ useEffect(() => {
1086
+ if (status !== 'ready') return
1087
+ const monaco = monacoRef.current
1088
+ if (monaco === null || monaco.editor === undefined) return
1089
+ try {
1090
+ const skin = gufaSkinMonacoTheme(palette)
1091
+ if (skin !== null) {
1092
+ monaco.editor.defineTheme(skin.name, skin.data)
1093
+ monaco.editor.setTheme(skin.name)
1094
+ } else {
1095
+ monaco.editor.setTheme(theme)
1096
+ }
1097
+ } catch (error) {
1098
+ /* 主题失败不阻塞编辑 */
1099
+ }
1100
+ }, [status, theme, paletteKey])
1101
+
1102
+ /* ── 外部触发的编辑器命令(快捷键层:注释、多光标等) ─────────────── */
1103
+ useEffect(() => {
1104
+ if (G.bus === null || G.bus === undefined) return undefined
1105
+ const off = G.bus.on('editor:command', (commandId) => {
1106
+ if (typeof commandId !== 'string' || commandId === '') return
1107
+ try {
1108
+ if (window.__gufaEditorCmdLogs === undefined) window.__gufaEditorCmdLogs = 20
1109
+ if (window.__gufaEditorCmdLogs > 0) {
1110
+ window.__gufaEditorCmdLogs -= 1
1111
+ void G.api('dev.log', { message: `editor-command-received ${commandId} hasEditor=${editorRef.current !== null}` }).catch(() => {})
1112
+ }
1113
+ const editor = editorRef.current
1114
+ const monaco = monacoRef.current
1115
+ if (editor !== null && monaco !== null) {
1116
+ editor.trigger('gufa-shortcut', commandId, null)
1117
+ if (commandId.includes('moveLines')) {
1118
+ window.setTimeout(() => {
1119
+ try {
1120
+ editor.trigger('gufa-reindent', 'editor.action.reindentselectedlines', null)
1121
+ } catch {
1122
+ /* ignore */
1123
+ }
1124
+ }, 0)
1125
+ }
1126
+ return
1127
+ }
1128
+ const textarea = textareaRef.current
1129
+ if (textarea !== null) textarea.focus()
1130
+ } catch (error) {
1131
+ void G.api('dev.log', { message: `editor-command-failed ${String(error)}` }).catch(() => {})
1132
+ }
1133
+ })
1134
+ return () => {
1135
+ try {
1136
+ off()
1137
+ } catch {
1138
+ /* ignore */
1139
+ }
1140
+ }
1141
+ }, [])
1142
+
1143
+ /* ── Ctrl/Cmd+S 保存 ───────────────────────────────────────────────
1144
+ * 监听挂在编辑区根容器的捕获阶段:App 还有一个 window 级 Ctrl/Cmd+S
1145
+ * 监听(90-app.js),它先于这里执行并通过 preventDefault 发起保存;
1146
+ * 用 defaultPrevented 判断「已被上层处理」,避免同一次按键触发两次
1147
+ * fs.write(会因 expectedRevision 竞态误报保存冲突)。单测/独立挂载
1148
+ * 没有上层监听时,这里负责调用 onSave。 */
1149
+ useEffect(() => {
1150
+ const node = rootRef.current
1151
+ if (node === null) return undefined
1152
+ const onKeyDown = (event) => {
1153
+ if (event.key !== 's' && event.key !== 'S') return
1154
+ if (!(event.metaKey || event.ctrlKey) || event.altKey) return
1155
+ const handledByOuterListener = event.defaultPrevented
1156
+ event.preventDefault()
1157
+ if (handledByOuterListener || event.repeat) return
1158
+ const path = activePathRef.current
1159
+ if (path === null) return
1160
+ const onSave = propsRef.current.onSave
1161
+ if (typeof onSave === 'function') onSave(path)
1162
+ }
1163
+ node.addEventListener('keydown', onKeyDown, true)
1164
+ return () => node.removeEventListener('keydown', onKeyDown, true)
1165
+ }, [])
1166
+
1167
+ /* ── AI 代码插入:G.bus 'editor:insert' ───────────────────────────── */
1168
+ useEffect(() => {
1169
+ if (G.bus === undefined || G.bus === null || typeof G.bus.on !== 'function') return undefined
1170
+ return G.bus.on('editor:insert', (detail) => {
1171
+ if (detail === null || detail === undefined || typeof detail !== 'object') return
1172
+ if (typeof detail.text !== 'string' || detail.text === '') return
1173
+ insertText(detail.text)
1174
+ })
1175
+ }, [insertText])
1176
+
1177
+ /* ── 降级 textarea:出现/切换 tab 时上报初始光标(不自动抢焦点) ── */
1178
+ useEffect(() => {
1179
+ if (status !== 'error') return
1180
+ reportFallbackCursor()
1181
+ }, [status, activePath, reportFallbackCursor])
1182
+
1183
+ /* ── Diff 工具条动作 ─────────────────────────────────────────────── */
1184
+ const diffNavigate = (direction) => {
1185
+ const instance = diffEditorRef.current
1186
+ if (instance === null) return
1187
+ try {
1188
+ const changes = typeof instance.getLineChanges === 'function' ? instance.getLineChanges() : null
1189
+ if (changes === null || changes === undefined || changes.length === 0) return
1190
+ const editor = instance.getModifiedEditor()
1191
+ const line = editor.getPosition() === null ? 0 : editor.getPosition().lineNumber
1192
+ let target = direction > 0 ? changes.find((item) => item.modifiedStartLineNumber > line) : [...changes].reverse().find((item) => item.modifiedEndLineNumber < line)
1193
+ if (target === undefined) target = direction > 0 ? changes[0] : changes[changes.length - 1]
1194
+ if (target === undefined) return
1195
+ editor.revealLineInCenter(target.modifiedStartLineNumber)
1196
+ editor.setPosition({ lineNumber: target.modifiedStartLineNumber, column: 1 })
1197
+ } catch {
1198
+ /* ignore */
1199
+ }
1200
+ }
1201
+
1202
+ /* ── 渲染 ───────────────────────────────────────────────────────── */
1203
+ let emptyState = null
1204
+ if (activeTab === null) {
1205
+ emptyState = h(G.Empty, { icon: 'seal', title: G.t('editor.noFile'), hint: G.t('editor.noFileHint') })
1206
+ } else if (activeTab.binary === true) {
1207
+ emptyState = h(G.Empty, { icon: 'file', title: G.t('editor.binary') })
1208
+ } else if (activeTab.tooLarge === true) {
1209
+ emptyState = h(G.Empty, { icon: 'file', title: G.t('editor.tooLarge') })
1210
+ }
1211
+
1212
+ return h(
1213
+ 'div',
1214
+ {
1215
+ ref: rootRef,
1216
+ className: 'gufa-editor-pane',
1217
+ style: { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', minWidth: 0, minHeight: 0 },
1218
+ },
1219
+ status === 'error'
1220
+ ? h(
1221
+ 'div',
1222
+ { className: 'gufa-error-banner', style: { margin: '8px 10px 0', flex: '0 0 auto' } },
1223
+ h(G.Icon, { name: 'warn', size: 13 }),
1224
+ h('span', { style: { flex: '1 1 auto', minWidth: 0 } }, `${G.t('app.error')} · ${monacoError}`),
1225
+ h(G.IconButton, { title: G.t('common.retry'), onClick: retryMonaco }, h(G.Icon, { name: 'refresh', size: 13 })),
1226
+ )
1227
+ : null,
1228
+ h(
1229
+ 'div',
1230
+ { style: { position: 'relative', flex: '1 1 auto', minWidth: 0, minHeight: 0 } },
1231
+ /* Monaco 挂载点:始终保留 DOM(卸载会破坏 editor 实例),用 display 控制可见性。 */
1232
+ h('div', {
1233
+ ref: hostRef,
1234
+ className: 'gufa-monaco-host',
1235
+ style: { position: 'absolute', inset: 0, display: status === 'error' || activeIsDiff ? 'none' : 'block' },
1236
+ }),
1237
+ activeIsDiff && status !== 'error'
1238
+ ? h(
1239
+ 'div',
1240
+ { className: 'gufa-diff-toolbar' },
1241
+ h(G.IconButton, { title: G.t('diff.prev'), onClick: () => diffNavigate(-1) }, h(G.Icon, { name: 'chevron-up', size: 13 })),
1242
+ h(G.IconButton, { title: G.t('diff.next'), onClick: () => diffNavigate(1) }, h(G.Icon, { name: 'chevron-down', size: 13 })),
1243
+ h('span', { style: { flex: '1 1 auto' } }),
1244
+ activeTab.diff !== null && activeTab.diff !== undefined && activeTab.diff.compare !== true && activeTab.diff.editable === true
1245
+ ? h(G.Button, { icon: 'undo', title: G.t('diff.revertFile'), onClick: () => props.onDiffRevert?.(activeTab.path) }, G.t('diff.revertFile'))
1246
+ : null,
1247
+ activeTab.diff !== null && activeTab.diff !== undefined && activeTab.diff.compare !== true && activeTab.diff.editable === true
1248
+ ? h(G.Button, { icon: 'plus', title: G.t('diff.stageFile'), onClick: () => props.onDiffStage?.(activeTab.path) }, G.t('diff.stageFile'))
1249
+ : null,
1250
+ )
1251
+ : null,
1252
+ h('div', {
1253
+ ref: diffHostRef,
1254
+ className: 'gufa-diff-host',
1255
+ style: { position: 'absolute', inset: 0, top: activeIsDiff && status !== 'error' ? 30 : 0, display: activeIsDiff && status !== 'error' ? 'block' : 'none' },
1256
+ }),
1257
+ /* 加载失败:复用原占位实现的 textarea 降级(仍可编辑、保存、上报光标)。 */
1258
+ status === 'error' && editableActiveTab !== null
1259
+ ? h('textarea', {
1260
+ ref: textareaRef,
1261
+ className: 'gufa-editor-fallback gufa-scroll',
1262
+ spellCheck: false,
1263
+ value: editableActiveTab.content ?? '',
1264
+ onChange: (event) => props.onChange?.(editableActiveTab.path, event.target.value),
1265
+ onKeyUp: reportFallbackCursor,
1266
+ onClick: reportFallbackCursor,
1267
+ onSelect: reportFallbackCursor,
1268
+ style: {
1269
+ border: 0,
1270
+ resize: 'none',
1271
+ outline: 'none',
1272
+ background: 'transparent',
1273
+ color: 'var(--dsw-alias-label-primary)',
1274
+ },
1275
+ })
1276
+ : null,
1277
+ /* 无文件 / 二进制 / 过大:空状态覆盖在 editor 之上(不清空 editor DOM)。 */
1278
+ emptyState !== null
1279
+ ? h('div', { style: { position: 'absolute', inset: 0, background: props.backdropMode === true ? 'transparent' : 'var(--gufa-bg)' } }, emptyState)
1280
+ : null,
1281
+ /* 首次加载 Monaco 的占位(覆盖在最上层,避免白屏)。 */
1282
+ status === 'loading'
1283
+ ? h(
1284
+ 'div',
1285
+ {
1286
+ style: {
1287
+ position: 'absolute',
1288
+ inset: 0,
1289
+ display: 'flex',
1290
+ alignItems: 'center',
1291
+ justifyContent: 'center',
1292
+ gap: 8,
1293
+ color: 'var(--gufa-text-dim)',
1294
+ background: props.backdropMode === true ? 'transparent' : 'var(--gufa-bg)',
1295
+ },
1296
+ },
1297
+ h(G.Spinner),
1298
+ h('span', null, G.t('editor.loading')),
1299
+ )
1300
+ : null,
1301
+ ),
1302
+ )
1303
+ }
1304
+
1305
+ /* 纯函数/加载状态测试钩子:供 scripts/test-editor.mjs 在无浏览器环境下校验
1306
+ * URI、光标推进、主题映射与 loader 失败降级 / 重试 / 单例注入。 */
1307
+ G.EditorPane.internals = Object.freeze({
1308
+ modelUri: gufaModelUri,
1309
+ advancePosition: gufaAdvancePosition,
1310
+ editorOptions: gufaEditorOptions,
1311
+ monacoTheme: gufaMonacoTheme,
1312
+ skinMonacoTheme: gufaSkinMonacoTheme,
1313
+ workerScript: gufaWorkerScript,
1314
+ resolveLanguage: gufaResolveLanguage,
1315
+ loadMonaco: gufaLoadMonaco,
1316
+ resetMonacoFailure: gufaResetMonacoFailure,
1317
+ })
1318
+ })()