dsh-vscode-mode 0.1.32 → 0.1.36
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/lib/client.js +1229 -124
- package/lib/client.js.map +1 -1
- package/lib/index.js +1959 -52
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +15 -2
- package/src/client/monaco/lsp/index.ts +30 -0
- package/src/client/monaco/lsp/lspClient.ts +192 -0
- package/src/client/monaco/lsp/providers.ts +229 -0
- package/src/client/outline/OutlinePanel.tsx +1 -1
- package/src/client/outline/lspSource.ts +49 -0
- package/src/client/outline/parse.ts +31 -1
- package/src/client/outline/types.ts +2 -0
- package/src/client/paths.ts +2 -0
- package/src/client/state/workspaceFoldCache.ts +95 -0
- package/src/client/styles/editor.css +15 -0
- package/src/client/styles/mcp.css +50 -12
- package/src/client/ui/EditorView.ts +55 -1
- package/src/client/ui/KeybindingsPanel.ts +8 -5
- package/src/client/ui/LspSettings.ts +274 -0
- package/src/client/ui/McpSettings.ts +112 -49
- package/src/client/ui/PerfSettings.ts +145 -34
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +20 -2
- package/src/lsp/client.ts +205 -0
- package/src/lsp/config.ts +107 -0
- package/src/lsp/extmgr.ts +208 -0
- package/src/lsp/jsonrpc.ts +86 -0
- package/src/lsp/manager.ts +133 -0
- package/src/lsp/providers.ts +235 -0
- package/src/lsp/rpc.ts +334 -0
- package/src/lsp/server.ts +346 -0
- package/src/lsp/settings.ts +44 -0
- package/src/lsp/transport.ts +143 -0
- package/src/lsp/uri.ts +62 -0
- package/src/lsp/zip.ts +82 -0
- package/src/rpc.ts +11 -2
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +33 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 配置设置 section 安装(动态依赖,缺失不抛错)。
|
|
3
|
+
* ns:dsh-vscode-mode.languageServers;每语言 { enabled, command, path }。
|
|
4
|
+
* 作者 ddj 2026-08-27
|
|
5
|
+
*/
|
|
6
|
+
import type { Ctx } from '../store.js'
|
|
7
|
+
import { LSP_LANGUAGES, LSP_SETTINGS_NS, type LspConfig } from './config.js'
|
|
8
|
+
import { loadSettingsDeps, type SettingsDeps } from '../fileOpenSettings.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 安装 LSP 设置 section。
|
|
12
|
+
* @author ddj 2026年08月27号
|
|
13
|
+
* @param ctx DSH 上下文
|
|
14
|
+
* @param entry 初始配置
|
|
15
|
+
* @returns 是否成功安装
|
|
16
|
+
*/
|
|
17
|
+
export async function installLspSettingsSection(ctx: Ctx, entry: LspConfig): Promise<boolean> {
|
|
18
|
+
const deps = await loadSettingsDeps()
|
|
19
|
+
if (!deps) return false
|
|
20
|
+
try {
|
|
21
|
+
// schemastery 无 .optional(),对象字段默认即可选;.default 必给避免 undefined 覆盖
|
|
22
|
+
const z = deps.z
|
|
23
|
+
const langShape: Record<string, unknown> = {}
|
|
24
|
+
for (const lang of LSP_LANGUAGES) {
|
|
25
|
+
langShape[lang] = z.object({
|
|
26
|
+
enabled: z.boolean(),
|
|
27
|
+
command: z.string(),
|
|
28
|
+
path: z.string(),
|
|
29
|
+
}).default({})
|
|
30
|
+
}
|
|
31
|
+
const schema = z.object(langShape).default({})
|
|
32
|
+
deps.installSettingsSection(ctx, LSP_SETTINGS_NS, schema, entry, {
|
|
33
|
+
setSource: () => {},
|
|
34
|
+
onChange: () => {},
|
|
35
|
+
})
|
|
36
|
+
return true
|
|
37
|
+
} catch (error) {
|
|
38
|
+
// 设置 section 安装失败不致命:降级为无设置 UI,LSP 核心功能不受影响
|
|
39
|
+
console.error('[dsh-vscode-mode] LSP 设置 section 安装失败:' + String(error))
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export type { SettingsDeps }
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — LSP 语言服务器进程传输层。
|
|
3
|
+
* 基于 node:child_process.spawn(stdio pipe 双向流)。刻意不用 DSH ctx.subprocess
|
|
4
|
+
* 抽象:LSP 需要持续写 stdin + 流式读 stdout,而该抽象 stdin 不可写、输出为一次性收集。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { spawn, type ChildProcess } from 'node:child_process'
|
|
8
|
+
import { createFrameParser } from './jsonrpc.js'
|
|
9
|
+
|
|
10
|
+
const STDERR_CAP = 256 << 10
|
|
11
|
+
|
|
12
|
+
export interface TransportSpec {
|
|
13
|
+
argv: string[]
|
|
14
|
+
cwd?: string
|
|
15
|
+
env?: Record<string, string>
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface Transport {
|
|
19
|
+
/** 写一行帧到 stdin(自动加换行)。 */
|
|
20
|
+
write(chunk: Buffer): boolean
|
|
21
|
+
/** 推送 stdout 解析出的原始消息(内部由帧解析器调用)。 */
|
|
22
|
+
onMessage: ((message: unknown) => void) | null
|
|
23
|
+
/** stderr 收集缓冲(有界,供诊断)。 */
|
|
24
|
+
stderr(): string
|
|
25
|
+
/** 进程退出/崩溃回调(code/signal)。 */
|
|
26
|
+
onExit: ((code: number | null, signal: string | null) => void) | null
|
|
27
|
+
/** 优雅关闭后强杀(Windows 下连带进程树)。 */
|
|
28
|
+
dispose(): void
|
|
29
|
+
/** 是否仍存活。 */
|
|
30
|
+
readonly alive: boolean
|
|
31
|
+
readonly pid: number | undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const running = new Set<ChildProcess>()
|
|
35
|
+
|
|
36
|
+
/** 当前仍在运行的 LSP 子进程(供 disposeAll / 诊断)。 */
|
|
37
|
+
export function liveChildren(): ReadonlySet<ChildProcess> {
|
|
38
|
+
return running
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 启动语言服务器进程。
|
|
43
|
+
* @author ddj 2026年08月27号
|
|
44
|
+
* @param spec 启动参数(argv 数组,不经 shell,Windows 引号安全)
|
|
45
|
+
* @param onMessage stdout 消息回调
|
|
46
|
+
* @param logger 诊断日志(可选)
|
|
47
|
+
* @returns 传输句柄
|
|
48
|
+
*/
|
|
49
|
+
export function spawnServer(spec: TransportSpec, onMessage: (message: unknown) => void, logger?: (line: string) => void): Transport {
|
|
50
|
+
const { argv, cwd, env } = spec
|
|
51
|
+
const child = spawn(argv[0], argv.slice(1), {
|
|
52
|
+
cwd,
|
|
53
|
+
env: { ...process.env, ...env },
|
|
54
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
55
|
+
windowsHide: true,
|
|
56
|
+
})
|
|
57
|
+
running.add(child)
|
|
58
|
+
let stderrBuf = Buffer.alloc(0)
|
|
59
|
+
let alive = true
|
|
60
|
+
const parser = createFrameParser()
|
|
61
|
+
|
|
62
|
+
child.stderr.on('data', (chunk: Buffer) => {
|
|
63
|
+
if (stderrBuf.length < STDERR_CAP) {
|
|
64
|
+
stderrBuf = Buffer.concat([stderrBuf, chunk], Math.min(stderrBuf.length + chunk.length, STDERR_CAP))
|
|
65
|
+
}
|
|
66
|
+
logger?.(chunk.toString('utf8'))
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
let exited = false
|
|
70
|
+
const emitExit = (code: number | null, signal: string | null): void => {
|
|
71
|
+
if (exited) return
|
|
72
|
+
exited = true
|
|
73
|
+
alive = false
|
|
74
|
+
running.delete(child)
|
|
75
|
+
transport.onExit?.(code, signal)
|
|
76
|
+
}
|
|
77
|
+
child.on('error', (error) => {
|
|
78
|
+
logger?.('spawn error: ' + String(error))
|
|
79
|
+
emitExit(null, null)
|
|
80
|
+
})
|
|
81
|
+
child.on('exit', (code, signal) => emitExit(code, signal))
|
|
82
|
+
child.on('close', (code, signal) => emitExit(code, signal))
|
|
83
|
+
|
|
84
|
+
const transport: Transport = {
|
|
85
|
+
write(chunk: Buffer): boolean {
|
|
86
|
+
if (!alive || !child.stdin.writable) return false
|
|
87
|
+
child.stdin.write(chunk)
|
|
88
|
+
return true
|
|
89
|
+
},
|
|
90
|
+
onMessage,
|
|
91
|
+
stderr(): string {
|
|
92
|
+
return stderrBuf.toString('utf8')
|
|
93
|
+
},
|
|
94
|
+
onExit: null,
|
|
95
|
+
dispose(): void {
|
|
96
|
+
if (!alive) return
|
|
97
|
+
// 先 SIGTERM 让服务器有机会清理,1.5s 后强杀进程树
|
|
98
|
+
child.kill()
|
|
99
|
+
const timer = setTimeout(() => {
|
|
100
|
+
if (!alive) return
|
|
101
|
+
alive = false
|
|
102
|
+
running.delete(child)
|
|
103
|
+
try {
|
|
104
|
+
if (process.platform === 'win32') {
|
|
105
|
+
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
|
|
106
|
+
} else {
|
|
107
|
+
child.kill('SIGKILL')
|
|
108
|
+
}
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger?.('force kill error: ' + String(error))
|
|
111
|
+
}
|
|
112
|
+
}, 1500)
|
|
113
|
+
timer.unref?.()
|
|
114
|
+
},
|
|
115
|
+
get alive() {
|
|
116
|
+
return alive
|
|
117
|
+
},
|
|
118
|
+
get pid() {
|
|
119
|
+
return child.pid
|
|
120
|
+
},
|
|
121
|
+
}
|
|
122
|
+
// stdout 帧解析 → onMessage(LSP 响应/通知;onMessage 由 client 接管后动态生效)
|
|
123
|
+
child.stdout.on('data', (chunk: Buffer) => {
|
|
124
|
+
for (const message of parser.push(chunk)) transport.onMessage?.(message)
|
|
125
|
+
})
|
|
126
|
+
return transport
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 强杀全部运行中的 LSP 子进程(插件卸载/退出用)。 */
|
|
130
|
+
export function disposeAllServers(): void {
|
|
131
|
+
for (const child of running) {
|
|
132
|
+
try {
|
|
133
|
+
if (process.platform === 'win32' && child.pid) {
|
|
134
|
+
spawn('taskkill', ['/PID', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' })
|
|
135
|
+
} else {
|
|
136
|
+
child.kill('SIGKILL')
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
/* 忽略单个清理失败 */
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
running.clear()
|
|
143
|
+
}
|
package/src/lsp/uri.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 路径 ↔ file:// URI 转换(纯函数,node 可测)。
|
|
3
|
+
* LSP 的 uri 字段必须是 file:// 绝对 URI;客户端 Monaco 用 edrv:///<encodeURI(path)>。
|
|
4
|
+
* 作者 ddj 2026-08-27
|
|
5
|
+
*/
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
7
|
+
import { isAbsolute, resolve as resolvePath } from 'node:path'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 绝对路径 → file:// URI(Windows 盘符保留:D:/x → file:///D:/x)。
|
|
11
|
+
* @author ddj 2026年08月27号
|
|
12
|
+
* @param absPath 绝对路径(正/反斜杠均可)
|
|
13
|
+
* @returns file:// URI
|
|
14
|
+
*/
|
|
15
|
+
export function pathToFileUri(absPath: string): string {
|
|
16
|
+
const normalized = absPath.replace(/\\/g, '/')
|
|
17
|
+
const drive = /^([A-Za-z]:)(?:\/(.*))?$/.exec(normalized)
|
|
18
|
+
if (drive) {
|
|
19
|
+
const tail = drive[2] ? '/' + drive[2].split('/').map(encodeURIComponent).join('/') : ''
|
|
20
|
+
return 'file:///' + drive[1] + tail
|
|
21
|
+
}
|
|
22
|
+
return pathToFileURL(normalized).toString()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* file:// URI → 绝对路径(Windows file:///C:/x → C:\x)。
|
|
27
|
+
* @author ddj 2026年08月27号
|
|
28
|
+
* @param uri file:// URI
|
|
29
|
+
* @returns 绝对路径
|
|
30
|
+
*/
|
|
31
|
+
export function fileUriToPath(fileUri: string): string {
|
|
32
|
+
try {
|
|
33
|
+
return fileURLToPath(fileUri)
|
|
34
|
+
} catch (error) {
|
|
35
|
+
return String(error)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 将(可能相对的)路径解析为绝对路径后转 file:// URI。
|
|
41
|
+
* @author ddj 2026年08月27号
|
|
42
|
+
* @param path 相对或绝对路径
|
|
43
|
+
* @param cwd 解析相对路径的基准(可选)
|
|
44
|
+
* @returns file:// URI
|
|
45
|
+
*/
|
|
46
|
+
export function resolveToFileUri(path: string, cwd?: string): string {
|
|
47
|
+
const abs = isAbsolute(path) ? path : cwd ? resolvePath(cwd, path) : resolvePath(path)
|
|
48
|
+
return pathToFileUri(abs)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 判定绝对路径是否在给定根目录(含)内。
|
|
53
|
+
* @author ddj 2026年08月27号
|
|
54
|
+
* @param root 根目录绝对路径
|
|
55
|
+
* @param target 目标绝对路径
|
|
56
|
+
* @returns 是否在根内
|
|
57
|
+
*/
|
|
58
|
+
export function isInside(root: string, target: string): boolean {
|
|
59
|
+
const r = root.replace(/\\/g, '/').replace(/\/+$/, '') + '/'
|
|
60
|
+
const t = target.replace(/\\/g, '/')
|
|
61
|
+
return t === r.slice(0, -1) || t.startsWith(r)
|
|
62
|
+
}
|
package/src/lsp/zip.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 纯 Node ZIP 解压(零依赖,跨平台)。
|
|
3
|
+
* 供 extmgr 解包 VSIX 使用;只实现 VSIX 需要的子集:
|
|
4
|
+
* 读取 EOCD + 中央目录,支持 stored(0)/deflate(8),忽略 zip64(VSIX < 4GB)。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
import { inflateRawSync } from 'node:zlib'
|
|
8
|
+
|
|
9
|
+
/** 单条解压结果:相对路径 + 内容(目录条目 path 以 / 结尾)。 */
|
|
10
|
+
export interface ZipEntry {
|
|
11
|
+
path: string
|
|
12
|
+
data: Buffer
|
|
13
|
+
isDirectory: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const EOCD_SIG = 0x06054b50
|
|
17
|
+
const CDIR_SIG = 0x02014b50
|
|
18
|
+
const LHDR_SIG = 0x04034b50
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 解析 ZIP 缓冲区 → 条目列表(不解压内容,仅元数据)。
|
|
22
|
+
* @author ddj 2026年08月27号
|
|
23
|
+
* @param buf zip 字节
|
|
24
|
+
* @returns 条目元数据
|
|
25
|
+
*/
|
|
26
|
+
export function zipEntries(buf: Buffer): { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] {
|
|
27
|
+
const eocd = findEocd(buf)
|
|
28
|
+
if (eocd < 0) throw new Error('不是有效的 ZIP 文件(找不到 EOCD)')
|
|
29
|
+
const total = buf.readUInt16LE(eocd + 10)
|
|
30
|
+
let offset = buf.readUInt32LE(eocd + 16)
|
|
31
|
+
const out: { name: string; method: number; compressed: number; uncompressed: number; localOffset: number }[] = []
|
|
32
|
+
for (let i = 0; i < total; i++) {
|
|
33
|
+
if (buf.readUInt32LE(offset) !== CDIR_SIG) throw new Error('ZIP 中央目录损坏')
|
|
34
|
+
const method = buf.readUInt16LE(offset + 10)
|
|
35
|
+
const compressed = buf.readUInt32LE(offset + 20)
|
|
36
|
+
const uncompressed = buf.readUInt32LE(offset + 24)
|
|
37
|
+
const nameLen = buf.readUInt16LE(offset + 28)
|
|
38
|
+
const extraLen = buf.readUInt16LE(offset + 30)
|
|
39
|
+
const commentLen = buf.readUInt16LE(offset + 32)
|
|
40
|
+
const localOffset = buf.readUInt32LE(offset + 42)
|
|
41
|
+
const name = buf.subarray(offset + 46, offset + 46 + nameLen).toString('utf8')
|
|
42
|
+
out.push({ name, method, compressed, uncompressed, localOffset })
|
|
43
|
+
offset += 46 + nameLen + extraLen + commentLen
|
|
44
|
+
}
|
|
45
|
+
return out
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 解压 ZIP → 条目列表(目录条目含尾部 /)。
|
|
50
|
+
* @author ddj 2026年08月27号
|
|
51
|
+
* @param buf zip 字节
|
|
52
|
+
* @returns 条目(含内容)
|
|
53
|
+
*/
|
|
54
|
+
export function unzip(buf: Buffer): ZipEntry[] {
|
|
55
|
+
const metas = zipEntries(buf)
|
|
56
|
+
const out: ZipEntry[] = []
|
|
57
|
+
for (const meta of metas) {
|
|
58
|
+
const lh = meta.localOffset
|
|
59
|
+
if (buf.readUInt32LE(lh) !== LHDR_SIG) throw new Error('ZIP 本地头损坏:' + meta.name)
|
|
60
|
+
const nameLen = buf.readUInt16LE(lh + 26)
|
|
61
|
+
const extraLen = buf.readUInt16LE(lh + 28)
|
|
62
|
+
const dataStart = lh + 30 + nameLen + extraLen
|
|
63
|
+
const isDir = /\/$/.test(meta.name)
|
|
64
|
+
let data: Buffer = Buffer.alloc(0)
|
|
65
|
+
if (!isDir && meta.compressed > 0) {
|
|
66
|
+
const raw = buf.subarray(dataStart, dataStart + meta.compressed)
|
|
67
|
+
data = meta.method === 8 ? Buffer.from(inflateRawSync(raw)) : meta.method === 0 ? Buffer.from(raw) : Buffer.alloc(0)
|
|
68
|
+
}
|
|
69
|
+
out.push({ path: meta.name, data, isDirectory: isDir })
|
|
70
|
+
}
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** 定位 EOCD(从尾部向前扫,容忍尾部注释)。 */
|
|
75
|
+
function findEocd(buf: Buffer): number {
|
|
76
|
+
const maxScan = Math.min(buf.length, 65557)
|
|
77
|
+
for (let i = buf.length - 22; i >= buf.length - maxScan; i--) {
|
|
78
|
+
if (i < 0) break
|
|
79
|
+
if (buf.readUInt32LE(i) === EOCD_SIG) return i
|
|
80
|
+
}
|
|
81
|
+
return -1
|
|
82
|
+
}
|
package/src/rpc.ts
CHANGED
|
@@ -216,8 +216,16 @@ async function latestPatchBackup(patchPath: string): Promise<string | undefined>
|
|
|
216
216
|
}
|
|
217
217
|
|
|
218
218
|
/** 各方法 handler 表(类型由 shared/rpc 的 RpcHandlerMap 约束)。 */
|
|
219
|
-
export function buildHandlers(
|
|
219
|
+
export function buildHandlers(
|
|
220
|
+
ctx: Ctx,
|
|
221
|
+
registry: Registry,
|
|
222
|
+
searcher = newSearcher(ctx),
|
|
223
|
+
contentSearcher = newContentSearcher(ctx),
|
|
224
|
+
lspHandlers?: Partial<RpcHandlerMap>,
|
|
225
|
+
): RpcHandlerMap {
|
|
220
226
|
return {
|
|
227
|
+
// edrv.lsp.* 由 createLspRpc 一次性提供(tracker 跨请求保留),这里并入。
|
|
228
|
+
...((lspHandlers ?? {}) as RpcHandlerMap),
|
|
221
229
|
'edrv.list': async (args) => {
|
|
222
230
|
const sc = await requireSession(ctx, args.sessionId)
|
|
223
231
|
if ('err' in sc) return { ok: false, error: sc.err }
|
|
@@ -664,8 +672,9 @@ export async function handleRpc<M extends RpcMethod>(
|
|
|
664
672
|
args: RpcRequestMap[M],
|
|
665
673
|
searcher = newSearcher(ctx),
|
|
666
674
|
contentSearcher = newContentSearcher(ctx),
|
|
675
|
+
lspHandlers?: Partial<RpcHandlerMap>,
|
|
667
676
|
): Promise<RpcResult<M>> {
|
|
668
|
-
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher)
|
|
677
|
+
const handlers = buildHandlers(ctx, registry, searcher, contentSearcher, lspHandlers)
|
|
669
678
|
const handler = handlers[method]
|
|
670
679
|
if (!handler) return { ok: false, error: '未知方法: ' + String(method) } as RpcResult<M>
|
|
671
680
|
return handler(args)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode shared — 语言服务器 RPC 契约(双面,禁 node/react)。
|
|
3
|
+
* LSP 请求/响应的 JSON 载荷形状 + 纯函数辅助(Monaco↔LSP 坐标换算等)。
|
|
4
|
+
* 只描述"过 RPC 的载荷",宿主与语言服务器之间的帧级交互在 host 侧 src/lsp/ 内。
|
|
5
|
+
* 作者 ddj 2026-08-27
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** LSP Position(0-based:line 行号从 0,character UTF-16 列从 0)。 */
|
|
9
|
+
export interface LspPosition {
|
|
10
|
+
line: number
|
|
11
|
+
character: number
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** LSP Range(start/end 均 0-based,包含 start、排除 end)。 */
|
|
15
|
+
export interface LspRange {
|
|
16
|
+
start: LspPosition
|
|
17
|
+
end: LspPosition
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** LSP Location:uri 为 file:// 绝对路径;root 用于还原为工作区相对路径。 */
|
|
21
|
+
export interface LspLocation {
|
|
22
|
+
uri: string
|
|
23
|
+
range: LspRange
|
|
24
|
+
root?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** 跳转结果(definition 常返回 LocationLink,归一化为 targetUri + targetRange + 可选 originSelectionRange)。 */
|
|
28
|
+
export interface LspLocationLink {
|
|
29
|
+
targetUri: string
|
|
30
|
+
targetRange: LspRange
|
|
31
|
+
targetSelectionRange: LspRange
|
|
32
|
+
originSelectionRange?: LspRange
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** 归一化后的符号(documentSymbol/workspaceSymbol 统一形状)。kind 为 LSP SymbolKind 数值。 */
|
|
36
|
+
export interface LspSymbol {
|
|
37
|
+
name: string
|
|
38
|
+
kind: number
|
|
39
|
+
range: LspRange
|
|
40
|
+
selectionRange: LspRange
|
|
41
|
+
detail?: string
|
|
42
|
+
containerName?: string
|
|
43
|
+
children?: LspSymbol[]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** hover 内容(markdown/纯文本行数组)。 */
|
|
47
|
+
export interface LspHover {
|
|
48
|
+
contents: string[]
|
|
49
|
+
range?: LspRange
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** LSP 服务器能力子集:本插件会用到的方法。 */
|
|
53
|
+
export interface LspServerCapabilities {
|
|
54
|
+
definition: boolean
|
|
55
|
+
references: boolean
|
|
56
|
+
documentSymbol: boolean
|
|
57
|
+
workspaceSymbol: boolean
|
|
58
|
+
hover: boolean
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** 服务器进程/会话状态。 */
|
|
62
|
+
export type LspServerPhase =
|
|
63
|
+
| 'idle' // 未启动(惰性)
|
|
64
|
+
| 'starting' // 启动中(initialize 握手)
|
|
65
|
+
| 'ready' // 就绪
|
|
66
|
+
| 'indexing' // 工作区索引中
|
|
67
|
+
| 'unavailable' // 连续失败,停止自动重启
|
|
68
|
+
| 'stopped' // 已停止(禁用/卸载/会话结束)
|
|
69
|
+
|
|
70
|
+
/** 单语言服务器状态(edrv.lsp.status 载荷元素)。 */
|
|
71
|
+
export interface LspServerStatus {
|
|
72
|
+
languageId: string
|
|
73
|
+
source: 'extension' | 'discover' | 'manual' | 'none'
|
|
74
|
+
phase: LspServerPhase
|
|
75
|
+
version?: string
|
|
76
|
+
reason?: string // unavailable/不可用原因(缺 dotnet、找不到服务器等)
|
|
77
|
+
progress?: number // LSP 工作区解析进度(0–100,未知时省略)
|
|
78
|
+
progressMessage?: string
|
|
79
|
+
root?: string // 绑定工作区根(相对显示)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Monaco 位置(1-based lineNumber/column)→ LSP Position(0-based)。
|
|
84
|
+
* @author ddj 2026年08月27号
|
|
85
|
+
* @param lineNumber Monaco 行(≥1)
|
|
86
|
+
* @param column Monaco 列(≥1)
|
|
87
|
+
* @returns LSP 0-based position
|
|
88
|
+
*/
|
|
89
|
+
export function monoToLsp(lineNumber: number, column: number): LspPosition {
|
|
90
|
+
return { line: Math.max(0, lineNumber - 1), character: Math.max(0, column - 1) }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* LSP Position(0-based)→ Monaco 位置(1-based)。
|
|
95
|
+
* @author ddj 2026年08月27号
|
|
96
|
+
* @param pos LSP position
|
|
97
|
+
* @returns { lineNumber, column }(均 ≥1)
|
|
98
|
+
*/
|
|
99
|
+
export function lspToMono(pos: LspPosition): { lineNumber: number; column: number } {
|
|
100
|
+
return { lineNumber: Math.max(1, pos.line + 1), column: Math.max(1, pos.character + 1) }
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** file:// URI → 路径(仅做规范化解码;Windows file:///C:/... 保留)。 */
|
|
104
|
+
export function fileUriToPath(uri: string): string {
|
|
105
|
+
if (!uri) return ''
|
|
106
|
+
const rest = uri.replace(/^file:\/\//, '')
|
|
107
|
+
const decoded = decodeURIComponent(rest)
|
|
108
|
+
return decoded.replace(/^\/([A-Za-z]:)/, '$1') // file:///C:/x → C:/x
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** 已装扩展信息(edrv.lsp.ext.list 载荷元素)。 */
|
|
112
|
+
export interface LspExtInfo {
|
|
113
|
+
id: string // publisher.name
|
|
114
|
+
namespace: string
|
|
115
|
+
name: string
|
|
116
|
+
version: string
|
|
117
|
+
displayName?: string
|
|
118
|
+
description?: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Open VSX 市场搜索结果项(edrv.lsp.ext.market 载荷元素)。 */
|
|
122
|
+
export interface LspMarketItem {
|
|
123
|
+
namespace: string
|
|
124
|
+
name: string
|
|
125
|
+
id: string
|
|
126
|
+
version: string
|
|
127
|
+
displayName?: string
|
|
128
|
+
description?: string
|
|
129
|
+
download?: string
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 有更新的已装扩展(edrv.lsp.ext.updates 载荷元素)。 */
|
|
133
|
+
export interface LspExtUpdate {
|
|
134
|
+
id: string
|
|
135
|
+
current: string
|
|
136
|
+
latest: string
|
|
137
|
+
displayName?: string
|
|
138
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -14,6 +14,7 @@ import type {
|
|
|
14
14
|
} from './types.js'
|
|
15
15
|
import type { MpcConfig, MpcProject, MpcProjectSaveInput, MpcServer } from './mcp.js'
|
|
16
16
|
import type { CompatReport, DevFormInfo } from './compat.js'
|
|
17
|
+
import type { LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspServerStatus, LspSymbol } from './lsp.js'
|
|
17
18
|
|
|
18
19
|
/** webServer 精确路由。 */
|
|
19
20
|
export const RPC_PATH = '/edrv/rpc'
|
|
@@ -134,6 +135,22 @@ export interface RpcRequestMap {
|
|
|
134
135
|
'vscode.devFormGet': {}
|
|
135
136
|
'vscode.devFormSet': { enabled: boolean; path?: string }
|
|
136
137
|
'compat': {}
|
|
138
|
+
'edrv.lsp.status': { sessionId?: string }
|
|
139
|
+
'edrv.lsp.sync': { sessionId?: string; path: string; text: string; version: number }
|
|
140
|
+
'edrv.lsp.close': { sessionId?: string; path: string }
|
|
141
|
+
'edrv.lsp.definition': { sessionId?: string; path: string; position: LspPosition }
|
|
142
|
+
'edrv.lsp.references': { sessionId?: string; path: string; position: LspPosition; includeDeclaration?: boolean }
|
|
143
|
+
'edrv.lsp.documentSymbol': { sessionId?: string; path: string }
|
|
144
|
+
'edrv.lsp.workspaceSymbol': { sessionId?: string; query: string }
|
|
145
|
+
'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
|
|
146
|
+
'edrv.lsp.configGet': {}
|
|
147
|
+
'edrv.lsp.configUpdate': { languageId: string; enabled?: boolean; command?: string; path?: string }
|
|
148
|
+
'edrv.lsp.ext.list': {}
|
|
149
|
+
'edrv.lsp.ext.install': { vsixPath?: string; namespace?: string; name?: string; version?: string }
|
|
150
|
+
'edrv.lsp.ext.uninstall': { id: string }
|
|
151
|
+
'edrv.lsp.ext.update': { id: string }
|
|
152
|
+
'edrv.lsp.ext.updates': {}
|
|
153
|
+
'edrv.lsp.ext.market': { query: string; size?: number }
|
|
137
154
|
'edrv.perf.inventory': { sessionId?: string }
|
|
138
155
|
'edrv.perf.sessionSize': { sessionId?: string; cwd: string }
|
|
139
156
|
'edrv.perf.movePlan': { workspaceKey?: string; sessionIds?: string[]; minBytes?: number; olderThanDays?: number }
|
|
@@ -180,6 +197,22 @@ export interface RpcOkMap {
|
|
|
180
197
|
'vscode.devFormGet': { devForm: DevFormInfo }
|
|
181
198
|
'vscode.devFormSet': { devForm: DevFormInfo; restart: boolean }
|
|
182
199
|
'compat': { report: CompatReport }
|
|
200
|
+
'edrv.lsp.status': { servers: LspServerStatus[] }
|
|
201
|
+
'edrv.lsp.sync': object
|
|
202
|
+
'edrv.lsp.close': object
|
|
203
|
+
'edrv.lsp.definition': { locations: LspLocation[]; truncated?: boolean }
|
|
204
|
+
'edrv.lsp.references': { locations: LspLocation[]; truncated?: boolean }
|
|
205
|
+
'edrv.lsp.documentSymbol': { symbols: LspSymbol[] }
|
|
206
|
+
'edrv.lsp.workspaceSymbol': { symbols: LspSymbol[]; truncated?: boolean }
|
|
207
|
+
'edrv.lsp.hover': { hover?: LspHover }
|
|
208
|
+
'edrv.lsp.configGet': { config: Record<string, unknown> }
|
|
209
|
+
'edrv.lsp.configUpdate': { config: Record<string, unknown> }
|
|
210
|
+
'edrv.lsp.ext.list': { extensions: LspExtInfo[] }
|
|
211
|
+
'edrv.lsp.ext.install': { extension: LspExtInfo; updated?: boolean }
|
|
212
|
+
'edrv.lsp.ext.uninstall': object
|
|
213
|
+
'edrv.lsp.ext.update': { extension: LspExtInfo; updated: boolean }
|
|
214
|
+
'edrv.lsp.ext.updates': { updates: LspExtUpdate[] }
|
|
215
|
+
'edrv.lsp.ext.market': { extensions: LspMarketItem[] }
|
|
183
216
|
'edrv.perf.inventory': { workspaces: PerfWorkspace[]; sessions: PerfSession[]; totals: PerfTotals; activeIds: string[] }
|
|
184
217
|
'edrv.perf.sessionSize': { bytes: number; exists: boolean }
|
|
185
218
|
'edrv.perf.movePlan': { items: PerfMoveItem[]; reclaimedBytes: number }
|