dsh-vscode-workbench 0.1.14 → 0.2.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.
Files changed (55) hide show
  1. package/README.en.md +4 -0
  2. package/README.md +8 -2
  3. package/lib/client.js +5014 -674
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +832 -80
  6. package/lib/index.js.map +1 -1
  7. package/lib/types/client/ActivityBar.d.ts +24 -0
  8. package/lib/types/client/DshConversationPanel.d.ts +1 -1
  9. package/lib/types/client/FileIcon.d.ts +8 -0
  10. package/lib/types/client/Icon.d.ts +1 -3
  11. package/lib/types/client/MonacoEditor.d.ts +36 -0
  12. package/lib/types/client/Workbench.d.ts +19 -7
  13. package/lib/types/client/WorkspaceSwitcher.d.ts +13 -0
  14. package/lib/types/client/api.d.ts +2 -0
  15. package/lib/types/client/documentPreview.d.ts +10 -2
  16. package/lib/types/client/dshComposer.d.ts +21 -0
  17. package/lib/types/client/dshNavigation.d.ts +46 -0
  18. package/lib/types/client/fileIconMap.d.ts +2 -0
  19. package/lib/types/client/gitError.d.ts +2 -0
  20. package/lib/types/client/lsp.d.ts +27 -0
  21. package/lib/types/client/searchHighlight.d.ts +5 -0
  22. package/lib/types/client/typescriptProject.d.ts +20 -0
  23. package/lib/types/context.d.ts +15 -0
  24. package/lib/types/host.d.ts +37 -0
  25. package/lib/types/lsp.d.ts +54 -0
  26. package/lib/types/office.d.ts +17 -0
  27. package/lib/types/types.d.ts +5 -0
  28. package/lib/types/watch-ignore.d.ts +4 -0
  29. package/package.json +5 -2
  30. package/src/client/ActivityBar.tsx +152 -0
  31. package/src/client/DshConversationPanel.tsx +15 -12
  32. package/src/client/FileIcon.tsx +6 -0
  33. package/src/client/Icon.tsx +1 -14
  34. package/src/client/MonacoEditor.tsx +187 -19
  35. package/src/client/TerminalPanel.tsx +6 -3
  36. package/src/client/Workbench.tsx +324 -81
  37. package/src/client/WorkspaceSwitcher.tsx +53 -0
  38. package/src/client/api.ts +27 -1
  39. package/src/client/documentPreview.tsx +92 -5
  40. package/src/client/dshComposer.ts +73 -0
  41. package/src/client/dshNavigation.ts +81 -0
  42. package/src/client/fileIconMap.ts +7 -0
  43. package/src/client/gitError.ts +5 -0
  44. package/src/client/index.tsx +28 -14
  45. package/src/client/lsp.ts +300 -0
  46. package/src/client/searchHighlight.ts +6 -0
  47. package/src/client/styles.ts +28 -31
  48. package/src/client/typescriptProject.ts +81 -0
  49. package/src/context.ts +8 -0
  50. package/src/host.ts +100 -15
  51. package/src/index.ts +121 -17
  52. package/src/lsp.ts +170 -0
  53. package/src/office.ts +95 -0
  54. package/src/types.ts +2 -0
  55. package/src/watch-ignore.ts +22 -1
package/lib/index.js CHANGED
@@ -1,11 +1,14 @@
1
1
  import { createRequire } from "node:module";
2
+ import { chmodSync, constants, createReadStream, realpathSync, statSync } from "node:fs";
2
3
  import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat } from "node:fs/promises";
