dsh-vscode-workbench 0.2.0 → 0.2.3
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/README.en.md +17 -123
- package/README.md +17 -125
- package/lib/client.js +771 -375
- package/lib/client.js.map +1 -1
- package/lib/index.js +423 -26
- package/lib/index.js.map +1 -1
- package/lib/types/client/ActivityBar.d.ts +5 -1
- package/lib/types/client/DshConversationPanel.d.ts +1 -1
- package/lib/types/client/DshMark.d.ts +3 -0
- package/lib/types/client/MonacoEditor.d.ts +18 -2
- package/lib/types/client/Workbench.d.ts +9 -0
- package/lib/types/client/WorkspaceSwitcher.d.ts +3 -2
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/lsp.d.ts +6 -0
- package/lib/types/client/service.d.ts +1 -1
- package/lib/types/client/terminal.d.ts +2 -2
- package/lib/types/commit-message.d.ts +43 -0
- package/lib/types/host.d.ts +3 -0
- package/lib/types/lsp.d.ts +21 -0
- package/package.json +3 -1
- package/src/client/ActivityBar.tsx +71 -34
- package/src/client/DshConversationPanel.tsx +1 -1
- package/src/client/DshMark.tsx +4 -4
- package/src/client/MonacoEditor.tsx +88 -7
- package/src/client/TerminalPanel.tsx +3 -3
- package/src/client/Workbench.tsx +97 -40
- package/src/client/WorkspaceSwitcher.tsx +7 -3
- package/src/client/api.ts +6 -1
- package/src/client/lsp.ts +3 -0
- package/src/client/service.ts +1 -1
- package/src/client/styles.ts +125 -2
- package/src/client/terminal.ts +16 -10
- package/src/commit-message.ts +224 -0
- package/src/host.ts +47 -0
- package/src/index.ts +18 -1
- package/src/lsp.ts +85 -25
package/src/index.ts
CHANGED
|
@@ -8,7 +8,9 @@ import { spawn } from 'node:child_process'
|
|
|
8
8
|
import chokidar from 'chokidar'
|
|
9
9
|
import { WebSocket, WebSocketServer } from 'ws'
|
|
10
10
|
import type { HostContext } from './context.ts'
|
|
11
|
-
import {
|
|
11
|
+
import type { LlmRuntime } from '@deepseek-ai/dsh-llm'
|
|
12
|
+
import { generateCommitMessage, normalizeCommitPreferences } from './commit-message.ts'
|
|
13
|
+
import { codiconAsset, createEntry, cwdFor, deleteEntry, fileIconAsset, gitAction, gitCommitContext, gitStatus, listFiles, monacoAsset, openWorkspaceFromPicker, pdfWorkerAsset, xlsxAsset, readDocument, projectSources, renameEntry, saveDocument, workspaceDownloadFile, searchWorkspace, uploadFile, workspacePreviewFile } from './host.ts'
|
|
12
14
|
import { installLsp, lspStatuses, resolveLsp, uninstallLsp, pipeLanguageServer } from './lsp.ts'
|
|
13
15
|
import { officePdf, officeStatus } from './office.ts'
|
|
14
16
|
import { installTerminal } from './terminal.ts'
|
|
@@ -44,6 +46,21 @@ async function dispatch(ctx: HostContext, method: string, body: Record<string, u
|
|
|
44
46
|
case 'search': return searchWorkspace(cwd, body as unknown as SearchRequest, signal)
|
|
45
47
|
case 'git.status': return gitStatus(cwd)
|
|
46
48
|
case 'git.action': return gitAction(cwd, stringField(body, 'action'), typeof body.path === 'string' ? body.path : undefined, typeof body.value === 'string' ? body.value : undefined)
|
|
49
|
+
case 'git.commitMessage': {
|
|
50
|
+
const session = ctx.sessions.get(sessionId)
|
|
51
|
+
const config = session?.requestHeader()?.config
|
|
52
|
+
if (config === undefined) throw new WorkbenchError('model-unavailable', '当前会话还没有模型配置,请先发送一条消息后再生成提交信息。', 409)
|
|
53
|
+
const llm: LlmRuntime | undefined = ctx.get('llm')
|
|
54
|
+
if (llm === undefined) throw new WorkbenchError('llm-unavailable', 'DSH 的模型服务当前不可用。', 503)
|
|
55
|
+
const context = await gitCommitContext(cwd)
|
|
56
|
+
const preferences = normalizeCommitPreferences(body.detail, body.language)
|
|
57
|
+
try {
|
|
58
|
+
return { message: await generateCommitMessage(llm, config, context, signal, preferences) }
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof WorkbenchError) throw error
|
|
61
|
+
throw new WorkbenchError('llm-failed', error instanceof Error ? error.message : String(error), 502)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
47
64
|
default: throw new WorkbenchError('not-found', `unknown workbench API method ${method}`, 404)
|
|
48
65
|
}
|
|
49
66
|
}
|
package/src/lsp.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
/** Host-side registry and process bridge for language servers exposed to the workbench editor. */
|
|
2
2
|
import { spawn, type ChildProcess } from 'node:child_process'
|
|
3
|
-
import {
|
|
3
|
+
import { constants } from 'node:fs'
|
|
4
|
+
import { access, mkdir, readdir, rm } from 'node:fs/promises'
|
|
4
5
|
import { homedir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
+
import { delimiter, join } from 'node:path'
|
|
6
7
|
import { WorkbenchError } from './wire.ts'
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
/** The toolchain a manual language server belongs to, detected separately from the server itself. */
|
|
10
|
+
export type LspLanguage = { name: string; commands: readonly string[] }
|
|
11
|
+
|
|
12
|
+
export type LspInstall =
|
|
13
|
+
| { kind: 'npm'; package: string; bin: string }
|
|
14
|
+
| { kind: 'system'; command: string; hint: string }
|
|
9
15
|
|
|
10
16
|
export interface LspDefinition {
|
|
11
17
|
id: string
|
|
@@ -14,24 +20,26 @@ export interface LspDefinition {
|
|
|
14
20
|
languages: string[]
|
|
15
21
|
install: LspInstall
|
|
16
22
|
args: string[]
|
|
23
|
+
/** Toolchain checked before the server; servers for pure data formats have nothing to check. */
|
|
24
|
+
language?: LspLanguage
|
|
17
25
|
}
|
|
18
26
|
|
|
19
27
|
/** The strongest maintained server for each language, installable where the ecosystem allows it. */
|
|
20
28
|
export const LSP_SERVERS: readonly LspDefinition[] = [
|
|
21
|
-
{ id: 'python', name: 'Python · Pyright', languages: ['python'], install: { kind: 'npm', package: 'pyright', bin: 'pyright-langserver' }, args: ['--stdio'] },
|
|
22
|
-
{ id: 'php', name: 'PHP · Intelephense', languages: ['php'], install: { kind: 'npm', package: 'intelephense', bin: 'intelephense' }, args: ['--stdio'] },
|
|
23
|
-
{ id: 'shell', name: 'Shell · Bash Language Server', languages: ['shell'], install: { kind: 'npm', package: 'bash-language-server', bin: 'bash-language-server' }, args: ['start'] },
|
|
29
|
+
{ id: 'python', name: 'Python · Pyright', languages: ['python'], install: { kind: 'npm', package: 'pyright', bin: 'pyright-langserver' }, args: ['--stdio'], language: { name: 'Python', commands: ['python3', 'python'] } },
|
|
30
|
+
{ id: 'php', name: 'PHP · Intelephense', languages: ['php'], install: { kind: 'npm', package: 'intelephense', bin: 'intelephense' }, args: ['--stdio'], language: { name: 'PHP', commands: ['php'] } },
|
|
31
|
+
{ id: 'shell', name: 'Shell · Bash Language Server', languages: ['shell'], install: { kind: 'npm', package: 'bash-language-server', bin: 'bash-language-server' }, args: ['start'], language: { name: 'Shell', commands: ['bash', 'zsh'] } },
|
|
24
32
|
{ id: 'yaml', name: 'YAML · YAML Language Server', languages: ['yaml'], install: { kind: 'npm', package: 'yaml-language-server', bin: 'yaml-language-server' }, args: ['--stdio'] },
|
|
25
33
|
{ id: 'json', name: 'JSON · VS Code JSON', languages: ['json'], install: { kind: 'npm', package: 'vscode-langservers-extracted', bin: 'vscode-json-language-server' }, args: ['--stdio'] },
|
|
26
34
|
{ id: 'css', name: 'CSS / SCSS / Less · VS Code CSS', languages: ['css', 'scss', 'less'], install: { kind: 'npm', package: 'vscode-langservers-extracted', bin: 'vscode-css-language-server' }, args: ['--stdio'] },
|
|
27
35
|
{ id: 'html', name: 'HTML · VS Code HTML', languages: ['html'], install: { kind: 'npm', package: 'vscode-langservers-extracted', bin: 'vscode-html-language-server' }, args: ['--stdio'] },
|
|
28
36
|
{ id: 'dockerfile', name: 'Dockerfile · Docker LS', languages: ['dockerfile'], install: { kind: 'npm', package: 'dockerfile-language-server-nodejs', bin: 'dockerfile-language-server-nodejs' }, args: ['--stdio'] },
|
|
29
|
-
{ id: 'go', name: 'Go · gopls', languages: ['go'], install: { kind: 'system', command: 'gopls', hint: '在终端运行:go install golang.org/x/tools/gopls@latest(需要 Go 工具链)' }, args: ['serve'] },
|
|
30
|
-
{ id: 'rust', name: 'Rust · rust-analyzer', languages: ['rust'], install: { kind: 'system', command: 'rust-analyzer', hint: '运行:rustup component add rust-analyzer' }, args: [] },
|
|
31
|
-
{ id: 'cpp', name: 'C / C++ · clangd', languages: ['c', 'cpp', 'objective-c'], install: { kind: 'system', command: 'clangd', hint: '安装 LLVM(Windows:winget install LLVM.LLVM),并确保 clangd 在 PATH 中' }, args: [] },
|
|
32
|
-
{ id: 'java', name: 'Java · Eclipse JDT LS', languages: ['java'], install: { kind: 'system', command: 'jdtls', hint: '下载 eclipse.jdt.ls 并让 jdtls 启动脚本在 PATH 中' }, args: [] },
|
|
33
|
-
{ id: 'csharp', name: 'C# · csharp-ls', languages: ['csharp'], install: { kind: 'system', command: 'csharp-ls', hint: '运行:dotnet tool install --global csharp-ls(需要 .NET SDK)' }, args: [] },
|
|
34
|
-
{ id: 'lua', name: 'Lua · Lua Language Server', languages: ['lua'], install: { kind: 'system', command: 'lua-language-server', hint: '从 github.com/LuaLS/lua-language-server/releases 下载,并把 lua-language-server 加入 PATH' }, args: [] },
|
|
37
|
+
{ id: 'go', name: 'Go · gopls', languages: ['go'], install: { kind: 'system', command: 'gopls', hint: '在终端运行:go install golang.org/x/tools/gopls@latest(需要 Go 工具链)' }, args: ['serve'], language: { name: 'Go', commands: ['go'] } },
|
|
38
|
+
{ id: 'rust', name: 'Rust · rust-analyzer', languages: ['rust'], install: { kind: 'system', command: 'rust-analyzer', hint: '运行:rustup component add rust-analyzer' }, args: [], language: { name: 'Rust', commands: ['rustc', 'cargo'] } },
|
|
39
|
+
{ id: 'cpp', name: 'C / C++ · clangd', languages: ['c', 'cpp', 'objective-c'], install: { kind: 'system', command: 'clangd', hint: '安装 LLVM(Windows:winget install LLVM.LLVM),并确保 clangd 在 PATH 中' }, args: [], language: { name: 'C / C++ 工具链', commands: ['clang', 'gcc', 'cc'] } },
|
|
40
|
+
{ id: 'java', name: 'Java · Eclipse JDT LS', languages: ['java'], install: { kind: 'system', command: 'jdtls', hint: '下载 eclipse.jdt.ls 并让 jdtls 启动脚本在 PATH 中' }, args: [], language: { name: 'Java', commands: ['java', 'javac'] } },
|
|
41
|
+
{ id: 'csharp', name: 'C# · csharp-ls', languages: ['csharp'], install: { kind: 'system', command: 'csharp-ls', hint: '运行:dotnet tool install --global csharp-ls(需要 .NET SDK)' }, args: [], language: { name: '.NET SDK', commands: ['dotnet'] } },
|
|
42
|
+
{ id: 'lua', name: 'Lua · Lua Language Server', languages: ['lua'], install: { kind: 'system', command: 'lua-language-server', hint: '从 github.com/LuaLS/lua-language-server/releases 下载,并把 lua-language-server 加入 PATH' }, args: [], language: { name: 'Lua', commands: ['lua', 'lua5.4', 'lua5.3', 'luajit'] } },
|
|
35
43
|
] as const
|
|
36
44
|
|
|
37
45
|
export interface LspStatus {
|
|
@@ -41,6 +49,10 @@ export interface LspStatus {
|
|
|
41
49
|
installable: boolean
|
|
42
50
|
installed: boolean
|
|
43
51
|
hint?: string
|
|
52
|
+
/** Executable name for manual servers, so the UI can name what it looked for. */
|
|
53
|
+
command?: string
|
|
54
|
+
/** Present for manual servers: whether the language toolchain itself is on this machine. */
|
|
55
|
+
language?: { name: string; installed: boolean }
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
/** Private install root so language servers never touch the user's global npm state. */
|
|
@@ -53,14 +65,57 @@ function npmBinPath(id: string, bin: string): string {
|
|
|
53
65
|
|
|
54
66
|
async function exists(path: string): Promise<boolean> { return access(path).then(() => true, () => false) }
|
|
55
67
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
68
|
+
/**
|
|
69
|
+
* GUI-launched apps inherit a trimmed PATH: Homebrew, `go install` and `cargo install` targets routinely sit
|
|
70
|
+
* outside it, so detection has to look in the usual per-user bin directories too.
|
|
71
|
+
*/
|
|
72
|
+
export function lspSearchDirs(env: NodeJS.ProcessEnv = process.env, home = homedir()): string[] {
|
|
73
|
+
const fromPath = (env.PATH ?? env.Path ?? '').split(delimiter).filter(Boolean)
|
|
74
|
+
const extras = [
|
|
75
|
+
join(home, 'go', 'bin'),
|
|
76
|
+
join(home, '.cargo', 'bin'),
|
|
77
|
+
join(home, '.dotnet', 'tools'),
|
|
78
|
+
join(home, '.local', 'bin'),
|
|
79
|
+
join(home, 'bin'),
|
|
80
|
+
'/opt/homebrew/bin',
|
|
81
|
+
'/usr/local/bin',
|
|
82
|
+
'/usr/bin',
|
|
83
|
+
'/bin',
|
|
84
|
+
'/usr/sbin',
|
|
85
|
+
'/sbin',
|
|
86
|
+
]
|
|
87
|
+
return [...new Set([...fromPath, ...extras])]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function executableIn(directory: string, command: string): Promise<string | undefined> {
|
|
91
|
+
const names = process.platform === 'win32'
|
|
92
|
+
? [`${command}.exe`, `${command}.cmd`, `${command}.bat`, command]
|
|
93
|
+
: [command]
|
|
94
|
+
for (const name of names) {
|
|
95
|
+
const file = join(directory, name)
|
|
96
|
+
const found = await access(file, constants.X_OK).then(() => true, () => false)
|
|
97
|
+
if (found) return file
|
|
98
|
+
}
|
|
99
|
+
return undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** First executable named `command` found in `dirs`; exposed for tests and for toolchain probing. */
|
|
103
|
+
export async function findExecutable(command: string, dirs: readonly string[]): Promise<string | undefined> {
|
|
104
|
+
for (const directory of dirs) {
|
|
105
|
+
const found = await executableIn(directory, command)
|
|
106
|
+
if (found !== undefined) return found
|
|
107
|
+
}
|
|
108
|
+
return undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function rustupBinDirs(home: string): Promise<string[]> {
|
|
112
|
+
const root = join(home, '.rustup', 'toolchains')
|
|
113
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => [])
|
|
114
|
+
return entries.filter(entry => entry.isDirectory()).map(entry => join(root, entry.name, 'bin'))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function searchDirs(home = homedir()): Promise<string[]> {
|
|
118
|
+
return [...lspSearchDirs(process.env, home), ...await rustupBinDirs(home)]
|
|
64
119
|
}
|
|
65
120
|
|
|
66
121
|
function definition(id: string): LspDefinition {
|
|
@@ -69,25 +124,30 @@ function definition(id: string): LspDefinition {
|
|
|
69
124
|
return found
|
|
70
125
|
}
|
|
71
126
|
|
|
72
|
-
async function commandFor(server: LspDefinition): Promise<{ command: string; shell: boolean } | undefined> {
|
|
127
|
+
async function commandFor(server: LspDefinition, dirs: readonly string[]): Promise<{ command: string; shell: boolean } | undefined> {
|
|
73
128
|
if (server.install.kind === 'npm') {
|
|
74
129
|
const command = npmBinPath(server.id, server.install.bin)
|
|
75
130
|
return await exists(command) ? { command, shell: process.platform === 'win32' } : undefined
|
|
76
131
|
}
|
|
77
|
-
const command = await
|
|
132
|
+
const command = await findExecutable(server.install.command, dirs)
|
|
78
133
|
return command === undefined ? undefined : { command, shell: process.platform === 'win32' }
|
|
79
134
|
}
|
|
80
135
|
|
|
81
136
|
export async function lspStatuses(): Promise<LspStatus[]> {
|
|
137
|
+
const dirs = await searchDirs()
|
|
82
138
|
return Promise.all(LSP_SERVERS.map(async server => {
|
|
83
|
-
const resolved = await commandFor(server)
|
|
139
|
+
const resolved = await commandFor(server, dirs)
|
|
140
|
+
const language = server.language === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: { name: server.language.name, installed: (await Promise.all(server.language.commands.map(command => findExecutable(command, dirs)))).some(Boolean) }
|
|
84
143
|
return {
|
|
85
144
|
id: server.id,
|
|
86
145
|
name: server.name,
|
|
87
146
|
languages: server.languages,
|
|
88
147
|
installable: server.install.kind === 'npm',
|
|
89
148
|
installed: resolved !== undefined,
|
|
90
|
-
...(server.install.kind === 'system' ? { hint: server.install.hint } : {}),
|
|
149
|
+
...(server.install.kind === 'system' ? { hint: server.install.hint, command: server.install.command } : {}),
|
|
150
|
+
...(language === undefined ? {} : { language }),
|
|
91
151
|
}
|
|
92
152
|
}))
|
|
93
153
|
}
|
|
@@ -123,7 +183,7 @@ export async function uninstallLsp(id: string): Promise<void> {
|
|
|
123
183
|
/** Resolve the spawn command for a running server, or explain what the user must install. */
|
|
124
184
|
export async function resolveLsp(id: string): Promise<{ command: string; shell: boolean; args: string[] }> {
|
|
125
185
|
const server = definition(id)
|
|
126
|
-
const resolved = await commandFor(server)
|
|
186
|
+
const resolved = await commandFor(server, await searchDirs())
|
|
127
187
|
if (resolved === undefined) {
|
|
128
188
|
throw new WorkbenchError('lsp-missing', server.install.kind === 'system' ? server.install.hint : '语言服务器尚未安装', 409)
|
|
129
189
|
}
|