dsh-vscode-mode 0.1.36 → 0.1.42
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/lib/client.js +728 -28
- package/lib/client.js.map +1 -1
- package/lib/index.js +447 -62
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/monaco/loader.ts +12 -1
- package/src/client/monaco/lsp/lspClient.ts +65 -11
- package/src/client/monaco/lsp/providers.ts +34 -1
- package/src/client/monaco/theme.ts +229 -0
- package/src/client/ui/EditorView.ts +21 -2
- package/src/client/ui/LspSettings.ts +39 -7
- package/src/lsp/client.ts +29 -2
- package/src/lsp/manager.ts +15 -0
- package/src/lsp/providers.ts +203 -24
- package/src/lsp/rpc.ts +86 -4
- package/src/lsp/server.ts +80 -3
- package/src/paths.ts +7 -0
- package/src/rpc.ts +4 -1
- package/src/shared/lsp.ts +24 -0
- package/src/shared/rpc.ts +6 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.42",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 迁移自原 src/client/index.ts 的 MONACO_BASE/LANG_BY_EXT/langOf/loadMonaco,语义不改。
|
|
5
5
|
* 作者 ddj 2026-08-20
|
|
6
6
|
*/
|
|
7
|
+
import { applyTheme, registerThemes } from './theme.js'
|
|
7
8
|
|
|
8
9
|
export const MONACO_BASE = '/edrv/vendor/monaco/vs'
|
|
9
10
|
let monacoPromise = null
|
|
@@ -58,7 +59,12 @@ export const LANG_BY_EXT = {
|
|
|
58
59
|
py: 'python', sh: 'shell', bash: 'shell', zsh: 'shell', ps1: 'powershell',
|
|
59
60
|
lua: 'lua', java: 'java', c: 'c', h: 'c', cpp: 'cpp', cc: 'cpp', cxx: 'cpp', hpp: 'cpp',
|
|
60
61
|
cs: 'csharp', go: 'go', rs: 'rust', rb: 'ruby', php: 'php', sql: 'sql',
|
|
61
|
-
swift: 'swift', kt: 'kotlin', kts: 'kotlin', dart: 'dart', dockerfile: 'dockerfile'
|
|
62
|
+
swift: 'swift', kt: 'kotlin', kts: 'kotlin', dart: 'dart', dockerfile: 'dockerfile',
|
|
63
|
+
// 工程/配置文件(拿到正确语法分色,而非 plaintext)
|
|
64
|
+
csproj: 'xml', props: 'xml', targets: 'xml', plist: 'xml',
|
|
65
|
+
lua51: 'lua', luac: 'lua',
|
|
66
|
+
gitattributes: 'ini', editorconfig: 'ini', env: 'ini', properties: 'ini',
|
|
67
|
+
json5: 'jsonc', log: 'plaintext', txt: 'plaintext',
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
/**
|
|
@@ -96,6 +102,11 @@ export function loadMonaco(onProgress) {
|
|
|
96
102
|
publishStage('core', MONACO_STAGES.core.progress, MONACO_STAGES.core.message)
|
|
97
103
|
window.require(['vs/editor/editor.main'], () => {
|
|
98
104
|
publishStage('ready', MONACO_STAGES.ready.progress, MONACO_STAGES.ready.message)
|
|
105
|
+
// 分色主题注册 + 应用(rich token 配色,替掉内置基础 vs 的少层次着色)
|
|
106
|
+
try {
|
|
107
|
+
registerThemes(window.monaco)
|
|
108
|
+
applyTheme(window.monaco)
|
|
109
|
+
} catch (error) { /* 主题失败不阻塞编辑器 */ }
|
|
99
110
|
resolve(window.monaco)
|
|
100
111
|
}, (err) => fail(new Error('Monaco 模块加载失败:' + String(err))))
|
|
101
112
|
} catch (error) {
|
|
@@ -113,6 +113,14 @@ export async function fetchHover(path, text, position) {
|
|
|
113
113
|
return res.hover || null
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/** 查询当前文档的 semantic tokens(host 已归一化 legend 与 delta data)。 */
|
|
117
|
+
export async function fetchSemanticTokens(path, text) {
|
|
118
|
+
await syncDoc(path, text, true)
|
|
119
|
+
const res = await rpc('edrv.lsp.semanticTokens', { sessionId, path }).catch(() => null)
|
|
120
|
+
if (!res || !res.ok || !res.tokens || !Array.isArray(res.tokens.data)) return null
|
|
121
|
+
return res.tokens
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
/** 拉取服务器状态(带 5s 缓存)。 */
|
|
117
125
|
export async function refreshStatus(force = false) {
|
|
118
126
|
if (!force && Date.now() - statusCache.at < 1500) return statusCache.servers
|
|
@@ -148,6 +156,10 @@ function toClientLocation(loc) {
|
|
|
148
156
|
Math.max(1, (end.line ?? 0) + 1),
|
|
149
157
|
Math.max(1, (end.character ?? 0) + 1),
|
|
150
158
|
),
|
|
159
|
+
// 保留原始 LSP uri/root:后续跳转不能把 Monaco edrv Uri 当作磁盘路径
|
|
160
|
+
lspUri: loc.uri,
|
|
161
|
+
lspRoot: loc.root,
|
|
162
|
+
lspRange: loc.range,
|
|
151
163
|
}
|
|
152
164
|
}
|
|
153
165
|
|
|
@@ -161,32 +173,74 @@ export function toEdrvUri(uri, root) {
|
|
|
161
173
|
|
|
162
174
|
/** 将 LSP 目标解析为当前工作区相对路径(跨文件跳转与 model URI 对齐)。 */
|
|
163
175
|
export function targetOpenPath(loc) {
|
|
164
|
-
return relativeLspPath(loc?.uri, loc?.root)
|
|
176
|
+
return relativeLspPath(loc?.lspUri ?? loc?.uri, loc?.lspRoot ?? loc?.root)
|
|
165
177
|
}
|
|
166
178
|
|
|
167
|
-
/**
|
|
168
|
-
|
|
179
|
+
/**
|
|
180
|
+
* 归一化 Location 的 uri → 工作区路径(剥离 scheme,不补盘符)。
|
|
181
|
+
* 三种输入形态(缺一即导致 scheme 泄漏进文件路径 → 跳转"文件不存在"):
|
|
182
|
+
* - Monaco Uri 对象(toClientLocation 产物,scheme=edrv)→ 取 .path(已是工作区相对路径)
|
|
183
|
+
* - 'edrv:///x' 字符串 → 取路径部分
|
|
184
|
+
* - 'file:///D:/x' → 去 authority 空段保留盘符
|
|
185
|
+
* @author ddj 2026年08月28号
|
|
186
|
+
* @param uri Monaco Uri 对象 / file:// 或 edrv:// URI / 裸路径
|
|
187
|
+
* @returns / 分隔的路径(edrv 相对路径 或 带盘符绝对路径)
|
|
188
|
+
*/
|
|
189
|
+
export function lspUriToAbs(uri) {
|
|
169
190
|
if (!uri) return ''
|
|
170
|
-
|
|
191
|
+
const isObject = typeof uri === 'object'
|
|
192
|
+
// Monaco Uri 对象:优先取其 path(已剥离 scheme)
|
|
193
|
+
const raw = isObject && typeof uri.path === 'string' ? uri.path : String(uri)
|
|
194
|
+
let path = raw
|
|
171
195
|
if (path.startsWith('file://')) {
|
|
172
|
-
try { path = decodeURIComponent(path.replace(/^file
|
|
196
|
+
try { path = decodeURIComponent(path.replace(/^file:\/\/\/?/, '')) } catch (error) { path = path.replace(/^file:\/\/\/?/, '') }
|
|
197
|
+
// file:///D:/x → D:/x(去 authority 空段保留盘符);/home/x 保留前导 /(Unix 绝对路径)
|
|
198
|
+
return path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
|
|
199
|
+
}
|
|
200
|
+
if (path.startsWith('edrv://')) {
|
|
201
|
+
try { path = decodeURIComponent(path.replace(/^edrv:\/\/\/?/, '')) } catch (error) { path = path.replace(/^edrv:\/\/\/?/, '') }
|
|
173
202
|
}
|
|
174
|
-
|
|
203
|
+
// edrv Uri(对象或字符串)→ 工作区相对路径:必须剥前导 /,否则 host 按绝对路径解析丢 cwd
|
|
204
|
+
if (isObject ? uri.scheme === 'edrv' : raw.startsWith('edrv://')) {
|
|
205
|
+
return path.replace(/\\/g, '/').replace(/^\/+/, '')
|
|
206
|
+
}
|
|
207
|
+
return path.replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* file URI/裸路径 → root 下的 / 分隔相对路径。
|
|
212
|
+
* root 匹配返回工作区相对路径;root 不匹配返回完整绝对路径(保留盘符),
|
|
213
|
+
* 供 edrv.read 按绝对路径解析(原先错误剥掉首个 / 导致跳转"文件不存在")。
|
|
214
|
+
* @author ddj 2026年08月28号
|
|
215
|
+
* @param uri file:// URI 或裸路径
|
|
216
|
+
* @param root 工作区根(可选)
|
|
217
|
+
* @returns 可 edrv.read 的路径
|
|
218
|
+
*/
|
|
219
|
+
function relativeLspPath(uri, root) {
|
|
220
|
+
if (!uri) return ''
|
|
221
|
+
const path = lspUriToAbs(uri)
|
|
175
222
|
const normalizedRoot = root ? String(root).replace(/\\/g, '/').replace(/^\/([A-Za-z]:)/, '$1').replace(/\/+$/, '') : ''
|
|
176
223
|
const lowerPath = path.toLowerCase()
|
|
177
224
|
const lowerRoot = normalizedRoot.toLowerCase()
|
|
178
225
|
if (normalizedRoot && (lowerPath === lowerRoot || lowerPath.startsWith(lowerRoot + '/'))) {
|
|
179
226
|
return path.slice(normalizedRoot.length).replace(/^\/+/, '')
|
|
180
227
|
}
|
|
181
|
-
return path
|
|
228
|
+
return path
|
|
182
229
|
}
|
|
183
230
|
|
|
184
231
|
/** 目标行/列(0-based LSP → 1-based Monaco)。 */
|
|
185
232
|
export function targetMonoPosition(loc) {
|
|
186
|
-
|
|
187
|
-
const
|
|
233
|
+
// 优先使用原始 LSP range(0-based);无原始值时兼容 Monaco Range(1-based)
|
|
234
|
+
const lspRange = loc?.lspRange
|
|
235
|
+
if (lspRange?.start) {
|
|
236
|
+
return {
|
|
237
|
+
line: Math.max(1, (lspRange.start.line ?? 0) + 1),
|
|
238
|
+
column: Math.max(1, (lspRange.start.character ?? 0) + 1),
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
const range = loc?.range ?? {}
|
|
188
242
|
return {
|
|
189
|
-
line: Math.max(1,
|
|
190
|
-
column: Math.max(1,
|
|
243
|
+
line: Math.max(1, range.startLineNumber ?? 1),
|
|
244
|
+
column: Math.max(1, range.startColumn ?? 1),
|
|
191
245
|
}
|
|
192
246
|
}
|
|
@@ -8,10 +8,16 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import {
|
|
10
10
|
pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
|
|
11
|
-
fetchDocumentSymbols, fetchHover,
|
|
11
|
+
fetchDocumentSymbols, fetchHover, fetchSemanticTokens,
|
|
12
|
+
targetOpenPath, targetMonoPosition, lspStatusFor, refreshStatus,
|
|
12
13
|
} from './lspClient.js'
|
|
14
|
+
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../../../shared/lsp.js'
|
|
13
15
|
|
|
14
16
|
const LSP_LANGS = ['lua', 'csharp']
|
|
17
|
+
const SEMANTIC_LEGEND = {
|
|
18
|
+
tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
19
|
+
tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
|
|
20
|
+
}
|
|
15
21
|
let registered = false
|
|
16
22
|
const disposables = []
|
|
17
23
|
let overlayEl = null
|
|
@@ -48,6 +54,22 @@ export function registerLspProviders(monaco) {
|
|
|
48
54
|
disposables.push(monaco.editor.onDidCreateModel(attachModel))
|
|
49
55
|
for (const model of monaco.editor.getModels()) attachModel(model)
|
|
50
56
|
|
|
57
|
+
// —— 重新检测事件:保存配置/切换启用/手动重检测后,把已打开模型重新同步给 host ——
|
|
58
|
+
// (host 已重置该语言 server,下一次 sync 会按最新配置重新 acquire,无需重开文件)
|
|
59
|
+
const onRedetect = (event) => {
|
|
60
|
+
const languageId = event?.detail?.languageId
|
|
61
|
+
if (!languageId) return
|
|
62
|
+
for (const model of monaco.editor.getModels()) {
|
|
63
|
+
const path = pathOfModel(model)
|
|
64
|
+
if (!path) continue
|
|
65
|
+
if (model.getLanguageId && model.getLanguageId() !== languageId) continue
|
|
66
|
+
void syncDoc(path, model.getValue(), true)
|
|
67
|
+
}
|
|
68
|
+
void refreshStatus(true)
|
|
69
|
+
}
|
|
70
|
+
window.addEventListener('edrv:lsp-redetect', onRedetect)
|
|
71
|
+
disposables.push(() => window.removeEventListener('edrv:lsp-redetect', onRedetect))
|
|
72
|
+
|
|
51
73
|
// —— 数据 provider ——
|
|
52
74
|
disposables.push(
|
|
53
75
|
monaco.languages.registerDefinitionProvider(LSP_LANGS, {
|
|
@@ -87,6 +109,17 @@ export function registerLspProviders(monaco) {
|
|
|
87
109
|
}
|
|
88
110
|
},
|
|
89
111
|
}),
|
|
112
|
+
monaco.languages.registerDocumentSemanticTokensProvider(LSP_LANGS, {
|
|
113
|
+
getLegend: () => SEMANTIC_LEGEND,
|
|
114
|
+
provideDocumentSemanticTokens: async (model, token) => {
|
|
115
|
+
const path = pathOfModel(model)
|
|
116
|
+
if (!path || token?.isCancellationRequested) return { data: new Uint32Array() }
|
|
117
|
+
const result = await fetchSemanticTokens(path, model.getValue())
|
|
118
|
+
if (!result || token?.isCancellationRequested) return { data: new Uint32Array() }
|
|
119
|
+
return { data: Uint32Array.from(result.data) }
|
|
120
|
+
},
|
|
121
|
+
releaseDocumentSemanticTokens: () => {},
|
|
122
|
+
}),
|
|
90
123
|
)
|
|
91
124
|
}
|
|
92
125
|
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* dsh-vscode-mode client — Monaco 语法分色主题(rich token 配色)。
|
|
4
|
+
* 编辑器原先硬编码 'vs'(内置基础主题),token 分色层次少;
|
|
5
|
+
* 这里定义 edrv-dark / edrv-light 两套完整 token 配色(对齐 VSCode Dark+ 语义分层),
|
|
6
|
+
* 并随 DSH 明暗主题自动切换(跟随文档根 data 主题属性 / prefers-color-scheme)。
|
|
7
|
+
* 作者 ddj 2026-08-28
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const EDRV_DARK = 'edrv-dark'
|
|
11
|
+
export const EDRV_LIGHT = 'edrv-light'
|
|
12
|
+
export const EDRV_FALLBACK = 'vs'
|
|
13
|
+
|
|
14
|
+
/** 语法分色(语义分层:关键字/字符串/数字/注释/函数/变量/类型/运算符/正则/标签/属性…)。 */
|
|
15
|
+
const DARK_RULES = [
|
|
16
|
+
{ token: '', foreground: 'd4d4d4', background: '1e1e1e' },
|
|
17
|
+
{ token: 'comment', foreground: '6a9955', fontStyle: 'italic' },
|
|
18
|
+
{ token: 'comment.doc', foreground: '7ca97c', fontStyle: 'italic' },
|
|
19
|
+
{ token: 'keyword', foreground: 'c586c0' },
|
|
20
|
+
{ token: 'keyword.control', foreground: 'c586c0' },
|
|
21
|
+
{ token: 'keyword.operator', foreground: 'd4d4d4' },
|
|
22
|
+
{ token: 'keyword.json', foreground: '9cdcfe' },
|
|
23
|
+
{ token: 'string', foreground: 'ce9178' },
|
|
24
|
+
{ token: 'string.escape', foreground: 'd7ba7d' },
|
|
25
|
+
{ token: 'string.key.json', foreground: '9cdcfe' },
|
|
26
|
+
{ token: 'string.value.json', foreground: 'ce9178' },
|
|
27
|
+
{ token: 'string.yaml', foreground: 'ce9178' },
|
|
28
|
+
{ token: 'number', foreground: 'b5cea8' },
|
|
29
|
+
{ token: 'number.hex', foreground: 'b5cea8' },
|
|
30
|
+
{ token: 'regexp', foreground: 'd16969' },
|
|
31
|
+
{ token: 'type', foreground: '4ec9b0' },
|
|
32
|
+
{ token: 'type.identifier', foreground: '4ec9b0' },
|
|
33
|
+
{ token: 'identifier', foreground: 'd4d4d4' },
|
|
34
|
+
{ token: 'variable', foreground: '9cdcfe' },
|
|
35
|
+
{ token: 'variable.global', foreground: 'ff6699' },
|
|
36
|
+
{ token: 'variable.local', foreground: '9cdcfe' },
|
|
37
|
+
{ token: 'variable.mutable', foreground: '9cdcfe' },
|
|
38
|
+
{ token: 'variable.predefined', foreground: '4fc1ff' },
|
|
39
|
+
{ token: 'variable.parameter', foreground: '00bfae' },
|
|
40
|
+
{ token: 'parameter', foreground: '00bfae' },
|
|
41
|
+
{ token: 'function', foreground: 'dcdcaa' },
|
|
42
|
+
{ token: 'method', foreground: 'dcdcaa' },
|
|
43
|
+
{ token: 'property', foreground: '9cdcfe' },
|
|
44
|
+
{ token: 'function.identifier', foreground: 'dcdcaa' },
|
|
45
|
+
{ token: 'member', foreground: 'dcdcaa' },
|
|
46
|
+
{ token: 'constant', foreground: '4fc1ff' },
|
|
47
|
+
{ token: 'constant.numeric', foreground: 'b5cea8' },
|
|
48
|
+
{ token: 'constant.language', foreground: '569cd6' },
|
|
49
|
+
{ token: 'tag', foreground: '569cd6' },
|
|
50
|
+
{ token: 'tag.xml', foreground: '569cd6' },
|
|
51
|
+
{ token: 'metatag', foreground: '808080' },
|
|
52
|
+
{ token: 'attribute.name', foreground: '9cdcfe' },
|
|
53
|
+
{ token: 'attribute.value', foreground: 'ce9178' },
|
|
54
|
+
{ token: 'delimiter', foreground: 'd4d4d4' },
|
|
55
|
+
{ token: 'delimiter.bracket', foreground: 'ffd700' },
|
|
56
|
+
{ token: 'delimiter.html', foreground: '808080' },
|
|
57
|
+
{ token: 'operator', foreground: 'd4d4d4' },
|
|
58
|
+
{ token: 'namespace', foreground: '4ec9b0' },
|
|
59
|
+
{ token: 'class', foreground: '4ec9b0' },
|
|
60
|
+
{ token: 'interface', foreground: '4ec9b0' },
|
|
61
|
+
{ token: 'enum', foreground: '4ec9b0' },
|
|
62
|
+
{ token: 'struct', foreground: '4ec9b0' },
|
|
63
|
+
{ token: 'annotation', foreground: 'dcdcaa' },
|
|
64
|
+
{ token: 'punctuation', foreground: 'd4d4d4' },
|
|
65
|
+
{ token: 'invalid', foreground: 'f44747' },
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
const LIGHT_RULES = [
|
|
69
|
+
{ token: '', foreground: '1e1e1e', background: 'ffffff' },
|
|
70
|
+
{ token: 'comment', foreground: '008000', fontStyle: 'italic' },
|
|
71
|
+
{ token: 'comment.doc', foreground: '2e7d32', fontStyle: 'italic' },
|
|
72
|
+
{ token: 'keyword', foreground: '0000ff' },
|
|
73
|
+
{ token: 'keyword.control', foreground: 'af00db' },
|
|
74
|
+
{ token: 'keyword.operator', foreground: '1e1e1e' },
|
|
75
|
+
{ token: 'keyword.json', foreground: '0451a5' },
|
|
76
|
+
{ token: 'string', foreground: 'a31515' },
|
|
77
|
+
{ token: 'string.escape', foreground: 'ee0000' },
|
|
78
|
+
{ token: 'string.key.json', foreground: '0451a5' },
|
|
79
|
+
{ token: 'string.value.json', foreground: 'a31515' },
|
|
80
|
+
{ token: 'string.yaml', foreground: 'a31515' },
|
|
81
|
+
{ token: 'number', foreground: '098658' },
|
|
82
|
+
{ token: 'number.hex', foreground: '098658' },
|
|
83
|
+
{ token: 'regexp', foreground: '811f3f' },
|
|
84
|
+
{ token: 'type', foreground: '267f99' },
|
|
85
|
+
{ token: 'type.identifier', foreground: '267f99' },
|
|
86
|
+
{ token: 'identifier', foreground: '1e1e1e' },
|
|
87
|
+
{ token: 'variable', foreground: '001080' },
|
|
88
|
+
{ token: 'variable.global', foreground: 'c2185b' },
|
|
89
|
+
{ token: 'variable.local', foreground: '001080' },
|
|
90
|
+
{ token: 'variable.mutable', foreground: '001080' },
|
|
91
|
+
{ token: 'variable.predefined', foreground: '001080' },
|
|
92
|
+
{ token: 'variable.parameter', foreground: '008577' },
|
|
93
|
+
{ token: 'parameter', foreground: '008577' },
|
|
94
|
+
{ token: 'function', foreground: '795e26' },
|
|
95
|
+
{ token: 'method', foreground: '795e26' },
|
|
96
|
+
{ token: 'function.identifier', foreground: '795e26' },
|
|
97
|
+
{ token: 'member', foreground: '795e26' },
|
|
98
|
+
{ token: 'property', foreground: '001080' },
|
|
99
|
+
{ token: 'constant', foreground: '0070c1' },
|
|
100
|
+
{ token: 'constant.numeric', foreground: '098658' },
|
|
101
|
+
{ token: 'constant.language', foreground: '0000ff' },
|
|
102
|
+
{ token: 'tag', foreground: '800000' },
|
|
103
|
+
{ token: 'tag.xml', foreground: '800000' },
|
|
104
|
+
{ token: 'metatag', foreground: '6b6b6b' },
|
|
105
|
+
{ token: 'attribute.name', foreground: 'e50000' },
|
|
106
|
+
{ token: 'attribute.value', foreground: '0451a5' },
|
|
107
|
+
{ token: 'delimiter', foreground: '1e1e1e' },
|
|
108
|
+
{ token: 'delimiter.bracket', foreground: 'b46400' },
|
|
109
|
+
{ token: 'delimiter.html', foreground: '800000' },
|
|
110
|
+
{ token: 'operator', foreground: '1e1e1e' },
|
|
111
|
+
{ token: 'namespace', foreground: '267f99' },
|
|
112
|
+
{ token: 'class', foreground: '267f99' },
|
|
113
|
+
{ token: 'interface', foreground: '267f99' },
|
|
114
|
+
{ token: 'enum', foreground: '267f99' },
|
|
115
|
+
{ token: 'struct', foreground: '267f99' },
|
|
116
|
+
{ token: 'annotation', foreground: '795e26' },
|
|
117
|
+
{ token: 'punctuation', foreground: '1e1e1e' },
|
|
118
|
+
{ token: 'invalid', foreground: 'cd3131' },
|
|
119
|
+
]
|
|
120
|
+
|
|
121
|
+
/** 编辑器 UI 配色(与语法配色同套,保证 gutter/minimap/selection 一致)。 */
|
|
122
|
+
const DARK_COLORS = {
|
|
123
|
+
'editor.background': '#1e1e1e',
|
|
124
|
+
'editor.foreground': '#d4d4d4',
|
|
125
|
+
'editorLineNumber.foreground': '#6e7681',
|
|
126
|
+
'editorLineNumber.activeForeground': '#c6c6c6',
|
|
127
|
+
'editor.selectionBackground': '#264f78',
|
|
128
|
+
'editor.inactiveSelectionBackground': '#3a3d41',
|
|
129
|
+
'editor.lineHighlightBackground': '#2a2d2e',
|
|
130
|
+
'editorCursor.foreground': '#aeafad',
|
|
131
|
+
'editorIndentGuide.background1': '#404040',
|
|
132
|
+
'editorIndentGuide.activeBackground1': '#707070',
|
|
133
|
+
'editorWidget.background': '#252526',
|
|
134
|
+
'editorHoverWidget.background': '#252526',
|
|
135
|
+
'editorGutter.background': '#1e1e1e',
|
|
136
|
+
'minimap.background': '#1e1e1e',
|
|
137
|
+
'scrollbarSlider.background': '#79797966',
|
|
138
|
+
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
139
|
+
'scrollbarSlider.activeBackground': '#bfbfbf66',
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const LIGHT_COLORS = {
|
|
143
|
+
'editor.background': '#ffffff',
|
|
144
|
+
'editor.foreground': '#1e1e1e',
|
|
145
|
+
'editorLineNumber.foreground': '#6c6c6c',
|
|
146
|
+
'editorLineNumber.activeForeground': '#171184',
|
|
147
|
+
'editor.selectionBackground': '#add6ff',
|
|
148
|
+
'editor.inactiveSelectionBackground': '#e5ebf1',
|
|
149
|
+
'editor.lineHighlightBackground': '#f0f0f0',
|
|
150
|
+
'editorCursor.foreground': '#000000',
|
|
151
|
+
'editorIndentGuide.background1': '#d3d3d3',
|
|
152
|
+
'editorIndentGuide.activeBackground1': '#939393',
|
|
153
|
+
'editorWidget.background': '#f3f3f3',
|
|
154
|
+
'editorHoverWidget.background': '#f3f3f3',
|
|
155
|
+
'editorGutter.background': '#ffffff',
|
|
156
|
+
'minimap.background': '#ffffff',
|
|
157
|
+
'scrollbarSlider.background': '#64646466',
|
|
158
|
+
'scrollbarSlider.hoverBackground': '#646464b3',
|
|
159
|
+
'scrollbarSlider.activeBackground': '#00000099',
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 注册 edrv 双套主题(幂等;Monaco 未加载时静默跳过)。
|
|
164
|
+
* @author ddj 2026年08月28号
|
|
165
|
+
* @param monaco Monaco 实例(window.monaco)
|
|
166
|
+
*/
|
|
167
|
+
export function registerThemes(monaco) {
|
|
168
|
+
if (!monaco?.editor?.defineTheme) return
|
|
169
|
+
try {
|
|
170
|
+
monaco.editor.defineTheme(EDRV_DARK, {
|
|
171
|
+
base: 'vs-dark',
|
|
172
|
+
inherit: true,
|
|
173
|
+
rules: DARK_RULES,
|
|
174
|
+
colors: DARK_COLORS,
|
|
175
|
+
})
|
|
176
|
+
monaco.editor.defineTheme(EDRV_LIGHT, {
|
|
177
|
+
base: 'vs',
|
|
178
|
+
inherit: true,
|
|
179
|
+
rules: LIGHT_RULES,
|
|
180
|
+
colors: LIGHT_COLORS,
|
|
181
|
+
})
|
|
182
|
+
} catch (error) {
|
|
183
|
+
/* 主题注册失败保留内置 vs */
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 探测当前 DSH 明暗(文档根 data 主题属性 → prefers-color-scheme → 暗)。
|
|
189
|
+
* @author ddj 2026年08月28号
|
|
190
|
+
* @returns 'dark' | 'light'
|
|
191
|
+
*/
|
|
192
|
+
export function detectColorScheme() {
|
|
193
|
+
try {
|
|
194
|
+
const attr = document.documentElement?.getAttribute?.('data-theme')
|
|
195
|
+
?? document.body?.getAttribute?.('data-theme')
|
|
196
|
+
const value = String(attr ?? '').toLowerCase()
|
|
197
|
+
if (value.includes('light') || value.includes('catppuccin-latte')) return 'light'
|
|
198
|
+
if (value.includes('dark') || value.includes('frappe') || value.includes('macchiato') || value.includes('mocha')) return 'dark'
|
|
199
|
+
if (typeof window.matchMedia === 'function') {
|
|
200
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
/* 探测失败回落暗色 */
|
|
204
|
+
}
|
|
205
|
+
return 'dark'
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* 主题名:按当前明暗返回已注册主题(未注册回落 vs)。
|
|
210
|
+
* @author ddj 2026年08月28号
|
|
211
|
+
* @returns 主题 id
|
|
212
|
+
*/
|
|
213
|
+
export function themeNameOf() {
|
|
214
|
+
return detectColorScheme() === 'light' ? EDRV_LIGHT : EDRV_DARK
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* 应用主题到 Monaco(全局切换;新建编辑器也用该主题)。
|
|
219
|
+
* @author ddj 2026年08月28号
|
|
220
|
+
* @param monaco Monaco 实例
|
|
221
|
+
*/
|
|
222
|
+
export function applyTheme(monaco) {
|
|
223
|
+
if (!monaco?.editor?.setTheme) return
|
|
224
|
+
try {
|
|
225
|
+
monaco.editor.setTheme(themeNameOf())
|
|
226
|
+
} catch (error) {
|
|
227
|
+
/* setTheme 失败忽略 */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -9,6 +9,7 @@ import React from 'react'
|
|
|
9
9
|
import { dbg, rpc } from '../rpc.js'
|
|
10
10
|
import { emitRefresh } from '../events.js'
|
|
11
11
|
import { langOf, loadMonaco } from '../monaco/loader.js'
|
|
12
|
+
import { applyTheme, registerThemes, themeNameOf } from '../monaco/theme.js'
|
|
12
13
|
import { createDiffRenderer } from '../monaco/diffRender.js'
|
|
13
14
|
import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
|
|
14
15
|
import { diffRegions } from '../state/regions.js'
|
|
@@ -244,7 +245,9 @@ export function EditorView(props) {
|
|
|
244
245
|
setLoadStage((prev) => ({ progress: Math.max(84, prev.progress), message: '文件已读取,准备创建编辑器…' }))
|
|
245
246
|
setStatus('已加载')
|
|
246
247
|
} else {
|
|
247
|
-
const
|
|
248
|
+
const base = res?.error ? String(res.error) : '读取失败'
|
|
249
|
+
// 带出 host 解析后的真实路径:跳转失败时一眼看出是路径解析错还是目标不存在
|
|
250
|
+
const message = res?.resolvedPath ? base + ':' + String(res.resolvedPath) : base
|
|
248
251
|
setLoadError(message)
|
|
249
252
|
setError(message)
|
|
250
253
|
setStatus('读取失败')
|
|
@@ -496,6 +499,22 @@ export function EditorView(props) {
|
|
|
496
499
|
return () => { alive = false }
|
|
497
500
|
}, [monaco, monacoErr])
|
|
498
501
|
|
|
502
|
+
// 分色主题:Monaco 就绪后注册(幂等)+ 应用;DSH 明暗切换时跟随重刷。
|
|
503
|
+
React.useEffect(() => {
|
|
504
|
+
if (!monaco) return
|
|
505
|
+
registerThemes(monaco)
|
|
506
|
+
applyTheme(monaco)
|
|
507
|
+
if (typeof window.matchMedia !== 'function') return
|
|
508
|
+
const query = window.matchMedia('(prefers-color-scheme: dark)')
|
|
509
|
+
const onChange = () => applyTheme(monaco)
|
|
510
|
+
if (typeof query.addEventListener === 'function') {
|
|
511
|
+
query.addEventListener('change', onChange)
|
|
512
|
+
return () => query.removeEventListener('change', onChange)
|
|
513
|
+
}
|
|
514
|
+
query.addListener(onChange)
|
|
515
|
+
return () => query.removeListener(onChange)
|
|
516
|
+
}, [monaco])
|
|
517
|
+
|
|
499
518
|
monacoRef.current = monaco
|
|
500
519
|
|
|
501
520
|
const getModel = (path, text) => {
|
|
@@ -587,7 +606,7 @@ export function EditorView(props) {
|
|
|
587
606
|
const ed = m.editor.create(node, {
|
|
588
607
|
value: '',
|
|
589
608
|
language: 'plaintext',
|
|
590
|
-
theme:
|
|
609
|
+
theme: themeNameOf(),
|
|
591
610
|
fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
|
|
592
611
|
fontSize: 13,
|
|
593
612
|
lineHeight: 20,
|
|
@@ -10,7 +10,7 @@ import { rpc } from '../rpc.js'
|
|
|
10
10
|
import '../styles/mcp.css'
|
|
11
11
|
|
|
12
12
|
const LANGUAGES = [
|
|
13
|
-
{ id: 'lua', label: 'Lua(LuaLS)', hint: 'lua-language-server
|
|
13
|
+
{ id: 'lua', label: 'Lua(EmmyLua / LuaLS)', hint: '优先使用已安装 EmmyLua;未安装时回退 LuaLS 或 PATH 中的 lua-language-server' },
|
|
14
14
|
{ id: 'csharp', label: 'C#(Roslyn / OmniSharp)', hint: '需 dotnet + ms-dotnettools.csharp 的 .roslyn 服务器,或手动指定' },
|
|
15
15
|
]
|
|
16
16
|
|
|
@@ -25,8 +25,8 @@ const TABS = [
|
|
|
25
25
|
{ id: 'updates', label: '更新' },
|
|
26
26
|
]
|
|
27
27
|
|
|
28
|
-
/** 语言卡片:状态 + 启用开关 +
|
|
29
|
-
function LangCard({ lang, config, status, busy, onToggle, onSave }) {
|
|
28
|
+
/** 语言卡片:状态 + 启用开关 + 命令/路径配置 + 重新检测。 */
|
|
29
|
+
function LangCard({ lang, config, status, busy, onToggle, onSave, onRedetect }) {
|
|
30
30
|
const [pathDraft, setPathDraft] = React.useState(config?.path ?? '')
|
|
31
31
|
const [commandDraft, setCommandDraft] = React.useState(config?.command ?? '')
|
|
32
32
|
React.useEffect(() => { setPathDraft(config?.path ?? ''); setCommandDraft(config?.command ?? '') }, [config?.path, config?.command])
|
|
@@ -40,12 +40,15 @@ function LangCard({ lang, config, status, busy, onToggle, onSave }) {
|
|
|
40
40
|
React.createElement('button', { className: 'vsm-switch ' + (config?.enabled !== false ? 'on' : ''), onClick: () => onToggle(lang.id, config?.enabled !== false ? false : true), 'aria-label': '启用/禁用' }, config?.enabled !== false ? '●' : '○'))),
|
|
41
41
|
React.createElement('div', { className: 'vsm-mcp-meta' },
|
|
42
42
|
PHASE_LABEL[phase] ?? phase, ' · ', SOURCE_LABEL[status?.source] ?? status?.source,
|
|
43
|
+
status?.providerName ? ' · ' + status.providerName : '',
|
|
44
|
+
status?.version ? ' · v' + status.version : '',
|
|
43
45
|
(phase === 'ready' && status?.root) ? ' · ' + String(status.root).split(/[\\/]/).pop() : ''),
|
|
44
46
|
status?.reason ? React.createElement('div', { className: 'vsm-mcp-error' }, status.reason) : null,
|
|
45
47
|
React.createElement('div', { className: 'vsm-lsp-form' },
|
|
46
48
|
React.createElement('label', null, React.createElement('span', null, '可执行文件路径(绝对路径,优先)'), React.createElement('input', { value: pathDraft, placeholder: '如 C:/.../lua-language-server.exe', onChange: (e) => setPathDraft(e.target.value) })),
|
|
47
49
|
React.createElement('label', null, React.createElement('span', null, '命令名(PATH 内查找)'), React.createElement('input', { value: commandDraft, placeholder: '如 lua-language-server', onChange: (e) => setCommandDraft(e.target.value) })),
|
|
48
|
-
React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置')
|
|
50
|
+
React.createElement('button', { className: 'vsm-primary', disabled: busy === lang.id, onClick: () => onSave(lang.id, pathDraft, commandDraft) }, busy === lang.id ? '保存中…' : '保存配置'),
|
|
51
|
+
React.createElement('button', { className: 'vsm-primary vsm-small', disabled: busy === lang.id, onClick: () => onRedetect(lang.id), title: '清缓存并重新选择语言服务器(EmmyLua / LuaLS)' }, busy === lang.id ? '检测中…' : '重新检测')),
|
|
49
52
|
React.createElement('div', { className: 'vsm-lsp-hint' }, lang.hint))
|
|
50
53
|
}
|
|
51
54
|
|
|
@@ -129,6 +132,18 @@ export function LspSettings() {
|
|
|
129
132
|
if (next === 'servers') void refreshServers()
|
|
130
133
|
}
|
|
131
134
|
|
|
135
|
+
/** 合并 redetect/configUpdate 返回的服务器状态:只替换该语言条目。 */
|
|
136
|
+
const mergeServers = (languageId, list) => {
|
|
137
|
+
setServers((prev) => Array.isArray(list)
|
|
138
|
+
? [...prev.filter((s) => s.languageId !== languageId), ...list]
|
|
139
|
+
: prev)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 重检测后通知编辑器:与 host 重新同步已打开的模型(触发重新 acquire)。 */
|
|
143
|
+
const notifyResync = (languageId) => {
|
|
144
|
+
window.dispatchEvent(new CustomEvent('edrv:lsp-redetect', { detail: { languageId } }))
|
|
145
|
+
}
|
|
146
|
+
|
|
132
147
|
const saveLang = (languageId, path, command) => {
|
|
133
148
|
setBusy(languageId)
|
|
134
149
|
setError('')
|
|
@@ -136,6 +151,8 @@ export function LspSettings() {
|
|
|
136
151
|
.then((res) => {
|
|
137
152
|
if (!res?.ok) { setError(res?.error ?? '保存失败'); return }
|
|
138
153
|
setConfig(res.config ?? {})
|
|
154
|
+
mergeServers(languageId, res.servers)
|
|
155
|
+
notifyResync(languageId) // 保存后自动重新检测 + 同步模型
|
|
139
156
|
})
|
|
140
157
|
.catch((e) => setError(String(e)))
|
|
141
158
|
.finally(() => setBusy(''))
|
|
@@ -145,8 +162,23 @@ export function LspSettings() {
|
|
|
145
162
|
setBusy(languageId)
|
|
146
163
|
rpc('edrv.lsp.configUpdate', { languageId, enabled })
|
|
147
164
|
.then((res) => {
|
|
148
|
-
if (res?.ok) {
|
|
149
|
-
|
|
165
|
+
if (!res?.ok) { setError(res?.error ?? '切换失败'); return }
|
|
166
|
+
setConfig(res.config ?? {})
|
|
167
|
+
mergeServers(languageId, res.servers)
|
|
168
|
+
notifyResync(languageId) // 切换启用状态后自动重新检测 + 同步模型
|
|
169
|
+
})
|
|
170
|
+
.catch((e) => setError(String(e)))
|
|
171
|
+
.finally(() => setBusy(''))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const redetectLang = (languageId) => {
|
|
175
|
+
setBusy(languageId)
|
|
176
|
+
setError('')
|
|
177
|
+
rpc('edrv.lsp.redetect', { languageId })
|
|
178
|
+
.then((res) => {
|
|
179
|
+
if (!res?.ok) { setError(res?.error ?? '重新检测失败'); return }
|
|
180
|
+
mergeServers(languageId, res.servers)
|
|
181
|
+
notifyResync(languageId)
|
|
150
182
|
})
|
|
151
183
|
.catch((e) => setError(String(e)))
|
|
152
184
|
.finally(() => setBusy(''))
|
|
@@ -239,7 +271,7 @@ export function LspSettings() {
|
|
|
239
271
|
key: lang.id, lang,
|
|
240
272
|
config: config[lang.id] ?? {},
|
|
241
273
|
status: servers.find((s) => s.languageId === lang.id),
|
|
242
|
-
busy, onToggle: toggleLang, onSave: saveLang,
|
|
274
|
+
busy, onToggle: toggleLang, onSave: saveLang, onRedetect: redetectLang,
|
|
243
275
|
})),
|
|
244
276
|
React.createElement('div', { className: 'vsm-lsp-hint' }, '提示:打开 Lua/C# 文件即自动(惰性)启动对应语言服务器;未配置时编辑器功能不受影响,大纲回退内置解析。')))
|
|
245
277
|
} else if (tab === 'installed') {
|
package/src/lsp/client.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* 作者 ddj 2026-08-27
|
|
6
6
|
*/
|
|
7
7
|
import { createFrameParser, encodeMessage, requestMessage, notifyMessage } from './jsonrpc.js'
|
|
8
|
-
import type
|
|
8
|
+
import { LSP_SEMANTIC_TOKEN_TYPES, LSP_SEMANTIC_TOKEN_MODIFIERS, type LspServerCapabilities } from '../shared/lsp.js'
|
|
9
9
|
|
|
10
10
|
export interface LspClientEvents {
|
|
11
11
|
onReady?: (capabilities: LspServerCapabilities) => void
|
|
@@ -54,7 +54,16 @@ export function createLspClient(
|
|
|
54
54
|
let nextId = 1
|
|
55
55
|
let alive = transport.alive
|
|
56
56
|
const pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void; timer: ReturnType<typeof setTimeout> }>()
|
|
57
|
-
let serverCapabilities: LspServerCapabilities = {
|
|
57
|
+
let serverCapabilities: LspServerCapabilities = {
|
|
58
|
+
definition: false,
|
|
59
|
+
references: false,
|
|
60
|
+
documentSymbol: false,
|
|
61
|
+
workspaceSymbol: false,
|
|
62
|
+
hover: false,
|
|
63
|
+
semanticTokens: false,
|
|
64
|
+
semanticTokenTypes: [],
|
|
65
|
+
semanticTokenModifiers: [],
|
|
66
|
+
}
|
|
58
67
|
|
|
59
68
|
const log = (line: string): void => events.onLog?.(line)
|
|
60
69
|
const parser = createFrameParser()
|
|
@@ -153,6 +162,9 @@ export function createLspClient(
|
|
|
153
162
|
documentSymbolProvider?: unknown
|
|
154
163
|
workspaceSymbolProvider?: unknown
|
|
155
164
|
hoverProvider?: unknown
|
|
165
|
+
semanticTokensProvider?: {
|
|
166
|
+
legend?: { tokenTypes?: unknown; tokenModifiers?: unknown }
|
|
167
|
+
}
|
|
156
168
|
}
|
|
157
169
|
}>('initialize', {
|
|
158
170
|
processId: process.pid,
|
|
@@ -170,16 +182,31 @@ export function createLspClient(
|
|
|
170
182
|
references: { dynamicRegistration: false },
|
|
171
183
|
documentSymbol: { dynamicRegistration: false },
|
|
172
184
|
hover: { dynamicRegistration: false },
|
|
185
|
+
semanticTokens: {
|
|
186
|
+
dynamicRegistration: false,
|
|
187
|
+
requests: { range: false, full: { delta: false } },
|
|
188
|
+
tokenTypes: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
189
|
+
tokenModifiers: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
|
|
190
|
+
formats: ['relative'],
|
|
191
|
+
},
|
|
173
192
|
},
|
|
174
193
|
},
|
|
175
194
|
})
|
|
176
195
|
const caps = init?.capabilities ?? {}
|
|
196
|
+
const legend = caps.semanticTokensProvider?.legend
|
|
177
197
|
serverCapabilities = {
|
|
178
198
|
definition: Boolean(caps.definitionProvider),
|
|
179
199
|
references: Boolean(caps.referencesProvider),
|
|
180
200
|
documentSymbol: Boolean(caps.documentSymbolProvider),
|
|
181
201
|
workspaceSymbol: Boolean(caps.workspaceSymbolProvider),
|
|
182
202
|
hover: Boolean(caps.hoverProvider),
|
|
203
|
+
semanticTokens: Boolean(caps.semanticTokensProvider),
|
|
204
|
+
semanticTokenTypes: Array.isArray(legend?.tokenTypes)
|
|
205
|
+
? legend.tokenTypes.filter((item): item is string => typeof item === 'string')
|
|
206
|
+
: [...LSP_SEMANTIC_TOKEN_TYPES],
|
|
207
|
+
semanticTokenModifiers: Array.isArray(legend?.tokenModifiers)
|
|
208
|
+
? legend.tokenModifiers.filter((item): item is string => typeof item === 'string')
|
|
209
|
+
: [...LSP_SEMANTIC_TOKEN_MODIFIERS],
|
|
183
210
|
}
|
|
184
211
|
client.notify('initialized', {})
|
|
185
212
|
events.onReady?.(serverCapabilities)
|