dsh-vscode-workbench 0.1.12 → 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 +15 -6
  2. package/README.md +17 -6
  3. package/lib/client.js +5118 -728
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +899 -57
  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 +27 -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 +12 -0
  29. package/package.json +21 -17
  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 +342 -78
  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 +31 -30
  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 +123 -12
  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 +84 -0
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);
@@ -968,9 +1544,96 @@ function installTerminal(ctx) {
968
1544
  }, "dsh-vscode-workbench: terminal teardown");
969
1545
  }
970
1546
  //#endregion
1547
+ //#region src/watch-ignore.ts
1548
+ /** Exclusion rules for the recursive workspace file watcher. */
1549
+ /** Directory names skipped by default; large dependency and build trees exhaust inotify watches. */
1550
+ const DEFAULT_WATCH_IGNORES = [
1551
+ ".git",
1552
+ ".hg",
1553
+ ".svn",
1554
+ "node_modules",
1555
+ "bower_components",
1556
+ ".pnpm-store",
1557
+ ".yarn",
1558
+ ".next",
1559
+ ".nuxt",
1560
+ ".turbo",
1561
+ ".vite",
1562
+ ".cache",
1563
+ "coverage",
1564
+ "vendor",
1565
+ "third_party",
1566
+ "third-party",
1567
+ "third_part",
1568
+ "third-part",
1569
+ "build",
1570
+ "dist",
1571
+ "out",
1572
+ "target",
1573
+ ".venv",
1574
+ "venv",
1575
+ "__pycache__",
1576
+ ".mypy_cache",
1577
+ ".pytest_cache",
1578
+ ".ruff_cache",
1579
+ ".gradle",
1580
+ ".idea",
1581
+ ".vs",
1582
+ "Pods"
1583
+ ];
1584
+ function normalize$1(entry) {
1585
+ return entry.trim().replace(/\\/g, "/").replace(/^\.\//, "").replace(/\/+$/, "");
1586
+ }
1587
+ function segmentsOf(entry) {
1588
+ return normalize$1(entry).split("/").filter((part) => part !== "" && part !== ".");
1589
+ }
1590
+ /** Return true when a workspace-relative path contains one of the excluded segment sequences. */
1591
+ function isIgnoredPath(relativePath, patterns) {
1592
+ const segments = relativePath.split(/[\\/]+/).filter(Boolean).map((segment) => segment.toLowerCase());
1593
+ if (segments.length === 0) return false;
1594
+ for (const pattern of patterns) {
1595
+ const parts = segmentsOf(pattern).map((part) => part.toLowerCase());
1596
+ if (parts.length === 0 || parts.includes("..")) continue;
1597
+ if (parts.length === 1) {
1598
+ if (segments.includes(parts[0])) return true;
1599
+ continue;
1600
+ }
1601
+ for (let index = 0; index + parts.length <= segments.length; index++) if (parts.every((part, offset) => segments[index + offset] === part)) return true;
1602
+ }
1603
+ return false;
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
+ }
1622
+ /** Build a chokidar `ignored` predicate for one workspace and the user's extra exclusions. */
1623
+ function watchIgnore(workspace, extra) {
1624
+ const patterns = [...DEFAULT_WATCH_IGNORES, ...extra];
1625
+ return (path) => {
1626
+ const path_ = relative(workspace, path);
1627
+ if (path_ === "" || path_.startsWith("..") || isAbsolute(path_)) return false;
1628
+ return isIgnoredPath(path_.split(sep).join("/"), patterns);
1629
+ };
1630
+ }
1631
+ //#endregion
971
1632
  //#region src/index.ts
972
1633
  /** DSH host plugin for the VS Code workbench. */
973
1634
  const name = "dsh-vscode-workbench";
1635
+ let dshDirectoryPicker;
1636
+ let dshWorkspaceRegistry;
974
1637
  const inject = [
975
1638
  "webServer",
976
1639
  "sessions",
@@ -990,6 +1653,16 @@ async function dispatch(ctx, method, body, signal) {
990
1653
  case "fs.create": return createEntry(cwd, stringField(body, "path"), body.kind === "directory" ? "directory" : "file").then(() => ({ ok: true }));
991
1654
  case "fs.rename": return renameEntry(cwd, stringField(body, "path"), stringField(body, "nextPath")).then(() => ({ ok: true }));
992
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();
993
1666
  case "search": return searchWorkspace(cwd, body, signal);
994
1667
  case "git.status": return gitStatus(cwd);
995
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);
@@ -997,6 +1670,21 @@ async function dispatch(ctx, method, body, signal) {
997
1670
  }
998
1671
  }
999
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");
1000
1688
  installTerminal(ctx);
1001
1689
  ctx.effect(() => ctx.webServer.register({
1002
1690
  kind: "exact",
@@ -1015,6 +1703,40 @@ function apply(ctx) {
1015
1703
  }
1016
1704
  }
1017
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");
1018
1740
  ctx.effect(() => ctx.webServer.register({
1019
1741
  kind: "exact",
1020
1742
  path: "/dsh-vscode/pdfjs/worker.mjs",
@@ -1105,6 +1827,54 @@ function apply(ctx) {
1105
1827
  }
1106
1828
  }
1107
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");
1108
1878
  ctx.effect(() => ctx.webServer.register({
1109
1879
  kind: "prefix",
1110
1880
  path: "/dsh-vscode/monaco",
@@ -1143,35 +1913,107 @@ function apply(ctx) {
1143
1913
  ws.close(1008, "session workspace unavailable");
1144
1914
  return;
1145
1915
  }
1146
- const watcher = chokidar.watch(workspace, {
1147
- ignoreInitial: true,
1148
- ignored: /(^|[/\\])(\.git|node_modules|\.next|\.nuxt|\.turbo|\.vite|coverage)([/\\]|$)/,
1149
- awaitWriteFinish: {
1150
- stabilityThreshold: 100,
1151
- pollInterval: 25
1152
- }
1153
- });
1916
+ let watcher;
1154
1917
  let paths = /* @__PURE__ */ new Set();
1155
1918
  let timer;
1156
- watcher.on("all", (_event, path) => {
1157
- const changed = isAbsolute(path) ? relative(workspace, path) : path;
1158
- paths.add(changed.split(sep).join("/"));
1159
- clearTimeout(timer);
1160
- 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);
1161
1930
  if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
1162
- type: "files.changed",
1163
- paths: [...paths]
1931
+ type: "files.watch-error",
1932
+ message: String(error)
1164
1933
  }));
1165
- paths = /* @__PURE__ */ new Set();
1166
- }, 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
+ });
1167
1948
  });
1168
1949
  ws.on("close", () => {
1169
1950
  clearTimeout(timer);
1170
- watcher.close();
1951
+ watcher?.close();
1171
1952
  });
1172
1953
  });
1173
1954
  }
1174
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");
1175
2017
  ctx.effect(() => () => {
1176
2018
  wss.close();
1177
2019
  }, "dsh-vscode-workbench: watcher teardown");