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.
- package/README.en.md +15 -6
- package/README.md +17 -6
- package/lib/client.js +5118 -728
- package/lib/client.js.map +1 -1
- package/lib/index.js +899 -57
- 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 +27 -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 +12 -0
- package/package.json +21 -17
- 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 +342 -78
- 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 +31 -30
- package/src/client/typescriptProject.ts +81 -0
- package/src/context.ts +8 -0
- package/src/host.ts +100 -15
- package/src/index.ts +123 -12
- package/src/lsp.ts +170 -0
- package/src/office.ts +95 -0
- package/src/types.ts +2 -0
- 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 {
|
|
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);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
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.
|
|
1163
|
-
|
|
1931
|
+
type: "files.watch-error",
|
|
1932
|
+
message: String(error)
|
|
1164
1933
|
}));
|
|
1165
|
-
|
|
1166
|
-
}
|
|
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
|
|
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");
|