dsh-git-changes 0.0.0-stage → 0.1.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.
Files changed (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +119 -0
  3. package/README.md +137 -2
  4. package/THIRD_PARTY_NOTICES.md +12 -0
  5. package/assets/vendor/monaco/LICENSE +21 -0
  6. package/assets/vendor/monaco/ThirdPartyNotices.txt +448 -0
  7. package/assets/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  8. package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +7 -0
  9. package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +7 -0
  10. package/assets/vendor/monaco/vs/base/worker/workerMain.js +26 -0
  11. package/assets/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  12. package/assets/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  13. package/assets/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  14. package/assets/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  15. package/assets/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  16. package/assets/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  17. package/assets/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  18. package/assets/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  19. package/assets/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  20. package/assets/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  21. package/assets/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  22. package/assets/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  23. package/assets/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  24. package/assets/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  25. package/assets/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  26. package/assets/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  27. package/assets/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  28. package/assets/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  29. package/assets/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  30. package/assets/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  31. package/assets/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  32. package/assets/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  33. package/assets/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  34. package/assets/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  35. package/assets/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  36. package/assets/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  37. package/assets/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  38. package/assets/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  39. package/assets/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  40. package/assets/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  41. package/assets/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  42. package/assets/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  43. package/assets/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  44. package/assets/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  45. package/assets/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  46. package/assets/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  47. package/assets/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  48. package/assets/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  49. package/assets/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  50. package/assets/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  51. package/assets/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  52. package/assets/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  53. package/assets/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  54. package/assets/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  55. package/assets/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  56. package/assets/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  57. package/assets/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  58. package/assets/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  59. package/assets/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  60. package/assets/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  61. package/assets/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  62. package/assets/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  63. package/assets/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  64. package/assets/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  65. package/assets/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  66. package/assets/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  67. package/assets/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  68. package/assets/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  69. package/assets/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  70. package/assets/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  71. package/assets/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  72. package/assets/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  73. package/assets/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  74. package/assets/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  75. package/assets/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  76. package/assets/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  77. package/assets/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  78. package/assets/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  79. package/assets/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  80. package/assets/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  81. package/assets/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  82. package/assets/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  83. package/assets/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  84. package/assets/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  85. package/assets/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  86. package/assets/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  87. package/assets/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  88. package/assets/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  89. package/assets/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  90. package/assets/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  91. package/assets/vendor/monaco/vs/editor/editor.main.css +6 -0
  92. package/assets/vendor/monaco/vs/editor/editor.main.js +744 -0
  93. package/assets/vendor/monaco/vs/editor/editor.main.nls.js +28 -0
  94. package/assets/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +30 -0
  95. package/assets/vendor/monaco/vs/language/css/cssMode.js +13 -0
  96. package/assets/vendor/monaco/vs/language/css/cssWorker.js +81 -0
  97. package/assets/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  98. package/assets/vendor/monaco/vs/language/html/htmlWorker.js +453 -0
  99. package/assets/vendor/monaco/vs/language/json/jsonMode.js +15 -0
  100. package/assets/vendor/monaco/vs/language/json/jsonWorker.js +36 -0
  101. package/assets/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  102. package/assets/vendor/monaco/vs/language/typescript/tsWorker.js +37016 -0
  103. package/assets/vendor/monaco/vs/loader.js +10 -0
  104. package/cordis.patch.yml +3 -0
  105. package/docs/images/git-changes-diff.png +0 -0
  106. package/docs/regression-checklist.md +47 -0
  107. package/icon.svg +10 -0
  108. package/lib/client.js +9418 -0
  109. package/lib/host/assets.js +161 -0
  110. package/lib/host/commit-message.js +158 -0
  111. package/lib/host/files-index.js +206 -0
  112. package/lib/host/git-write.js +194 -0
  113. package/lib/host/git.js +1147 -0
  114. package/lib/host/lsp.js +842 -0
  115. package/lib/host/routes.js +976 -0
  116. package/lib/host/session.js +84 -0
  117. package/lib/host/util.js +107 -0
  118. package/lib/index.js +33 -0
  119. package/licenses/dsh-vscode-mode-LICENSE +21 -0
  120. package/licenses/vscode-theme-seti-LICENSE +21 -0
  121. package/licenses/vscode-theme-seti-ThirdPartyNotices.txt +32 -0
  122. package/locale/en.json +6 -0
  123. package/locale/zh.json +6 -0
  124. package/package.json +65 -4
  125. package/src/client/00-const.js +79 -0
  126. package/src/client/05-utils.js +277 -0
  127. package/src/client/10-i18n.js +548 -0
  128. package/src/client/12-vscode-icons.js +460 -0
  129. package/src/client/15-theme.js +509 -0
  130. package/src/client/20-monaco.js +1404 -0
  131. package/src/client/22-lsp.js +592 -0
  132. package/src/client/25-tree.js +563 -0
  133. package/src/client/28-changes.js +1163 -0
  134. package/src/client/30-page.js +2259 -0
  135. package/src/client/35-pill.js +185 -0
  136. package/src/client/40-commands.js +248 -0
  137. package/src/client/45-css.js +357 -0
  138. package/src/client/47-pickers.js +457 -0
  139. package/src/client/50-apply.js +207 -0
  140. package/src/client/99-footer.js +62 -0
@@ -0,0 +1,592 @@
1
+ /* dsh-git-changes client half — language-server features (Phase 3). */
2
+
3
+ /**
4
+ * Runtime binding for the active editor document. The vendored Monaco runs
5
+ * without a file system, so providers read this record instead of the model URI.
6
+ */
7
+ const lspRuntime = {
8
+ sessionId: null,
9
+ cwd: null,
10
+ path: null,
11
+ text: null,
12
+ languageId: null,
13
+ monaco: null,
14
+ model: null,
15
+ host: null,
16
+ // Historical (= git revision) content: navigation/hover keep working against
17
+ // the overlaid text, but diagnostics are suppressed because the server would
18
+ // mix that snapshot with the current worktree project.
19
+ historical: false,
20
+ missing: null,
21
+ registered: false,
22
+ unsupported: null,
23
+ diagnosticsTimer: null,
24
+ diagnosticsPollId: 0,
25
+ diagnosticsVisibility: null,
26
+ diagOpenedAt: 0,
27
+ diagLastSignature: null,
28
+ diagStableCount: 0,
29
+ builtinValidationDisabled: false,
30
+ generation: 0,
31
+ dismissed: new Set(),
32
+ providers: [],
33
+ navigationId: 0,
34
+ }
35
+
36
+ /** Preserve absolute-path identity until containment has been checked. */
37
+ function workspaceLocationPath(raw, cwd = lspRuntime.cwd) {
38
+ let path = String(raw || '').replace(/\\/g, '/')
39
+ if (/^file:\/\//i.test(path)) {
40
+ try {
41
+ const uri = new URL(path)
42
+ path = decodeURIComponent(uri.pathname)
43
+ if (uri.hostname && uri.hostname !== 'localhost') path = '//' + uri.hostname + path
44
+ else if (/^\/[a-z]:\//i.test(path)) path = path.slice(1)
45
+ } catch { return null }
46
+ }
47
+ const rawRoot = String(cwd || '').replace(/\\/g, '/')
48
+ const root = rawRoot.replace(/\/+$/, '') || (rawRoot.startsWith('/') ? '/' : '')
49
+ const windows = /^[a-z]:\//i.test(rawRoot) || rawRoot.startsWith('//')
50
+ const key = (value) => windows ? value.toLowerCase() : value
51
+ if (/^(?:[a-z]:|\/)/i.test(path)) {
52
+ const prefix = root.endsWith('/') ? root : root + '/'
53
+ if (!root || !key(path).startsWith(key(prefix))) return null
54
+ path = path.slice(prefix.length)
55
+ }
56
+ path = path.replace(/^\.\//, '')
57
+ if (!path || /[:\x00-\x1f]/.test(path) || path.split('/').some((part) => !part || part === '..' || part === '.')) return null
58
+ return path
59
+ }
60
+
61
+ /** A selection's start belongs to the selected symbol; its end may not. */
62
+ function lspPosition(editor) {
63
+ const selection = editor?.getSelection?.()
64
+ return selection && !selection.isEmpty() ? selection.getStartPosition() : editor?.getPosition?.()
65
+ }
66
+
67
+ function lspForgetMissing() {
68
+ if (lspRuntime.missing) lspRuntime.dismissed.add(lspRuntime.missing.language)
69
+ lspRuntime.missing = null
70
+ }
71
+
72
+ async function lspCheckAvailability(refresh = false) {
73
+ const generation = lspRuntime.generation
74
+ if (refresh) {
75
+ lspRuntime.dismissed.delete(lspRuntime.languageId)
76
+ lspRuntime.missing = null
77
+ }
78
+ const response = await lspRequest('status', { refresh })
79
+ if (generation !== lspRuntime.generation) return response
80
+ if (response.available) {
81
+ lspRuntime.missing = null
82
+ lspRuntime.host?.onMissing?.(null)
83
+ if (refresh) scheduleLspDiagnostics()
84
+ } else if (response.reason === 'missing-server') noteLspUnavailable(response)
85
+ if (refresh) lspRuntime.host?.notify?.(response.available ? '语言服务器已就绪' : lspFailureMessage(response, '语言服务器尚未就绪'), response.available ? 'info' : 'warn')
86
+ return response
87
+ }
88
+
89
+ async function lspDiagnostics() {
90
+ const response = await lspCheckAvailability()
91
+ return [
92
+ '语言:' + (lspRuntime.languageId || '—'),
93
+ '文件:' + (lspRuntime.path || '—'),
94
+ response.available ? '服务器:' + response.server : lspFailureMessage(response, '状态:' + response.reason),
95
+ ]
96
+ }
97
+
98
+ function lspClearDocument(model) {
99
+ if (model && model !== lspRuntime.model) return
100
+ stopLspDiagnostics()
101
+ clearLspMarkers()
102
+ lspRuntime.generation += 1
103
+ lspRuntime.model = null
104
+ lspRuntime.path = null
105
+ lspRuntime.text = null
106
+ lspRuntime.historical = false
107
+ }
108
+
109
+ function lspDispose() {
110
+ lspClearDocument()
111
+ if (lspRuntime.diagnosticsVisibility && typeof document !== 'undefined') {
112
+ document.removeEventListener('visibilitychange', lspRuntime.diagnosticsVisibility)
113
+ lspRuntime.diagnosticsVisibility = null
114
+ }
115
+ for (const provider of lspRuntime.providers) { try { provider?.dispose?.() } catch { /* already disposed */ } }
116
+ lspRuntime.providers = []
117
+ lspRuntime.registered = false
118
+ lspRuntime.host = null
119
+ }
120
+
121
+ /**
122
+ * Resolve the official composer input facade for one session.
123
+ *
124
+ * DSH exposes the composer only through `ctx.get('conversation').input.for(...)`
125
+ * (the same path the product's own UI uses); there is no standalone input
126
+ * service in the client Service catalog.
127
+ * @param {string} sessionId session id
128
+ * @returns {any | null} composer shell (setDraft / insertText / submit) or null
129
+ */
130
+ function composerFor(sessionId) {
131
+ try {
132
+ const ctx = globalCtxRef.current
133
+ if (!ctx || !sessionId) return null
134
+ const sessions = safeGet(ctx, 'sessions')
135
+ const conversation = safeGet(ctx, 'conversation')
136
+ const actx = sessions && typeof sessions.scope === 'function' ? sessions.scope(sessionId) : undefined
137
+ const input = conversation && conversation.input && typeof conversation.input.for === 'function'
138
+ ? conversation.input.for(actx)
139
+ : undefined
140
+ return input || null
141
+ } catch (error) {
142
+ return null
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Hand the install request to the Agent: fill the composer draft and submit it
148
+ * (one explicit click is the confirmation, so nothing installs silently).
149
+ * @param {string} install install command
150
+ * @param {string} language language label
151
+ * @returns {Promise<'sent'|'draft'|null>} how far the request got
152
+ */
153
+ async function lspAskAgentToInstall(install, language) {
154
+ if (!install || !language) return null
155
+ const prompt = '请在这个工作区安装并配置 ' + language + ' 语言服务器(用于代码审查页的跳转定义/查找引用/悬停提示/诊断):' + install + '。安装完成后告诉我如何验证。'
156
+ const shell = composerFor(lspRuntime.sessionId)
157
+ try {
158
+ if (shell && typeof shell.setDraft === 'function') {
159
+ await shell.setDraft(prompt)
160
+ if (typeof shell.submit === 'function') {
161
+ await shell.submit()
162
+ return 'sent'
163
+ }
164
+ return 'draft'
165
+ }
166
+ if (shell && typeof shell.insertText === 'function') {
167
+ await shell.insertText(prompt, null)
168
+ if (typeof shell.submit === 'function') {
169
+ await shell.submit()
170
+ return 'sent'
171
+ }
172
+ return 'draft'
173
+ }
174
+ } catch (error) {
175
+ /* fall back to the clipboard in the caller */
176
+ }
177
+ return null
178
+ }
179
+
180
+ /**
181
+ * Put a language-server install request into the chat composer draft.
182
+ * Fallback for when the composer cannot submit directly.
183
+ * @param {string} install install command
184
+ * @param {string} language language label
185
+ * @returns {'draft'|'insert'|null} how the request was inserted
186
+ */
187
+ function lspInsertInstallRequest(install, language) {
188
+ if (!install || !language) return null
189
+ const prompt = '请在这个工作区安装并配置 ' + language + ' 语言服务器(用于代码审查页的跳转定义/查找引用/悬停提示/诊断):' + install + '。安装完成后告诉我如何验证。'
190
+ const shell = composerFor(lspRuntime.sessionId)
191
+ try {
192
+ if (shell && typeof shell.setDraft === 'function') {
193
+ shell.setDraft(prompt)
194
+ return 'draft'
195
+ }
196
+ if (shell && typeof shell.insertText === 'function') {
197
+ shell.insertText(prompt, null)
198
+ return 'insert'
199
+ }
200
+ } catch (error) {
201
+ /* fall back to the clipboard */
202
+ }
203
+ return null
204
+ }
205
+
206
+ /** Languages whose servers this build can talk to. */
207
+ const LSP_LANGUAGES = [
208
+ 'typescript',
209
+ 'javascript',
210
+ 'typescriptreact',
211
+ 'javascriptreact',
212
+ 'python',
213
+ 'json',
214
+ 'jsonc',
215
+ 'go',
216
+ 'rust',
217
+ ]
218
+
219
+ /**
220
+ * One LSP action through the Host bridge.
221
+ * @param {string} action action name
222
+ * @param {object} payload extra fields
223
+ * @returns {Promise<any>}
224
+ */
225
+ async function lspRequest(action, payload) {
226
+ if (!lspRuntime.sessionId) return { available: false, reason: 'no-session' }
227
+ if (action !== 'status' && !lspRuntime.model) return { available: false, reason: 'no-document' }
228
+ try {
229
+ const response = await apiPost('lsp', {
230
+ sessionId: lspRuntime.sessionId,
231
+ action,
232
+ path: lspRuntime.path,
233
+ text: lspRuntime.text,
234
+ languageId: lspRuntime.languageId,
235
+ ...(payload || {}),
236
+ })
237
+ if (!response || typeof response !== 'object') return { available: false, reason: 'transport-error' }
238
+ if (response.error) return { available: false, reason: 'host-stale', message: String(response.error.message || '') }
239
+ return response
240
+ } catch (error) {
241
+ return { available: false, reason: 'transport-error', message: String(error && error.message ? error.message : error) }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * User-facing explanation for a failed LSP response: distinguishes a stale Host
247
+ * (route missing until DSH restarts) from "no server installed" and a missing
248
+ * page session, so a silent failure never looks like "nothing found".
249
+ * @param {any} response LSP response
250
+ * @param {string} fallback generic message
251
+ * @returns {string}
252
+ */
253
+ function lspFailureMessage(response, fallback) {
254
+ if (!response) return fallback
255
+ if (response.reason === 'no-session') return '当前页面没有会话上下文,无法查询语言服务器'
256
+ if (response.reason === 'no-document') return '请先打开代码文件并选中要查询的符号'
257
+ if (response.reason === 'host-stale' || response.reason === 'transport-error') {
258
+ return '语言服务器接口不可用:请重启 DSH 以加载最新插件 Host 端(新增了 LSP 路由)'
259
+ }
260
+ if (response.message) return fallback + ':' + String(response.message)
261
+ return fallback
262
+ }
263
+
264
+ /**
265
+ * Remember the active document (called whenever the Monaco model changes).
266
+ * Historical (git revision) content is a first-class document for navigation:
267
+ * the Host bridge overlays the shown text, so F12 / references / hover / symbols
268
+ * work against the snapshot. Diagnostics stay off there (see `historical`).
269
+ * @param {object} info document facts plus page callbacks
270
+ */
271
+ function lspSetDocument(info) {
272
+ const historical = info.historical === true
273
+ const wasHistorical = lspRuntime.historical
274
+ const changed = lspRuntime.sessionId !== info.sessionId || lspRuntime.cwd !== info.cwd || lspRuntime.languageId !== info.languageId || lspRuntime.model !== info.model || lspRuntime.path !== info.path || lspRuntime.text !== info.text
275
+ if (lspRuntime.languageId !== info.languageId) {
276
+ lspRuntime.missing = null
277
+ lspRuntime.host?.onMissing?.(null)
278
+ }
279
+ if (changed) { clearLspMarkers(); lspRuntime.generation += 1 }
280
+ lspRuntime.sessionId = info.sessionId
281
+ lspRuntime.cwd = info.cwd
282
+ lspRuntime.path = info.path
283
+ lspRuntime.text = info.text
284
+ lspRuntime.languageId = info.languageId
285
+ lspRuntime.monaco = info.monaco
286
+ lspRuntime.model = info.model
287
+ lspRuntime.historical = historical
288
+ if (historical) {
289
+ // A historical snapshot is never mixed with the worktree project for
290
+ // diagnostics; navigation still uses the overlaid document text.
291
+ stopLspDiagnostics()
292
+ clearLspMarkers()
293
+ }
294
+ if (changed) {
295
+ lspRuntime.diagOpenedAt = Date.now()
296
+ lspRuntime.diagLastSignature = null
297
+ lspRuntime.diagStableCount = 0
298
+ clearLspMarkers()
299
+ clearForeignDiagnosticMarkers(info.monaco, info.model)
300
+ disableBuiltinTsValidation(info.monaco)
301
+ if (!historical) scheduleLspDiagnostics()
302
+ return
303
+ }
304
+ // Same text/model but the snapshot kind changed (e.g. a commit version whose
305
+ // content equals the worktree): re-arm polling only when leaving historical.
306
+ if (wasHistorical && !historical) {
307
+ stopLspDiagnostics()
308
+ scheduleLspDiagnostics()
309
+ }
310
+ }
311
+
312
+ /**
313
+ * Turn off the Monaco built-in TypeScript/JavaScript worker validation. Those
314
+ * workers run without the project's tsconfig and are the source of the
315
+ * "foreign" markers that VS Code never shows (the user reported e.g. a
316
+ * moduleResolution 2792 hover that this page's own bridge cannot produce).
317
+ * The plugin's language-server markers (`dsh-lsp`) stay the single source.
318
+ * @param {any} monaco Monaco namespace
319
+ */
320
+ function disableBuiltinTsValidation(monaco) {
321
+ if (lspRuntime.builtinValidationDisabled) return
322
+ try {
323
+ const languages = monaco && monaco.languages && monaco.languages.typescript
324
+ if (!languages) return
325
+ const options = { noSemanticValidation: true, noSyntaxValidation: true, noSuggestionDiagnostics: true }
326
+ if (languages.typescriptDefaults && typeof languages.typescriptDefaults.setDiagnosticsOptions === 'function') {
327
+ languages.typescriptDefaults.setDiagnosticsOptions(options)
328
+ }
329
+ if (languages.javascriptDefaults && typeof languages.javascriptDefaults.setDiagnosticsOptions === 'function') {
330
+ languages.javascriptDefaults.setDiagnosticsOptions(options)
331
+ }
332
+ lspRuntime.builtinValidationDisabled = true
333
+ } catch (error) { /* validation settings are best-effort */ }
334
+ }
335
+
336
+ /**
337
+ * Drop markers owned by anything other than this bridge on a plugin model
338
+ * (defensive: a stale foreign provider marker would survive model switches).
339
+ * @param {any} monaco Monaco namespace
340
+ * @param {any} model Monaco text model
341
+ */
342
+ function clearForeignDiagnosticMarkers(monaco, model) {
343
+ try {
344
+ if (!monaco || !model || !monaco.editor || typeof monaco.editor.getModelMarkers !== 'function') return
345
+ const markers = monaco.editor.getModelMarkers({ resource: model.uri })
346
+ const owners = new Set()
347
+ for (const marker of markers) {
348
+ if (marker && marker.owner && marker.owner !== 'dsh-lsp') owners.add(marker.owner)
349
+ }
350
+ for (const owner of owners) monaco.editor.setModelMarkers(model, owner, [])
351
+ } catch (error) { /* markers are best-effort */ }
352
+ }
353
+
354
+ /** Clear Monaco diagnostics markers on the active model. */
355
+ function clearLspMarkers() {
356
+ try {
357
+ if (lspRuntime.monaco && lspRuntime.model) {
358
+ lspRuntime.monaco.editor.setModelMarkers(lspRuntime.model, 'dsh-lsp', [])
359
+ }
360
+ } catch (error) { /* markers are best-effort */ }
361
+ }
362
+
363
+ /**
364
+ * Report a missing language server to the page once per language.
365
+ * @param {any} response LSP response
366
+ * @returns {boolean} whether the response means "server not installed"
367
+ */
368
+ function noteLspUnavailable(response) {
369
+ if (!response || response.available !== false) return false
370
+ const language = response.language || lspRuntime.languageId
371
+ if (response.reason === 'unsupported-language') {
372
+ if (!lspRuntime.unsupported) lspRuntime.unsupported = new Set()
373
+ if (lspRuntime.unsupported.has(language)) return true
374
+ lspRuntime.unsupported.add(language)
375
+ try { lspRuntime.host?.onUnsupported?.(language) } catch (error) { /* ignore */ }
376
+ return true
377
+ }
378
+ if (response.reason !== 'missing-server') return false
379
+ const install = response.install || ''
380
+ if (!install) return false
381
+ if (lspRuntime.dismissed.has(language)) return true
382
+ const next = { language, install }
383
+ if (lspRuntime.missing && lspRuntime.missing.language === next.language) return true
384
+ lspRuntime.missing = next
385
+ try { lspRuntime.host?.onMissing?.(next) } catch (error) { /* ignore */ }
386
+ return true
387
+ }
388
+
389
+ /** Diagnostics settle window (ms): never paint before the server had time to
390
+ * load the project and replace the early syntax/semantic snapshots. */
391
+ const LSP_DIAG_SETTLE_MS = 3500
392
+ /** Poll quickly during project startup, then back off to one request per 5 s. */
393
+ const LSP_DIAG_FAST_ATTEMPTS = 16
394
+ const LSP_DIAG_IDLE_MS = 5000
395
+
396
+ /** Cancel the timer and invalidate any response that is still in flight. */
397
+ function stopLspDiagnostics() {
398
+ clearTimeout(lspRuntime.diagnosticsTimer)
399
+ lspRuntime.diagnosticsTimer = null
400
+ lspRuntime.diagnosticsPollId += 1
401
+ }
402
+
403
+ function lspDiagnosticsVisible() {
404
+ return typeof document === 'undefined' || !document.hidden
405
+ }
406
+
407
+ /** Poll diagnostics shortly after the document changed. */
408
+ function scheduleLspDiagnostics(attempt = 0, pollId) {
409
+ // Historical snapshots never publish diagnostics (see `lspRuntime.historical`).
410
+ if (lspRuntime.historical) return
411
+ if (pollId === undefined) {
412
+ stopLspDiagnostics()
413
+ pollId = lspRuntime.diagnosticsPollId
414
+ }
415
+ if (!lspRuntime.diagnosticsVisibility && typeof document !== 'undefined') {
416
+ lspRuntime.diagnosticsVisibility = () => {
417
+ stopLspDiagnostics()
418
+ if (lspDiagnosticsVisible()) scheduleLspDiagnostics()
419
+ }
420
+ document.addEventListener('visibilitychange', lspRuntime.diagnosticsVisibility)
421
+ }
422
+ if (!lspRuntime.model || !lspRuntime.sessionId || !lspDiagnosticsVisible() || pollId !== lspRuntime.diagnosticsPollId) return
423
+ clearTimeout(lspRuntime.diagnosticsTimer)
424
+ const delay = attempt === 0 ? 300 : attempt <= LSP_DIAG_FAST_ATTEMPTS ? 700
425
+ : Math.min(LSP_DIAG_IDLE_MS, 700 * 2 ** Math.min(4, attempt - LSP_DIAG_FAST_ATTEMPTS))
426
+ lspRuntime.diagnosticsTimer = setTimeout(() => {
427
+ lspRuntime.diagnosticsTimer = null
428
+ void refreshLspDiagnostics(attempt, pollId)
429
+ }, delay)
430
+ }
431
+
432
+ /** Fetch diagnostics and publish them as Monaco markers. */
433
+ async function refreshLspDiagnostics(attempt = 0, pollId = lspRuntime.diagnosticsPollId) {
434
+ if (lspRuntime.historical) return
435
+ if (!lspRuntime.sessionId || !lspRuntime.model || !lspDiagnosticsVisible() || pollId !== lspRuntime.diagnosticsPollId) return
436
+ const model = lspRuntime.model
437
+ const generation = lspRuntime.generation
438
+ try {
439
+ const response = await lspRequest('diagnostics', {})
440
+ if (model !== lspRuntime.model || generation !== lspRuntime.generation || pollId !== lspRuntime.diagnosticsPollId || !lspDiagnosticsVisible()) return
441
+ if (!response || response.available !== true) {
442
+ noteLspUnavailable(response)
443
+ // Missing/unsupported servers need an explicit retry or a new document.
444
+ // Transient transport/process failures may recover during a visible view.
445
+ if (!['missing-server', 'unsupported-language', 'no-session', 'no-document', 'disposed'].includes(response?.reason)) {
446
+ scheduleLspDiagnostics(Math.max(attempt + 1, LSP_DIAG_FAST_ATTEMPTS + 3), pollId)
447
+ }
448
+ return
449
+ }
450
+ scheduleLspDiagnostics(attempt + 1, pollId)
451
+ // The bridge returns `pending` while the server has not yet published a
452
+ // snapshot for the opened document version. Never paint in that state.
453
+ if (response.pending === true) {
454
+ lspRuntime.diagLastSignature = null
455
+ lspRuntime.diagStableCount = 0
456
+ return
457
+ }
458
+ const list = Array.isArray(response.diagnostics) ? response.diagnostics : []
459
+ const signature = JSON.stringify(list)
460
+ if (signature === lspRuntime.diagLastSignature) lspRuntime.diagStableCount += 1
461
+ else {
462
+ lspRuntime.diagLastSignature = signature
463
+ lspRuntime.diagStableCount = 1
464
+ }
465
+ const settled = Date.now() - (lspRuntime.diagOpenedAt || 0) >= LSP_DIAG_SETTLE_MS
466
+ const stable = lspRuntime.diagStableCount >= 2
467
+ // Paint only a snapshot that had time to settle and did not change between
468
+ // two polls; the very first false snapshot (or an empty syntax snapshot) is
469
+ // therefore never shown, and a late correct snapshot still clears anything.
470
+ if (!settled || !stable) return
471
+ const markers = list.map((diagnostic) => ({
472
+ severity: diagnostic.severity === 1 ? 8 : diagnostic.severity === 2 ? 4 : 2,
473
+ message: diagnostic.message + (diagnostic.source ? ' (' + diagnostic.source + ')' : ''),
474
+ startLineNumber: diagnostic.line,
475
+ startColumn: diagnostic.column,
476
+ endLineNumber: diagnostic.endLine,
477
+ endColumn: diagnostic.endColumn,
478
+ }))
479
+ lspRuntime.markers = markers
480
+ try { lspRuntime.monaco.editor.setModelMarkers(model, 'dsh-lsp', markers) } catch (error) { /* ignore */ }
481
+ } catch (error) {
482
+ if (model === lspRuntime.model && generation === lspRuntime.generation && pollId === lspRuntime.diagnosticsPollId) {
483
+ scheduleLspDiagnostics(Math.max(attempt + 1, LSP_DIAG_FAST_ATTEMPTS + 3), pollId)
484
+ }
485
+ }
486
+ }
487
+
488
+ /**
489
+ * Go-to-definition (F12) for the active document.
490
+ * @param {number} line 1-based line
491
+ * @param {number} column 1-based column
492
+ * @returns {Promise<boolean>} whether a location was opened
493
+ */
494
+ async function lspDefinition(line, column) {
495
+ if (!lspRuntime.host) { lspRuntime.host?.notify?.('LSP 未初始化:页面还没准备好,请重新打开本页', 'warn'); return false }
496
+ const generation = lspRuntime.generation
497
+ const navigationId = ++lspRuntime.navigationId
498
+ const response = await lspRequest('definition', { line, column })
499
+ if (generation !== lspRuntime.generation || navigationId !== lspRuntime.navigationId) return false
500
+ if (noteLspUnavailable(response)) return false
501
+ const host = lspRuntime.host
502
+ if (!response || response.available !== true) {
503
+ host?.notify?.(lspFailureMessage(response, '未找到定义(语言服务器不可用)'), 'warn')
504
+ return false
505
+ }
506
+ const all = response.locations || []
507
+ const locations = all.map((location) => ({ ...location, path: workspaceLocationPath(location.path) })).filter((location) => location.path)
508
+ if (locations.length === 0) {
509
+ if (all.length > 0) {
510
+ host?.notify?.('定义在工作区外的文件:' + all[0].path + ':' + all[0].line + '(本页只能打开工作区内的文件)', 'info')
511
+ } else {
512
+ host?.notify?.('未找到定义(该符号可能来自内置库/依赖,或项目依赖未安装)', 'info')
513
+ }
514
+ return false
515
+ }
516
+ if (locations.length > 1) host?.showLocations?.(locations, 'definitions')
517
+ else host?.openLocation?.(locations[0].path, locations[0].line, locations[0].column, locations[0])
518
+ return true
519
+ }
520
+
521
+ /**
522
+ * Find references (Shift+F12) and list them through the page picker.
523
+ * @param {number} line 1-based line
524
+ * @param {number} column 1-based column
525
+ * @returns {Promise<boolean>} whether any reference was found
526
+ */
527
+ async function lspReferences(line, column) {
528
+ if (!lspRuntime.host) { lspRuntime.host?.notify?.('LSP 未初始化:页面还没准备好,请重新打开本页', 'warn'); return false }
529
+ const generation = lspRuntime.generation
530
+ const navigationId = ++lspRuntime.navigationId
531
+ const response = await lspRequest('references', { line, column })
532
+ if (generation !== lspRuntime.generation || navigationId !== lspRuntime.navigationId) return false
533
+ if (noteLspUnavailable(response)) return false
534
+ const host = lspRuntime.host
535
+ if (!response || response.available !== true) {
536
+ host?.notify?.(lspFailureMessage(response, '查找引用失败(语言服务器不可用)'), 'warn')
537
+ return false
538
+ }
539
+ const all = response.locations || []
540
+ const locations = all.map((location) => ({ ...location, path: workspaceLocationPath(location.path) })).filter((location) => location.path)
541
+ if (locations.length === 0) {
542
+ if (all.length > 0) host?.notify?.('找到 ' + all.length + ' 处引用,但都在工作区外(库/依赖),本页无法定位', 'info')
543
+ else host?.notify?.('没有找到引用', 'info')
544
+ return false
545
+ }
546
+ host?.showLocations?.(locations)
547
+ return true
548
+ }
549
+
550
+ /**
551
+ * Fetch the document outline for the picker.
552
+ * @returns {Promise<Array<object>>}
553
+ */
554
+ async function lspSymbols() {
555
+ const generation = lspRuntime.generation
556
+ const response = await lspRequest('symbols', {})
557
+ if (generation !== lspRuntime.generation) return []
558
+ if (noteLspUnavailable(response)) return []
559
+ if (!response || response.available !== true) return []
560
+ return Array.isArray(response.symbols) ? response.symbols : []
561
+ }
562
+
563
+ /**
564
+ * Register a hover provider for every supported language.
565
+ * @param {any} monaco Monaco namespace
566
+ * @param {object} host page callbacks (`notify`, `onMissing`, `openLocation`, `showLocations`)
567
+ */
568
+ function lspRegisterProviders(monaco, host) {
569
+ lspRuntime.host = host
570
+ disableBuiltinTsValidation(monaco)
571
+ if (!monaco || lspRuntime.registered) return
572
+ lspRuntime.registered = true
573
+ for (const language of LSP_LANGUAGES) {
574
+ try {
575
+ const provider = monaco.languages.registerHoverProvider(language, {
576
+ async provideHover(model, position) {
577
+ if (!lspRuntime.sessionId || model !== lspRuntime.model) return null
578
+ try {
579
+ const generation = lspRuntime.generation
580
+ const response = await lspRequest('hover', { line: position.lineNumber, column: position.column })
581
+ if (generation !== lspRuntime.generation) return null
582
+ if (!response || response.available !== true || !response.contents) return null
583
+ return { contents: [{ value: String(response.contents) }] }
584
+ } catch {
585
+ return null
586
+ }
587
+ },
588
+ })
589
+ lspRuntime.providers.push(provider)
590
+ } catch (error) { /* ignore */ }
591
+ }
592
+ }