3
4
  import { basename, dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
5
+ import { execFile, spawn } from "node:child_process";
4
6
  import chokidar from "chokidar";
5
7
  import { WebSocket, WebSocketServer } from "ws";
6
8
  import { createHash, randomUUID } from "node:crypto";
7
- import { spawn } from "node:child_process";
8
- import { chmodSync, constants, realpathSync, statSync } from "node:fs";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import { pathToFileURL } from "node:url";
11
+ import { promisify } from "node:util";
9
12
  import * as pty from "node-pty";
10
13
  //#region src/wire.ts
11
14
  var WorkbenchError = class extends Error {
@@ -127,6 +130,86 @@ function relativePath(root, absolute) {
127
130
  function revision(content, mtimeMs) {
128
131
  return createHash("sha256").update(content).update(String(mtimeMs)).digest("hex");
129
132
  }
133
+ const PROJECT_SOURCE = /\.(?:[cm]?[jt]sx?|d\.ts)$/i;
134
+ const PROJECT_CONFIG = /* @__PURE__ */ new Set([
135
+ "tsconfig.json",
136
+ "jsconfig.json",
137
+ "package.json"
138
+ ]);
139
+ const PROJECT_SKIP = /* @__PURE__ */ new Set([
140
+ ".git",
141
+ "node_modules",
142
+ "dist",
143
+ "build",
144
+ "out",
145
+ "coverage",
146
+ ".next",
147
+ ".nuxt",
148
+ ".turbo",
149
+ ".cache",
150
+ "vendor",
151
+ "third_party"
152
+ ]);
153
+ /** Add a folder picked in the OS chooser to DSH's workspace list, reusing an existing registration. */
154
+ async function openWorkspaceFromPicker(picker, registry, signal) {
155
+ if (picker === void 0) throw new WorkbenchError("picker-unavailable", "当前环境没有可用的文件夹选择器", 409);
156
+ const capability = picker.capability();
157
+ if (capability.kind !== "native" || capability.pick === void 0) throw new WorkbenchError("picker-browse", "浏览器模式暂不支持从工作台选择文件夹,请使用 DSH 侧栏的「添加工作区」", 409);
158
+ const picked = await capability.pick(signal);
159
+ if (picked === null) return { opened: false };
160
+ if (registry === void 0) throw new WorkbenchError("workspace-unavailable", "工作区服务不可用", 503);
161
+ const workspace = await registry.create(picked);
162
+ return {
163
+ opened: true,
164
+ workspace: {
165
+ id: workspace.id,
166
+ path: workspace.path,
167
+ title: workspace.title
168
+ }
169
+ };
170
+ }
171
+ /** TS/JS sources the editor's language service indexes, bounded so huge repos stay responsive. */
172
+ async function projectSources(cwd, limits = {}) {
173
+ const { files: maxFiles = 2e3, bytes: maxBytes = 25165824, fileBytes = 524288 } = limits;
174
+ const { root } = await workspacePath(cwd, "");
175
+ const files = [];
176
+ let total = 0;
177
+ let truncated = false;
178
+ const queue = [root];
179
+ while (queue.length > 0) {
180
+ const directory = queue.shift();
181
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
182
+ entries.sort((a, b) => a.name.localeCompare(b.name));
183
+ for (const entry of entries) {
184
+ const full = join(directory, entry.name);
185
+ if (entry.isDirectory()) {
186
+ if (!PROJECT_SKIP.has(entry.name) && !entry.name.startsWith(".")) queue.push(full);
187
+ continue;
188
+ }
189
+ if (!entry.isFile() || !(PROJECT_SOURCE.test(entry.name) || PROJECT_CONFIG.has(entry.name))) continue;
190
+ const info = await stat(full).catch(() => void 0);
191
+ if (info === void 0 || info.size > fileBytes) continue;
192
+ if (files.length >= maxFiles || total + info.size > maxBytes) {
193
+ truncated = true;
194
+ return {
195
+ files,
196
+ truncated
197
+ };
198
+ }
199
+ const content = await readFile(full, "utf8").catch(() => void 0);
200
+ if (content === void 0) continue;
201
+ total += info.size;
202
+ files.push({
203
+ path: relativePath(root, full),
204
+ content
205
+ });
206
+ }
207
+ }
208
+ return {
209
+ files,
210
+ truncated
211
+ };
212
+ }
130
213
  async function listFiles(cwd, path) {
131
214
  const { root, absolute } = await workspacePath(cwd, path);
132
215
  const values = await readdir(absolute, { withFileTypes: true });
@@ -265,7 +348,13 @@ const PREVIEW_TYPES = {
265
348
  ".pdf": "application/pdf",
266
349
  ".doc": "application/msword",
267
350
  ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
268
- ".odt": "application/vnd.oasis.opendocument.text"
351
+ ".odt": "application/vnd.oasis.opendocument.text",
352
+ ".xls": "application/vnd.ms-excel",
353
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
354
+ ".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
355
+ ".ods": "application/vnd.oasis.opendocument.spreadsheet",
356
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
357
+ ".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12"
269
358
  };
270
359
  /** Resolve a supported preview file inside the workspace. */
271
360
  async function workspacePreviewFile(cwd, path) {
@@ -277,6 +366,17 @@ async function workspacePreviewFile(cwd, path) {
277
366
  type
278
367
  };
279
368
  }
369
+ /** Resolve a regular workspace file for drag-out downloads. */
370
+ async function workspaceDownloadFile(cwd, path) {
371
+ const resolved = await workspacePath(cwd, path);
372
+ const info = await stat(resolved.absolute);
373
+ if (!info.isFile()) throw new WorkbenchError("not-file", "only files can be downloaded", 400);
374
+ return {
375
+ path: resolved.absolute,
376
+ name: basename(resolved.absolute),
377
+ size: info.size
378
+ };
379
+ }
280
380
  /** Resolve the PDF.js worker shipped with this plugin's runtime dependency. */
281
381
  function pdfWorkerAsset() {
282
382
  return {
@@ -367,7 +467,7 @@ async function searchWorkspace(cwd, request, signal) {
367
467
  if (event.type !== "match" || event.data?.path?.text === void 0 || event.data.lines?.text === void 0) continue;
368
468
  for (const hit of event.data.submatches ?? []) {
369
469
  matches.push({
370
- path: event.data.path.text.replace(/^\.\//, ""),
470
+ path: event.data.path.text.replace(/\\/g, "/").replace(/^\.\//, ""),
371
471
  line: event.data.line_number ?? 1,
372
472
  column: hit.start + 1,
373
473
  preview: event.data.lines.text.trimEnd(),
@@ -405,18 +505,21 @@ async function gitObject(repository, spec) {
405
505
  ], repository);
406
506
  return result.code === 0 ? result.stdout : "";
407
507
  }
408
- async function commitFiles(repository, hash) {
508
+ async function commitParent(repository, hash) {
409
509
  const parent = await run("git", [
410
510
  "rev-parse",
411
511
  "--verify",
412
512
  `${hash}^1`
413
513
  ], repository);
414
- const result = parent.code === 0 ? await run("git", [
514
+ return parent.code === 0 ? parent.stdout.trim() : void 0;
515
+ }
516
+ async function commitFiles(repository, hash, parent) {
517
+ const result = parent !== void 0 ? await run("git", [
415
518
  "diff",
416
519
  "--name-status",
417
520
  "-z",
418
521
  "-M",
419
- parent.stdout.trim(),
522
+ parent,
420
523
  hash,
421
524
  "--"
422
525
  ], repository) : await run("git", [
@@ -585,6 +688,33 @@ async function gitAction(cwd, action, path, value) {
585
688
  case "branch.switch":
586
689
  args = ["switch", value ?? ""];
587
690
  break;
691
+ case "branch.list": {
692
+ const refs = await run("git", [
693
+ "for-each-ref",
694
+ "--format=%(refname:short)",
695
+ "refs/heads"
696
+ ], repository);
697
+ if (refs.code !== 0) throw new WorkbenchError("git-failed", refs.stderr, 500);
698
+ const worktrees = await run("git", [
699
+ "worktree",
700
+ "list",
701
+ "--porcelain"
702
+ ], repository);
703
+ const used = /* @__PURE__ */ new Map();
704
+ if (worktrees.code === 0) {
705
+ const same = (a, b) => normalize(a) === normalize(b) || process.platform === "win32" && normalize(a).toLowerCase() === normalize(b).toLowerCase();
706
+ let worktree = "";
707
+ for (const line of worktrees.stdout.split("\n")) if (line.startsWith("worktree ")) worktree = line.slice(9).trim();
708
+ else if (line.startsWith("branch refs/heads/")) {
709
+ const name = line.slice(18).trim();
710
+ if (name !== "" && !same(worktree, repository)) used.set(name, worktree);
711
+ }
712
+ }
713
+ return refs.stdout.split("\n").filter(Boolean).map((name) => ({
714
+ name,
715
+ ...used.has(name) ? { worktree: used.get(name) } : {}
716
+ }));
717
+ }
588
718
  case "branches": {
589
719
  const result = await run("git", [
590
720
  "for-each-ref",
@@ -640,43 +770,44 @@ async function gitAction(cwd, action, path, value) {
640
770
  modified: result.stdout
641
771
  };
642
772
  }
643
- case "commit.files": return commitFiles(repository, validateCommitHash(value));
773
+ case "commit.files": {
774
+ const hash = validateCommitHash(value);
775
+ return commitFiles(repository, hash, await commitParent(repository, hash));
776
+ }
644
777
  case "commit.file.diff": {
645
778
  const hash = validateCommitHash(value);
646
779
  const target = validateGitPath(repository, path);
647
- const file = (await commitFiles(repository, hash)).find((item) => item.path === target);
780
+ const parent = await commitParent(repository, hash);
781
+ const file = (await commitFiles(repository, hash, parent)).find((item) => item.path === target);
648
782
  if (file === void 0) throw new WorkbenchError("not-found", "file is not part of the commit", 404);
649
- const parent = await run("git", [
650
- "rev-parse",
651
- "--verify",
652
- `${hash}^1`
653
- ], repository);
654
- const shown = parent.code === 0 ? await run("git", [
655
- "diff",
656
- "--no-ext-diff",
657
- "--find-renames",
658
- "--patch",
659
- "--no-color",
660
- parent.stdout.trim(),
661
- hash,
662
- "--",
663
- file.previousPath ?? target,
664
- target
665
- ], repository) : await run("git", [
666
- "show",
667
- "--no-ext-diff",
668
- "--find-renames",
669
- "--format=",
670
- "--patch",
671
- "--no-color",
672
- hash,
673
- "--",
674
- file.previousPath ?? target,
675
- target
676
- ], repository);
783
+ const [shown, original, modified] = await Promise.all([
784
+ parent !== void 0 ? run("git", [
785
+ "diff",
786
+ "--no-ext-diff",
787
+ "--find-renames",
788
+ "--patch",
789
+ "--no-color",
790
+ parent,
791
+ hash,
792
+ "--",
793
+ file.previousPath ?? target,
794
+ target
795
+ ], repository) : run("git", [
796
+ "show",
797
+ "--no-ext-diff",
798
+ "--find-renames",
799
+ "--format=",
800
+ "--patch",
801
+ "--no-color",
802
+ hash,
803
+ "--",
804
+ file.previousPath ?? target,
805
+ target
806
+ ], repository),
807
+ file.status === "A" || parent === void 0 ? "" : gitObject(repository, `${parent}:${file.previousPath ?? target}`),
808
+ file.status === "D" ? "" : gitObject(repository, `${hash}:${target}`)
809
+ ]);
677
810
  if (shown.code !== 0) throw new WorkbenchError("git-failed", shown.stderr || shown.stdout, 409);
678
- const original = file.status === "A" || parent.code !== 0 ? "" : await gitObject(repository, `${parent.stdout.trim()}:${file.previousPath ?? target}`);
679
- const modified = file.status === "D" ? "" : await gitObject(repository, `${hash}:${target}`);
680
811
  return {
681
812
  diff: shown.stdout,
682
813
  original,
@@ -721,6 +852,14 @@ function codiconAsset() {
721
852
  type: "font/ttf"
722
853
  };
723
854
  }
855
+ /** Browser build of SheetJS served as an asset so the client bundle stays free of Node builtins. */
856
+ function xlsxAsset() {
857
+ const packageRoot = dirname(require$1.resolve("xlsx/package.json"));
858
+ return {
859
+ path: resolve(packageRoot, "dist", "xlsx.full.min.js"),
860
+ type: "text/javascript; charset=utf-8"
861
+ };
862
+ }
724
863
  function monacoAsset(raw) {
725
864
  const packageRoot = dirname(require$1.resolve("monaco-editor/package.json"));
726
865
  const path = resolve(packageRoot, "min", raw);
@@ -730,6 +869,443 @@ function monacoAsset(raw) {
730
869
  type: extname(path) === ".css" ? "text/css" : extname(path) === ".js" ? "text/javascript" : extname(path) === ".ttf" ? "font/ttf" : "application/octet-stream"
731
870
  };
732
871
  }
872
+ /** Resolve a vscode-icons SVG shipped by @vscode-icons/vscode-icons-api. */
873
+ function fileIconAsset(raw) {
874
+ if (!/^[a-z0-9_-]+\.svg$/i.test(raw)) throw new WorkbenchError("asset-invalid", "invalid file icon path", 403);
875
+ const packageRoot = dirname(require$1.resolve("@vscode-icons/vscode-icons-api/package.json"));
876
+ return {
877
+ path: resolve(packageRoot, "icons", raw),
878
+ type: "image/svg+xml"
879
+ };
880
+ }
881
+ //#endregion
882
+ //#region src/lsp.ts
883
+ /** Host-side registry and process bridge for language servers exposed to the workbench editor. */
884
+ /** The strongest maintained server for each language, installable where the ecosystem allows it. */
885
+ const LSP_SERVERS = [
886
+ {
887
+ id: "python",
888
+ name: "Python · Pyright",
889
+ languages: ["python"],
890
+ install: {
891
+ kind: "npm",
892
+ package: "pyright",
893
+ bin: "pyright-langserver"
894
+ },
895
+ args: ["--stdio"]
896
+ },
897
+ {
898
+ id: "php",
899
+ name: "PHP · Intelephense",
900
+ languages: ["php"],
901
+ install: {
902
+ kind: "npm",
903
+ package: "intelephense",
904
+ bin: "intelephense"
905
+ },
906
+ args: ["--stdio"]
907
+ },
908
+ {
909
+ id: "shell",
910
+ name: "Shell · Bash Language Server",
911
+ languages: ["shell"],
912
+ install: {
913
+ kind: "npm",
914
+ package: "bash-language-server",
915
+ bin: "bash-language-server"
916
+ },
917
+ args: ["start"]
918
+ },
919
+ {
920
+ id: "yaml",
921
+ name: "YAML · YAML Language Server",
922
+ languages: ["yaml"],
923
+ install: {
924
+ kind: "npm",
925
+ package: "yaml-language-server",
926
+ bin: "yaml-language-server"
927
+ },
928
+ args: ["--stdio"]
929
+ },
930
+ {
931
+ id: "json",
932
+ name: "JSON · VS Code JSON",
933
+ languages: ["json"],
934
+ install: {
935
+ kind: "npm",
936
+ package: "vscode-langservers-extracted",
937
+ bin: "vscode-json-language-server"
938
+ },
939
+ args: ["--stdio"]
940
+ },
941
+ {
942
+ id: "css",
943
+ name: "CSS / SCSS / Less · VS Code CSS",
944
+ languages: [
945
+ "css",
946
+ "scss",
947
+ "less"
948
+ ],
949
+ install: {
950
+ kind: "npm",
951
+ package: "vscode-langservers-extracted",
952
+ bin: "vscode-css-language-server"
953
+ },
954
+ args: ["--stdio"]
955
+ },
956
+ {
957
+ id: "html",
958
+ name: "HTML · VS Code HTML",
959
+ languages: ["html"],
960
+ install: {
961
+ kind: "npm",
962
+ package: "vscode-langservers-extracted",
963
+ bin: "vscode-html-language-server"
964
+ },
965
+ args: ["--stdio"]
966
+ },
967
+ {
968
+ id: "dockerfile",
969
+ name: "Dockerfile · Docker LS",
970
+ languages: ["dockerfile"],
971
+ install: {
972
+ kind: "npm",
973
+ package: "dockerfile-language-server-nodejs",
974
+ bin: "dockerfile-language-server-nodejs"
975
+ },
976
+ args: ["--stdio"]
977
+ },
978
+ {
979
+ id: "go",
980
+ name: "Go · gopls",
981
+ languages: ["go"],
982
+ install: {
983
+ kind: "system",
984
+ command: "gopls",
985
+ hint: "在终端运行:go install golang.org/x/tools/gopls@latest(需要 Go 工具链)"
986
+ },
987
+ args: ["serve"]
988
+ },
989
+ {
990
+ id: "rust",
991
+ name: "Rust · rust-analyzer",
992
+ languages: ["rust"],
993
+ install: {
994
+ kind: "system",
995
+ command: "rust-analyzer",
996
+ hint: "运行:rustup component add rust-analyzer"
997
+ },
998
+ args: []
999
+ },
1000
+ {
1001
+ id: "cpp",
1002
+ name: "C / C++ · clangd",
1003
+ languages: [
1004
+ "c",
1005
+ "cpp",
1006
+ "objective-c"
1007
+ ],
1008
+ install: {
1009
+ kind: "system",
1010
+ command: "clangd",
1011
+ hint: "安装 LLVM(Windows:winget install LLVM.LLVM),并确保 clangd 在 PATH 中"
1012
+ },
1013
+ args: []
1014
+ },
1015
+ {
1016
+ id: "java",
1017
+ name: "Java · Eclipse JDT LS",
1018
+ languages: ["java"],
1019
+ install: {
1020
+ kind: "system",
1021
+ command: "jdtls",
1022
+ hint: "下载 eclipse.jdt.ls 并让 jdtls 启动脚本在 PATH 中"
1023
+ },
1024
+ args: []
1025
+ },
1026
+ {
1027
+ id: "csharp",
1028
+ name: "C# · csharp-ls",
1029
+ languages: ["csharp"],
1030
+ install: {
1031
+ kind: "system",
1032
+ command: "csharp-ls",
1033
+ hint: "运行:dotnet tool install --global csharp-ls(需要 .NET SDK)"
1034
+ },
1035
+ args: []
1036
+ },
1037
+ {
1038
+ id: "lua",
1039
+ name: "Lua · Lua Language Server",
1040
+ languages: ["lua"],
1041
+ install: {
1042
+ kind: "system",
1043
+ command: "lua-language-server",
1044
+ hint: "从 github.com/LuaLS/lua-language-server/releases 下载,并把 lua-language-server 加入 PATH"
1045
+ },
1046
+ args: []
1047
+ }
1048
+ ];
1049
+ /** Private install root so language servers never touch the user's global npm state. */
1050
+ function lspRoot() {
1051
+ return join(homedir(), ".dsh", "dsh-vscode-workbench", "lsp");
1052
+ }
1053
+ function npmBinPath(id, bin) {
1054
+ const path = join(lspRoot(), id, "node_modules", ".bin", bin);
1055
+ return process.platform === "win32" ? `${path}.cmd` : path;
1056
+ }
1057
+ async function exists$1(path) {
1058
+ return access(path).then(() => true, () => false);
1059
+ }
1060
+ function which$1(command) {
1061
+ return new Promise((resolve) => {
1062
+ const child = spawn(process.platform === "win32" ? "where" : "which", [command], { windowsHide: true });
1063
+ let output = "";
1064
+ child.stdout.on("data", (chunk) => {
1065
+ output += String(chunk);
1066
+ });
1067
+ child.on("error", () => resolve(void 0));
1068
+ child.on("close", (code) => resolve(code === 0 ? output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) : void 0));
1069
+ });
1070
+ }
1071
+ function definition(id) {
1072
+ const found = LSP_SERVERS.find((server) => server.id === id);
1073
+ if (found === void 0) throw new WorkbenchError("lsp-unknown", `unknown language server: ${id}`, 404);
1074
+ return found;
1075
+ }
1076
+ async function commandFor(server) {
1077
+ if (server.install.kind === "npm") {
1078
+ const command = npmBinPath(server.id, server.install.bin);
1079
+ return await exists$1(command) ? {
1080
+ command,
1081
+ shell: process.platform === "win32"
1082
+ } : void 0;
1083
+ }
1084
+ const command = await which$1(server.install.command);
1085
+ return command === void 0 ? void 0 : {
1086
+ command,
1087
+ shell: process.platform === "win32"
1088
+ };
1089
+ }
1090
+ async function lspStatuses() {
1091
+ return Promise.all(LSP_SERVERS.map(async (server) => {
1092
+ const resolved = await commandFor(server);
1093
+ return {
1094
+ id: server.id,
1095
+ name: server.name,
1096
+ languages: server.languages,
1097
+ installable: server.install.kind === "npm",
1098
+ installed: resolved !== void 0,
1099
+ ...server.install.kind === "system" ? { hint: server.install.hint } : {}
1100
+ };
1101
+ }));
1102
+ }
1103
+ /** Install an npm-distributed server into the plugin's private directory. */
1104
+ async function installLsp(id) {
1105
+ const server = definition(id);
1106
+ if (server.install.kind !== "npm") throw new WorkbenchError("lsp-manual", server.install.hint, 409);
1107
+ const directory = join(lspRoot(), id);
1108
+ await mkdir(directory, { recursive: true });
1109
+ await new Promise((resolve, reject) => {
1110
+ const child = spawn("npm", [
1111
+ "install",
1112
+ "--prefix",
1113
+ directory,
1114
+ "--no-save",
1115
+ "--no-audit",
1116
+ "--no-fund",
1117
+ "--loglevel=error",
1118
+ server.install.kind === "npm" ? server.install.package : ""
1119
+ ], {
1120
+ cwd: directory,
1121
+ shell: process.platform === "win32",
1122
+ windowsHide: true
1123
+ });
1124
+ let error = "";
1125
+ child.stderr.on("data", (chunk) => {
1126
+ error = (error + String(chunk)).slice(-4e3);
1127
+ });
1128
+ const timer = setTimeout(() => {
1129
+ child.kill();
1130
+ reject(new WorkbenchError("lsp-timeout", "语言服务器安装超时", 504));
1131
+ }, 6e5);
1132
+ child.on("error", (cause) => {
1133
+ clearTimeout(timer);
1134
+ reject(new WorkbenchError("lsp-install", String(cause), 500));
1135
+ });
1136
+ child.on("close", (code) => {
1137
+ clearTimeout(timer);
1138
+ if (code === 0) resolve();
1139
+ else reject(new WorkbenchError("lsp-install", error.trim() || `npm exited with ${code}`, 500));
1140
+ });
1141
+ });
1142
+ }
1143
+ async function uninstallLsp(id) {
1144
+ if (definition(id).install.kind !== "npm") return;
1145
+ await rm(join(lspRoot(), id), {
1146
+ recursive: true,
1147
+ force: true
1148
+ });
1149
+ }
1150
+ /** Resolve the spawn command for a running server, or explain what the user must install. */
1151
+ async function resolveLsp(id) {
1152
+ const server = definition(id);
1153
+ const resolved = await commandFor(server);
1154
+ if (resolved === void 0) throw new WorkbenchError("lsp-missing", server.install.kind === "system" ? server.install.hint : "语言服务器尚未安装", 409);
1155
+ return {
1156
+ ...resolved,
1157
+ args: server.args
1158
+ };
1159
+ }
1160
+ /** Serialize a JSON-RPC message using the Language Server Protocol base framing. */
1161
+ function frameLsp(message) {
1162
+ const body = JSON.stringify(message);
1163
+ return `Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`;
1164
+ }
1165
+ /** Incremental decoder for LSP frames arriving in arbitrary chunks. */
1166
+ function lspFrameParser(onMessage) {
1167
+ let buffer = Buffer.alloc(0);
1168
+ return (chunk) => {
1169
+ buffer = Buffer.concat([buffer, chunk]);
1170
+ for (;;) {
1171
+ const headerEnd = buffer.indexOf("\r\n\r\n");
1172
+ if (headerEnd < 0) return;
1173
+ const header = buffer.subarray(0, headerEnd).toString("ascii");
1174
+ const length = /content-length:\s*(\d+)/i.exec(header)?.[1];
1175
+ if (length === void 0) {
1176
+ buffer = buffer.subarray(headerEnd + 4);
1177
+ continue;
1178
+ }
1179
+ const size = Number(length);
1180
+ if (buffer.length < headerEnd + 4 + size) return;
1181
+ const body = buffer.subarray(headerEnd + 4, headerEnd + 4 + size).toString("utf8");
1182
+ buffer = buffer.subarray(headerEnd + 4 + size);
1183
+ try {
1184
+ onMessage(JSON.parse(body));
1185
+ } catch {}
1186
+ }
1187
+ };
1188
+ }
1189
+ /** Wire a spawned language server's stdio to JSON-RPC frames in both directions. */
1190
+ function pipeLanguageServer(child, handlers) {
1191
+ const parse = lspFrameParser((message) => handlers.onMessage(message));
1192
+ child.stdout?.on("data", (chunk) => parse(chunk));
1193
+ child.stderr?.on("data", (chunk) => handlers.onStderr?.(String(chunk)));
1194
+ child.stdin?.on("error", () => void 0);
1195
+ child.on("exit", (code) => handlers.onExit?.(code));
1196
+ return {
1197
+ write(text) {
1198
+ try {
1199
+ child.stdin?.write(frameLsp(JSON.parse(text)));
1200
+ } catch {}
1201
+ },
1202
+ kill() {
1203
+ child.kill();
1204
+ }
1205
+ };
1206
+ }
1207
+ //#endregion
1208
+ //#region src/office.ts
1209
+ /** Convert Word and PowerPoint documents to PDF with a locally installed LibreOffice, like DSH's own preview. */
1210
+ /** Whether a path is a document that can only be shown through the converter. */
1211
+ function needsOfficeConversion(path) {
1212
+ const extension = extname(path).toLowerCase();
1213
+ return extension === ".doc" || extension === ".docx" || extension === ".ppt" || extension === ".pptx" || extension === ".odt" || extension === ".odp" || extension === ".rtf";
1214
+ }
1215
+ /** Install guidance mirrors DSH: the preview never downloads an engine itself. */
1216
+ const OFFICE_HINT = "未检测到 LibreOffice。安装 LibreOffice 后可像 DSH 官方预览一样把 Word / PowerPoint 转成 PDF 查看。";
1217
+ function candidates() {
1218
+ const list = [process.env.DVW_SOFFICE];
1219
+ if (process.platform === "win32") list.push("soffice", "C:\\Program Files\\LibreOffice\\program\\soffice.exe", "C:\\Program Files (x86)\\LibreOffice\\program\\soffice.exe");
1220
+ else if (process.platform === "darwin") list.push("soffice", "/Applications/LibreOffice.app/Contents/MacOS/soffice");
1221
+ else list.push("soffice", "libreoffice", "/usr/bin/soffice", "/usr/bin/libreoffice", "/snap/bin/libreoffice");
1222
+ return list.filter((value) => typeof value === "string" && value !== "");
1223
+ }
1224
+ function which(command) {
1225
+ return new Promise((resolve) => {
1226
+ const child = spawn(process.platform === "win32" ? "where" : "which", [command], { windowsHide: true });
1227
+ let output = "";
1228
+ child.stdout.on("data", (chunk) => {
1229
+ output += String(chunk);
1230
+ });
1231
+ child.on("error", () => resolve(void 0));
1232
+ child.on("close", (code) => resolve(code === 0 ? output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) : void 0));
1233
+ });
1234
+ }
1235
+ async function exists(path) {
1236
+ return access(path).then(() => true, () => false);
1237
+ }
1238
+ let detected;
1239
+ /** Locate LibreOffice without repeatedly probing the system for every open tab. */
1240
+ async function findOfficeConverter() {
1241
+ if (detected !== void 0 && Date.now() - detected.checked < 6e4) return detected.path;
1242
+ let found;
1243
+ for (const candidate of candidates()) {
1244
+ if ((candidate.includes("/") || candidate.includes("\\")) && await exists(candidate)) {
1245
+ found = candidate;
1246
+ break;
1247
+ }
1248
+ const resolved = await which(candidate);
1249
+ if (resolved !== void 0) {
1250
+ found = resolved;
1251
+ break;
1252
+ }
1253
+ }
1254
+ detected = {
1255
+ path: found,
1256
+ checked: Date.now()
1257
+ };
1258
+ return found;
1259
+ }
1260
+ /** Availability payload for the settings and preview UI. */
1261
+ async function officeStatus() {
1262
+ return await findOfficeConverter() === void 0 ? {
1263
+ available: false,
1264
+ hint: OFFICE_HINT
1265
+ } : { available: true };
1266
+ }
1267
+ /** Convert one workspace document to a cached PDF and return its absolute path. */
1268
+ async function officePdf(cwd, path) {
1269
+ if (!needsOfficeConversion(path)) throw new WorkbenchError("unsupported-media", "file is not a Word or PowerPoint document", 415);
1270
+ const source = await workspacePath(cwd, path);
1271
+ const converter = await findOfficeConverter();
1272
+ if (converter === void 0) throw new WorkbenchError("converter-missing", OFFICE_HINT, 503);
1273
+ const info = await stat(source.absolute);
1274
+ const root = join(tmpdir(), "dsh-vscode-workbench-office");
1275
+ const key = createHash("sha1").update(`${source.absolute}:${info.size}:${info.mtimeMs}`).digest("hex").slice(0, 20);
1276
+ const target = join(root, `${key}.pdf`);
1277
+ if (await exists(target)) return { path: target };
1278
+ await mkdir(root, { recursive: true });
1279
+ const output = join(root, `out-${key}`);
1280
+ await mkdir(output, { recursive: true });
1281
+ const profile = pathToFileURL(join(root, `profile-${key}-${process.pid}-${Date.now()}`)).href;
1282
+ try {
1283
+ await promisify(execFile)(converter, [
1284
+ "--headless",
1285
+ "--norestore",
1286
+ "--invisible",
1287
+ "--convert-to",
1288
+ "pdf",
1289
+ "--outdir",
1290
+ output,
1291
+ `-env:UserInstallation=${profile}`,
1292
+ source.absolute
1293
+ ], {
1294
+ timeout: 12e4,
1295
+ windowsHide: true,
1296
+ maxBuffer: 4194304
1297
+ });
1298
+ const produced = join(output, `${basename(source.absolute, extname(source.absolute))}.pdf`);
1299
+ if (!await exists(produced)) throw new WorkbenchError("converter-failed", "LibreOffice 未能生成 PDF", 500);
1300
+ await rename(produced, target);
1301
+ return { path: target };
1302
+ } finally {
1303
+ await rm(output, {
1304
+ recursive: true,
1305
+ force: true
1306
+ }).catch(() => void 0);
1307
+ }
1308
+ }
733
1309
  //#endregion
734
1310
  //#region src/terminal.ts
735
1311
  const require = createRequire(import.meta.url);
@@ -1011,19 +1587,6 @@ function normalize$1(entry) {
1011
1587
  function segmentsOf(entry) {
1012
1588
  return normalize$1(entry).split("/").filter((part) => part !== "" && part !== ".");
1013
1589
  }
1014
- /** Parse a newline- or comma-separated exclusion list, dropping blanks, comments and unsafe paths. */
1015
- function parseWatchIgnores(value) {
1016
- if (value === null || value === void 0) return [];
1017
- const entries = /* @__PURE__ */ new Set();
1018
- for (const raw of value.split(/[\n,]+/)) {
1019
- const entry = normalize$1(raw);
1020
- const parts = segmentsOf(entry);
1021
- if (parts.length === 0 || parts.includes("..") || entry.startsWith("#")) continue;
1022
- entries.add(parts.slice(0, 8).join("/").slice(0, 200));
1023
- if (entries.size >= 200) break;
1024
- }
1025
- return [...entries];
1026
- }
1027
1590
  /** Return true when a workspace-relative path contains one of the excluded segment sequences. */
1028
1591
  function isIgnoredPath(relativePath, patterns) {
1029
1592
  const segments = relativePath.split(/[\\/]+/).filter(Boolean).map((segment) => segment.toLowerCase());
@@ -1039,6 +1602,23 @@ function isIgnoredPath(relativePath, patterns) {
1039
1602
  }
1040
1603
  return false;
1041
1604
  }
1605
+ /** Literal directory/path rules from a .gitignore; glob and negation rules are left to git itself. */
1606
+ function parseGitIgnore(text) {
1607
+ const entries = /* @__PURE__ */ new Set();
1608
+ for (const raw of text.split(/\r?\n/)) {
1609
+ const line = raw.trim();
1610
+ if (line === "" || line.startsWith("#") || line.startsWith("!") || /[*?[\]{}]/.test(line)) continue;
1611
+ const parts = segmentsOf(normalize$1(line).replace(/^\//, ""));
1612
+ if (parts.length === 0 || parts.includes("..")) continue;
1613
+ entries.add(parts.slice(0, 8).join("/").slice(0, 200));
1614
+ if (entries.size >= 200) break;
1615
+ }
1616
+ return [...entries];
1617
+ }
1618
+ /** Ignore patterns derived from the workspace's root .gitignore, without asking the user to type exclusions. */
1619
+ async function workspaceWatchIgnores(workspace) {
1620
+ return parseGitIgnore(await readFile(join(workspace, ".gitignore"), "utf8").catch(() => ""));
1621
+ }
1042
1622
  /** Build a chokidar `ignored` predicate for one workspace and the user's extra exclusions. */
1043
1623
  function watchIgnore(workspace, extra) {
1044
1624
  const patterns = [...DEFAULT_WATCH_IGNORES, ...extra];
@@ -1052,6 +1632,8 @@ function watchIgnore(workspace, extra) {
1052
1632
  //#region src/index.ts
1053
1633
  /** DSH host plugin for the VS Code workbench. */
1054
1634
  const name = "dsh-vscode-workbench";
1635
+ let dshDirectoryPicker;
1636
+ let dshWorkspaceRegistry;
1055
1637
  const inject = [
1056
1638
  "webServer",
1057
1639
  "sessions",
@@ -1071,6 +1653,16 @@ async function dispatch(ctx, method, body, signal) {
1071
1653
  case "fs.create": return createEntry(cwd, stringField(body, "path"), body.kind === "directory" ? "directory" : "file").then(() => ({ ok: true }));
1072
1654
  case "fs.rename": return renameEntry(cwd, stringField(body, "path"), stringField(body, "nextPath")).then(() => ({ ok: true }));
1073
1655
  case "fs.delete": return deleteEntry(cwd, stringField(body, "path")).then(() => ({ ok: true }));
1656
+ case "project.sources": return projectSources(cwd);
1657
+ case "office.status": return officeStatus();
1658
+ case "workspace.open": return openWorkspaceFromPicker(dshDirectoryPicker, dshWorkspaceRegistry, AbortSignal.timeout(6e5));
1659
+ case "lsp.status": return lspStatuses();
1660
+ case "lsp.install":
1661
+ await installLsp(stringField(body, "id"));
1662
+ return lspStatuses();
1663
+ case "lsp.uninstall":
1664
+ await uninstallLsp(stringField(body, "id"));
1665
+ return lspStatuses();
1074
1666
  case "search": return searchWorkspace(cwd, body, signal);
1075
1667
  case "git.status": return gitStatus(cwd);
1076
1668
  case "git.action": return gitAction(cwd, stringField(body, "action"), typeof body.path === "string" ? body.path : void 0, typeof body.value === "string" ? body.value : void 0);
@@ -1078,6 +1670,21 @@ async function dispatch(ctx, method, body, signal) {
1078
1670
  }
1079
1671
  }
1080
1672
  function apply(ctx) {
1673
+ ctx.effect(() => {
1674
+ if (typeof ctx.inject !== "function") return;
1675
+ const fiber = ctx.inject(["directoryPicker", "workspaceRegistry"], (child) => {
1676
+ const services = child;
1677
+ dshDirectoryPicker = services.directoryPicker;
1678
+ dshWorkspaceRegistry = services.workspaceRegistry;
1679
+ return () => {
1680
+ dshDirectoryPicker = void 0;
1681
+ dshWorkspaceRegistry = void 0;
1682
+ };
1683
+ });
1684
+ return () => {
1685
+ fiber.dispose?.();
1686
+ };
1687
+ }, "dsh-vscode-workbench: optional DSH services");
1081
1688
  installTerminal(ctx);
1082
1689
  ctx.effect(() => ctx.webServer.register({
1083
1690
  kind: "exact",
@@ -1096,6 +1703,40 @@ function apply(ctx) {
1096
1703
  }
1097
1704
  }
1098
1705
  }), "dsh-vscode-workbench: Codicon font");
1706
+ ctx.effect(() => ctx.webServer.register({
1707
+ kind: "exact",
1708
+ path: "/dsh-vscode/xlsx.js",
1709
+ handler: async (req, res) => {
1710
+ if (!authorize(req, res, ctx.connection)) return;
1711
+ try {
1712
+ const asset = xlsxAsset();
1713
+ res.writeHead(200, {
1714
+ "content-type": asset.type,
1715
+ "cache-control": "public, max-age=31536000, immutable"
1716
+ });
1717
+ res.end(await readFile(asset.path));
1718
+ } catch (error) {
1719
+ sendError(res, error);
1720
+ }
1721
+ }
1722
+ }), "dsh-vscode-workbench: SheetJS asset");
1723
+ ctx.effect(() => ctx.webServer.register({
1724
+ kind: "prefix",
1725
+ path: "/dsh-vscode/file-icons",
1726
+ handler: async (req, res) => {
1727
+ if (!authorize(req, res, ctx.connection)) return;
1728
+ try {
1729
+ const asset = fileIconAsset(decodeURIComponent(new URL(req.url ?? "/", "http://dsh.local").pathname.slice(23)));
1730
+ res.writeHead(200, {
1731
+ "content-type": asset.type,
1732
+ "cache-control": "public, max-age=31536000, immutable"
1733
+ });
1734
+ res.end(await readFile(asset.path));
1735
+ } catch (error) {
1736
+ sendError(res, error);
1737
+ }
1738
+ }
1739
+ }), "dsh-vscode-workbench: file icons");
1099
1740
  ctx.effect(() => ctx.webServer.register({
1100
1741
  kind: "exact",
1101
1742
  path: "/dsh-vscode/pdfjs/worker.mjs",
@@ -1186,6 +1827,54 @@ function apply(ctx) {
1186
1827
  }
1187
1828
  }
1188
1829
  }), "dsh-vscode-workbench: file preview");
1830
+ ctx.effect(() => ctx.webServer.register({
1831
+ kind: "exact",
1832
+ path: "/dsh-vscode/download",
1833
+ handler: async (req, res) => {
1834
+ if (!authorize(req, res, ctx.connection)) return;
1835
+ try {
1836
+ const url = new URL(req.url ?? "/", "http://dsh.local");
1837
+ const sessionId = url.searchParams.get("sessionId");
1838
+ const path = url.searchParams.get("path");
1839
+ if (sessionId === null || path === null) throw new WorkbenchError("bad-request", "sessionId and path are required");
1840
+ const file = await workspaceDownloadFile(cwdFor(ctx, sessionId), path);
1841
+ res.writeHead(200, {
1842
+ "content-type": "application/octet-stream",
1843
+ "content-length": file.size,
1844
+ "content-disposition": `attachment; filename*=UTF-8''${encodeURIComponent(file.name)}`,
1845
+ "x-content-type-options": "nosniff",
1846
+ "cache-control": "no-store"
1847
+ });
1848
+ createReadStream(file.path).on("error", (error) => res.destroy(error)).pipe(res);
1849
+ } catch (error) {
1850
+ sendError(res, error);
1851
+ }
1852
+ }
1853
+ }), "dsh-vscode-workbench: file download");
1854
+ ctx.effect(() => ctx.webServer.register({
1855
+ kind: "exact",
1856
+ path: "/dsh-vscode/office",
1857
+ handler: async (req, res) => {
1858
+ if (!authorize(req, res, ctx.connection)) return;
1859
+ try {
1860
+ const url = new URL(req.url ?? "/", "http://dsh.local");
1861
+ const sessionId = url.searchParams.get("sessionId");
1862
+ const path = url.searchParams.get("path");
1863
+ if (sessionId === null || path === null) throw new WorkbenchError("bad-request", "sessionId and path are required");
1864
+ const pdf = await officePdf(cwdFor(ctx, sessionId), path);
1865
+ const info = await stat(pdf.path);
1866
+ res.writeHead(200, {
1867
+ "content-type": "application/pdf",
1868
+ "content-length": info.size,
1869
+ "x-content-type-options": "nosniff",
1870
+ "cache-control": "no-store"
1871
+ });
1872
+ createReadStream(pdf.path).on("error", (error) => res.destroy(error)).pipe(res);
1873
+ } catch (error) {
1874
+ sendError(res, error);
1875
+ }
1876
+ }
1877
+ }), "dsh-vscode-workbench: office preview");
1189
1878
  ctx.effect(() => ctx.webServer.register({
1190
1879
  kind: "prefix",
1191
1880
  path: "/dsh-vscode/monaco",
@@ -1212,8 +1901,7 @@ function apply(ctx) {
1212
1901
  return;
1213
1902
  }
1214
1903
  wss.handleUpgrade(req, socket, head, (ws) => {
1215
- const search = new URL(req.url ?? "/", "http://dsh.local").searchParams;
1216
- const sessionId = search.get("sessionId");
1904
+ const sessionId = new URL(req.url ?? "/", "http://dsh.local").searchParams.get("sessionId");
1217
1905
  if (sessionId === null) {
1218
1906
  ws.close(1008, "sessionId required");
1219
1907
  return;
@@ -1225,43 +1913,107 @@ function apply(ctx) {
1225
1913
  ws.close(1008, "session workspace unavailable");
1226
1914
  return;
1227
1915
  }
1228
- const watcher = chokidar.watch(workspace, {
1229
- ignoreInitial: true,
1230
- ignored: watchIgnore(workspace, parseWatchIgnores(search.get("ignore"))),
1231
- awaitWriteFinish: {
1232
- stabilityThreshold: 100,
1233
- pollInterval: 25
1234
- }
1235
- });
1236
- watcher.on("error", (error) => {
1237
- console.warn("[dsh-vscode-workbench] file watcher stopped:", error);
1238
- if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
1239
- type: "files.watch-error",
1240
- message: String(error)
1241
- }));
1242
- watcher.close();
1243
- });
1916
+ let watcher;
1244
1917
  let paths = /* @__PURE__ */ new Set();
1245
1918
  let timer;
1246
- watcher.on("all", (_event, path) => {
1247
- const changed = isAbsolute(path) ? relative(workspace, path) : path;
1248
- paths.add(changed.split(sep).join("/"));
1249
- clearTimeout(timer);
1250
- timer = setTimeout(() => {
1919
+ workspaceWatchIgnores(workspace).then((extra) => {
1920
+ const instance = watcher = chokidar.watch(workspace, {
1921
+ ignoreInitial: true,
1922
+ ignored: watchIgnore(workspace, extra),
1923
+ awaitWriteFinish: {
1924
+ stabilityThreshold: 100,
1925
+ pollInterval: 25
1926
+ }
1927
+ });
1928
+ instance.on("error", (error) => {
1929
+ console.warn("[dsh-vscode-workbench] file watcher stopped:", error);
1251
1930
  if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
1252
- type: "files.changed",
1253
- paths: [...paths]
1931
+ type: "files.watch-error",
1932
+ message: String(error)
1254
1933
  }));
1255
- paths = /* @__PURE__ */ new Set();
1256
- }, 150);
1934
+ instance.close();
1935
+ });
1936
+ instance.on("all", (_event, path) => {
1937
+ const changed = isAbsolute(path) ? relative(workspace, path) : path;
1938
+ paths.add(changed.split(sep).join("/"));
1939
+ clearTimeout(timer);
1940
+ timer = setTimeout(() => {
1941
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
1942
+ type: "files.changed",
1943
+ paths: [...paths]
1944
+ }));
1945
+ paths = /* @__PURE__ */ new Set();
1946
+ }, 150);
1947
+ });
1257
1948
  });
1258
1949
  ws.on("close", () => {
1259
1950
  clearTimeout(timer);
1260
- watcher.close();
1951
+ watcher?.close();
1261
1952
  });
1262
1953
  });
1263
1954
  }
1264
1955
  }), "dsh-vscode-workbench: file watcher");
1956
+ const lspWss = new WebSocketServer({ noServer: true });
1957
+ ctx.effect(() => ctx.webServer.registerUpgrade({
1958
+ path: "/dsh-vscode/ws/lsp",
1959
+ handler(req, socket, head) {
1960
+ if (ctx.connection.requestRejection(req) !== void 0) {
1961
+ socket.destroy();
1962
+ return;
1963
+ }
1964
+ lspWss.handleUpgrade(req, socket, head, (ws) => {
1965
+ const search = new URL(req.url ?? "/", "http://dsh.local").searchParams;
1966
+ const sessionId = search.get("sessionId");
1967
+ const serverId = search.get("server");
1968
+ if (sessionId === null || serverId === null) {
1969
+ ws.close(1008, "sessionId and server required");
1970
+ return;
1971
+ }
1972
+ let workspace;
1973
+ try {
1974
+ workspace = cwdFor(ctx, sessionId);
1975
+ } catch {
1976
+ ws.close(1008, "session workspace unavailable");
1977
+ return;
1978
+ }
1979
+ (async () => {
1980
+ try {
1981
+ const server = await resolveLsp(serverId);
1982
+ const child = spawn(server.command, server.args, {
1983
+ cwd: workspace,
1984
+ stdio: [
1985
+ "pipe",
1986
+ "pipe",
1987
+ "pipe"
1988
+ ],
1989
+ shell: server.shell,
1990
+ windowsHide: true
1991
+ });
1992
+ const bridge = pipeLanguageServer(child, {
1993
+ onMessage: (message) => {
1994
+ if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(message));
1995
+ },
1996
+ onStderr: (text) => console.warn("[dsh-vscode-workbench] lsp:", text.trim().slice(0, 500)),
1997
+ onExit: (code) => {
1998
+ if (ws.readyState === WebSocket.OPEN) ws.close(1e3, `server exited ${code ?? ""}`.trim());
1999
+ }
2000
+ });
2001
+ ws.on("message", (data) => bridge.write(String(data)));
2002
+ ws.on("close", () => bridge.kill());
2003
+ ws.on("error", () => bridge.kill());
2004
+ child.on("error", (error) => {
2005
+ if (ws.readyState === WebSocket.OPEN) ws.close(1011, String(error).slice(0, 120));
2006
+ });
2007
+ } catch (error) {
2008
+ ws.close(1008, (error instanceof Error ? error.message : "language server unavailable").slice(0, 120));
2009
+ }
2010
+ })();
2011
+ });
2012
+ }
2013
+ }), "dsh-vscode-workbench: language server bridge");
2014
+ ctx.effect(() => () => {
2015
+ lspWss.close();
2016
+ }, "dsh-vscode-workbench: language server teardown");
1265
2017
  ctx.effect(() => () => {
1266
2018
  wss.close();
1267
2019
  }, "dsh-vscode-workbench: watcher teardown");