dsh-gufa-code 0.0.0-stage → 1.0.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/LICENSE +21 -0
- package/README.md +93 -2
- package/client.js +11548 -0
- package/cordis.patch.yml +8 -0
- package/docs/icon-180.png +0 -0
- package/docs/icon.svg +18 -0
- package/docs/market-submission.md +32 -0
- package/docs/screenshot-ide.jpg +0 -0
- package/host/assets.js +31 -0
- package/host/chat.js +523 -0
- package/host/context.js +438 -0
- package/host/dev.js +35 -0
- package/host/fs.js +306 -0
- package/host/git.js +532 -0
- package/host/project.js +305 -0
- package/host/ssh.js +54 -0
- package/host/terminal-pty.js +237 -0
- package/host/terminal.js +947 -0
- package/icon.svg +18 -0
- package/index.js +63 -0
- package/locale/en.json +121 -0
- package/locale/zh.json +121 -0
- package/package.json +67 -4
- package/screenshots.json +3 -0
- package/scripts/build-client.mjs +38 -0
- package/scripts/selftest-client.mjs +457 -0
- package/scripts/selftest-host.mjs +197 -0
- package/scripts/selftest-http.mjs +284 -0
- package/scripts/selftest-router.mjs +101 -0
- package/scripts/selftest-terminal-client.mjs +314 -0
- package/scripts/selftest-terminal-pty.mjs +190 -0
- package/scripts/selftest-terminal.mjs +351 -0
- package/scripts/test-chat-render.mjs +702 -0
- package/scripts/test-chat.mjs +761 -0
- package/scripts/test-editor.mjs +516 -0
- package/scripts/verify-workspace-sessions.mjs +135 -0
- package/src/client/00-core.js +1686 -0
- package/src/client/10-icons.js +205 -0
- package/src/client/20-editor.js +1318 -0
- package/src/client/30-terminal.js +1510 -0
- package/src/client/40-chat.js +1184 -0
- package/src/client/46-gitgraph.js +278 -0
- package/src/client/50-panels.js +916 -0
- package/src/client/55-backdrop.js +508 -0
- package/src/client/60-dock.js +662 -0
- package/src/client/85-shortcuts.js +516 -0
- package/src/client/90-app.js +2272 -0
- package/src/client/99-main.js +449 -0
- package/vendor/monaco/LICENSE +21 -0
- package/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/vendor/monaco/vs/base/worker/workerMain.js +31 -0
- package/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
- package/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/vendor/monaco/vs/editor/editor.main.css +8 -0
- package/vendor/monaco/vs/editor/editor.main.js +798 -0
- package/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/vendor/monaco/vs/language/css/cssWorker.js +77 -0
- package/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
- package/vendor/monaco/vs/language/json/jsonMode.js +19 -0
- package/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
- package/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
- package/vendor/monaco/vs/loader.js +11 -0
- package/vendor/monaco/vs/nls.messages.de.js +21 -0
- package/vendor/monaco/vs/nls.messages.es.js +21 -0
- package/vendor/monaco/vs/nls.messages.fr.js +19 -0
- package/vendor/monaco/vs/nls.messages.it.js +19 -0
- package/vendor/monaco/vs/nls.messages.ja.js +21 -0
- package/vendor/monaco/vs/nls.messages.ko.js +19 -0
- package/vendor/monaco/vs/nls.messages.ru.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-cn.js +21 -0
- package/vendor/monaco/vs/nls.messages.zh-tw.js +19 -0
- package/vendor/xterm/LICENSE +21 -0
- package/vendor/xterm/LICENSE.addon-fit +19 -0
- package/vendor/xterm/addon-fit.js +2 -0
- package/vendor/xterm/xterm.css +285 -0
- 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
|
+
})()
|