dsh-git-changes 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.en.md +105 -0
- package/README.md +122 -2
- package/THIRD_PARTY_NOTICES.md +12 -0
- package/assets/vendor/monaco/LICENSE +21 -0
- package/assets/vendor/monaco/ThirdPartyNotices.txt +448 -0
- package/assets/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.js +7 -0
- package/assets/vendor/monaco/vs/base/common/worker/simpleWorker.nls.zh-cn.js +7 -0
- package/assets/vendor/monaco/vs/base/worker/workerMain.js +26 -0
- package/assets/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/css/css.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/go/go.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/html/html.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/java/java.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/less/less.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/php/php.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
- package/assets/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/python/python.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/r/r.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
- package/assets/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/st/st.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
- package/assets/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
- package/assets/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
- package/assets/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
- package/assets/vendor/monaco/vs/editor/editor.main.css +6 -0
- package/assets/vendor/monaco/vs/editor/editor.main.js +744 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.js +28 -0
- package/assets/vendor/monaco/vs/editor/editor.main.nls.zh-cn.js +30 -0
- package/assets/vendor/monaco/vs/language/css/cssMode.js +13 -0
- package/assets/vendor/monaco/vs/language/css/cssWorker.js +81 -0
- package/assets/vendor/monaco/vs/language/html/htmlMode.js +13 -0
- package/assets/vendor/monaco/vs/language/html/htmlWorker.js +453 -0
- package/assets/vendor/monaco/vs/language/json/jsonMode.js +15 -0
- package/assets/vendor/monaco/vs/language/json/jsonWorker.js +36 -0
- package/assets/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
- package/assets/vendor/monaco/vs/language/typescript/tsWorker.js +37016 -0
- package/assets/vendor/monaco/vs/loader.js +10 -0
- package/cordis.patch.yml +3 -0
- package/docs/images/git-changes-diff.png +0 -0
- package/icon.svg +10 -0
- package/lib/client.js +9418 -0
- package/lib/host/assets.js +161 -0
- package/lib/host/commit-message.js +158 -0
- package/lib/host/files-index.js +206 -0
- package/lib/host/git-write.js +194 -0
- package/lib/host/git.js +1147 -0
- package/lib/host/lsp.js +842 -0
- package/lib/host/routes.js +976 -0
- package/lib/host/session.js +84 -0
- package/lib/host/util.js +107 -0
- package/lib/index.js +33 -0
- package/licenses/dsh-vscode-mode-LICENSE +21 -0
- package/licenses/vscode-theme-seti-LICENSE +21 -0
- package/licenses/vscode-theme-seti-ThirdPartyNotices.txt +32 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +63 -3
- package/src/client/00-const.js +79 -0
- package/src/client/05-utils.js +277 -0
- package/src/client/10-i18n.js +548 -0
- package/src/client/12-vscode-icons.js +460 -0
- package/src/client/15-theme.js +509 -0
- package/src/client/20-monaco.js +1404 -0
- package/src/client/22-lsp.js +592 -0
- package/src/client/25-tree.js +563 -0
- package/src/client/28-changes.js +1163 -0
- package/src/client/30-page.js +2259 -0
- package/src/client/35-pill.js +185 -0
- package/src/client/40-commands.js +248 -0
- package/src/client/45-css.js +357 -0
- package/src/client/47-pickers.js +457 -0
- package/src/client/50-apply.js +207 -0
- 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
|
+
}
|