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.
- package/README.en.md +4 -0
- package/README.md +8 -2
- package/lib/client.js +5014 -674
- package/lib/client.js.map +1 -1
- package/lib/index.js +832 -80
- package/lib/index.js.map +1 -1
- package/lib/types/client/ActivityBar.d.ts +24 -0
- package/lib/types/client/DshConversationPanel.d.ts +1 -1
- package/lib/types/client/FileIcon.d.ts +8 -0
- package/lib/types/client/Icon.d.ts +1 -3
- package/lib/types/client/MonacoEditor.d.ts +36 -0
- package/lib/types/client/Workbench.d.ts +19 -7
- package/lib/types/client/WorkspaceSwitcher.d.ts +13 -0
- package/lib/types/client/api.d.ts +2 -0
- package/lib/types/client/documentPreview.d.ts +10 -2
- package/lib/types/client/dshComposer.d.ts +21 -0
- package/lib/types/client/dshNavigation.d.ts +46 -0
- package/lib/types/client/fileIconMap.d.ts +2 -0
- package/lib/types/client/gitError.d.ts +2 -0
- package/lib/types/client/lsp.d.ts +27 -0
- package/lib/types/client/searchHighlight.d.ts +5 -0
- package/lib/types/client/typescriptProject.d.ts +20 -0
- package/lib/types/context.d.ts +15 -0
- package/lib/types/host.d.ts +37 -0
- package/lib/types/lsp.d.ts +54 -0
- package/lib/types/office.d.ts +17 -0
- package/lib/types/types.d.ts +5 -0
- package/lib/types/watch-ignore.d.ts +4 -0
- package/package.json +5 -2
- package/src/client/ActivityBar.tsx +152 -0
- package/src/client/DshConversationPanel.tsx +15 -12
- package/src/client/FileIcon.tsx +6 -0
- package/src/client/Icon.tsx +1 -14
- package/src/client/MonacoEditor.tsx +187 -19
- package/src/client/TerminalPanel.tsx +6 -3
- package/src/client/Workbench.tsx +324 -81
- package/src/client/WorkspaceSwitcher.tsx +53 -0
- package/src/client/api.ts +27 -1
- package/src/client/documentPreview.tsx +92 -5
- package/src/client/dshComposer.ts +73 -0
- package/src/client/dshNavigation.ts +81 -0
- package/src/client/fileIconMap.ts +7 -0
- package/src/client/gitError.ts +5 -0
- package/src/client/index.tsx +28 -14
- package/src/client/lsp.ts +300 -0
- package/src/client/searchHighlight.ts +6 -0
- package/src/client/styles.ts +28 -31
- package/src/client/typescriptProject.ts +81 -0
- package/src/context.ts +8 -0
- package/src/host.ts +100 -15
- package/src/index.ts +121 -17
- package/src/lsp.ts +170 -0
- package/src/office.ts +95 -0
- package/src/types.ts +2 -0
- 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 {
|
|
8
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
|
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":
|
|
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
|
|
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
|
|
650
|
-
"
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
"
|
|
674
|
-
file.
|
|
675
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
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.
|
|
1253
|
-
|
|
1931
|
+
type: "files.watch-error",
|
|
1932
|
+
message: String(error)
|
|
1254
1933
|
}));
|
|
1255
|
-
|
|
1256
|
-
}
|
|
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
|
|
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");
|