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/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 { codiconAsset, createEntry, cwdFor, deleteEntry, fileIconAsset, gitAction, gitStatus, listFiles, monacoAsset, openWorkspaceFromPicker, pdfWorkerAsset, xlsxAsset, readDocument, projectSources, renameEntry, saveDocument, workspaceDownloadFile, searchWorkspace, uploadFile, workspacePreviewFile } from './host.ts'
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 { access, mkdir, rm } from 'node:fs/promises'
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
- export type LspInstall = { kind: 'npm'; package: string; bin: string } | { kind: 'system'; command: string; hint: string }
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
- function which(command: string): Promise<string | undefined> {
57
- return new Promise(resolve => {
58
- const child = spawn(process.platform === 'win32' ? 'where' : 'which', [command], { windowsHide: true })
59
- let output = ''
60
- child.stdout.on('data', chunk => { output += String(chunk) })
61
- child.on('error', () => resolve(undefined))
62
- child.on('close', code => resolve(code === 0 ? output.split(/\r?\n/).map(line => line.trim()).find(Boolean) : undefined))
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 which(server.install.command)
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
  }