dsh-vscode-workbench 0.1.5 → 0.1.6
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/lib/client.js +199 -71
- package/lib/client.js.map +1 -1
- package/lib/index.js +16 -7
- package/lib/index.js.map +1 -1
- package/lib/types/client/Workbench.d.ts +7 -0
- package/lib/types/client/api.d.ts +4 -0
- package/lib/types/types.d.ts +2 -2
- package/package.json +1 -1
- package/src/client/Workbench.tsx +30 -16
- package/src/client/api.ts +6 -2
- package/src/client/styles.ts +1 -1
- package/src/host.ts +6 -3
- package/src/index.ts +5 -2
- package/src/types.ts +1 -1
package/lib/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
import { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
3
4
|
import chokidar from "chokidar";
|
|
4
5
|
import { WebSocket, WebSocketServer } from "ws";
|
|
5
6
|
import { createHash, randomUUID } from "node:crypto";
|
|
6
7
|
import { spawn } from "node:child_process";
|
|
7
|
-
import { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from "node:path";
|
|
8
8
|
import { constants } from "node:fs";
|
|
9
9
|
//#region src/wire.ts
|
|
10
10
|
var WorkbenchError = class extends Error {
|
|
@@ -134,8 +134,15 @@ async function listFiles(cwd, path) {
|
|
|
134
134
|
const values = await readdir(absolute, { withFileTypes: true });
|
|
135
135
|
return (await Promise.all(values.filter((item) => item.name !== ".git").map(async (item) => {
|
|
136
136
|
const full = join(absolute, item.name);
|
|
137
|
-
|
|
138
|
-
|
|
137
|
+
if (!item.isSymbolicLink()) return {
|
|
138
|
+
name: item.name,
|
|
139
|
+
path: relativePath(root, full),
|
|
140
|
+
kind: item.isDirectory() ? "directory" : "file"
|
|
141
|
+
};
|
|
142
|
+
const actual = await realpath(full).catch(() => void 0);
|
|
143
|
+
if (actual === void 0 || !inside(root, actual)) return void 0;
|
|
144
|
+
const info = await stat(full);
|
|
145
|
+
const kind = info.isDirectory() ? "directory" : "file";
|
|
139
146
|
return {
|
|
140
147
|
name: item.name,
|
|
141
148
|
path: relativePath(root, full),
|
|
@@ -143,7 +150,7 @@ async function listFiles(cwd, path) {
|
|
|
143
150
|
size: info.size,
|
|
144
151
|
mtimeMs: info.mtimeMs
|
|
145
152
|
};
|
|
146
|
-
}))).sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "directory" ? -1 : 1);
|
|
153
|
+
}))).filter((row) => row !== void 0).sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === "directory" ? -1 : 1);
|
|
147
154
|
}
|
|
148
155
|
async function readDocument(cwd, path) {
|
|
149
156
|
const { root, absolute } = await workspacePath(cwd, path);
|
|
@@ -858,9 +865,10 @@ function apply(ctx) {
|
|
|
858
865
|
ws.close(1008, "sessionId required");
|
|
859
866
|
return;
|
|
860
867
|
}
|
|
861
|
-
const
|
|
868
|
+
const workspace = cwdFor(ctx, sessionId);
|
|
869
|
+
const watcher = chokidar.watch(workspace, {
|
|
862
870
|
ignoreInitial: true,
|
|
863
|
-
ignored: /(^|[/\\])(\.git|node_modules)([/\\]|$)/,
|
|
871
|
+
ignored: /(^|[/\\])(\.git|node_modules|\.next|\.nuxt|\.turbo|\.vite|coverage)([/\\]|$)/,
|
|
864
872
|
awaitWriteFinish: {
|
|
865
873
|
stabilityThreshold: 100,
|
|
866
874
|
pollInterval: 25
|
|
@@ -869,7 +877,8 @@ function apply(ctx) {
|
|
|
869
877
|
let paths = /* @__PURE__ */ new Set();
|
|
870
878
|
let timer;
|
|
871
879
|
watcher.on("all", (_event, path) => {
|
|
872
|
-
|
|
880
|
+
const changed = isAbsolute(path) ? relative(workspace, path) : path;
|
|
881
|
+
paths.add(changed.split(sep).join("/"));
|
|
873
882
|
clearTimeout(timer);
|
|
874
883
|
timer = setTimeout(() => {
|
|
875
884
|
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/wire.ts","../src/host.ts","../src/index.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport class WorkbenchError extends Error {\n constructor(public code: string, message: string, public status = 400) { super(message) }\n}\n\nexport async function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += value.length\n if (size > 2 * 1024 * 1024) throw new WorkbenchError('too-large', 'request body is too large', 413)\n chunks.push(value)\n }\n try {\n const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')\n if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('object expected')\n return value as Record<string, unknown>\n } catch { throw new WorkbenchError('bad-json', 'request body must be a JSON object') }\n}\n\n/** Read a binary upload while enforcing a fixed request limit. */\nexport async function readBinaryBody(req: IncomingMessage, limit = 100 * 1024 * 1024): Promise<Buffer> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += value.length\n if (size > limit) throw new WorkbenchError('too-large', 'uploaded file is too large', 413)\n chunks.push(value)\n }\n return Buffer.concat(chunks)\n}\n\nexport function stringField(body: Record<string, unknown>, key: string): string {\n const value = body[key]\n if (typeof value !== 'string') throw new WorkbenchError('bad-request', `${key} must be a string`)\n return value\n}\n\nexport function send(res: ServerResponse, status: number, value: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(JSON.stringify(value))\n}\n\nexport function sendError(res: ServerResponse, error: unknown): void {\n const known = error instanceof WorkbenchError ? error : new WorkbenchError('internal', error instanceof Error ? error.message : String(error), 500)\n send(res, known.status, { ok: false, error: { code: known.code, message: known.message } })\n}\n\nexport function trusted(req: IncomingMessage, allowed: readonly string[]): boolean {\n const host = req.headers.host\n if (host === undefined || req.headers['sec-fetch-site'] === 'cross-site') return false\n let hostUrl: URL\n try { hostUrl = new URL(`http://${host}`) } catch { return false }\n const loopback = hostUrl.hostname === 'localhost' || hostUrl.hostname === '[::1]' || /^127(?:\\.\\d{1,3}){3}$/.test(hostUrl.hostname)\n if (!loopback && !allowed.some(candidate => candidate === host || candidate === hostUrl.hostname)) return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try { return new URL(origin).host === hostUrl.host } catch { return false }\n}\n","import { createHash, randomUUID } from 'node:crypto'\nimport { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'\nimport { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { constants } from 'node:fs'\nimport type { HostContext } from './context.ts'\nimport type { FileDocument, FileEntry, GitChange, GitCommit, GitCommitFile, GitDiff, GitStatus, SearchMatch, SearchRequest } from './types.ts'\nimport { WorkbenchError } from './wire.ts'\n\nconst require = createRequire(import.meta.url)\nconst TEXT_LIMIT = 5 * 1024 * 1024\nconst PROCESS_TIMEOUT = 30_000\n\nexport function cwdFor(ctx: HostContext, sessionId: string): string {\n const cwd = ctx.sessions.get(sessionId)?.header.cwd\n if (cwd === undefined || cwd === '') return process.cwd()\n if (!isAbsolute(cwd)) throw new WorkbenchError('cwd-invalid', 'session working directory is not absolute')\n return normalize(cwd)\n}\n\nfunction inside(root: string, target: string): boolean {\n const rel = relative(root, target)\n return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel))\n}\n\nasync function canonicalRoot(cwd: string): Promise<string> {\n return realpath(cwd).catch(() => { throw new WorkbenchError('cwd-missing', 'session working directory does not exist', 404) })\n}\n\n/** Resolve a caller-relative path and reject traversal and symlink escape. */\nexport async function workspacePath(cwd: string, raw: string, create = false): Promise<{ root: string; absolute: string }> {\n if (isAbsolute(raw)) throw new WorkbenchError('path-invalid', 'absolute paths are not accepted')\n const root = await canonicalRoot(cwd)\n const absolute = resolve(root, raw === '' ? '.' : raw)\n if (!inside(root, absolute)) throw new WorkbenchError('path-outside', 'path is outside the workspace', 403)\n if (create) {\n const parent = await realpath(dirname(absolute)).catch(() => { throw new WorkbenchError('parent-missing', 'parent directory does not exist', 404) })\n if (!inside(root, parent)) throw new WorkbenchError('path-outside', 'path follows a symlink outside the workspace', 403)\n } else {\n const actual = await realpath(absolute).catch(() => { throw new WorkbenchError('not-found', 'path does not exist', 404) })\n if (!inside(root, actual)) throw new WorkbenchError('path-outside', 'path follows a symlink outside the workspace', 403)\n }\n return { root, absolute }\n}\n\nfunction relativePath(root: string, absolute: string): string { return relative(root, absolute).split(sep).join('/') }\nfunction revision(content: Buffer, mtimeMs: number): string {\n return createHash('sha256').update(content).update(String(mtimeMs)).digest('hex')\n}\n\nexport async function listFiles(cwd: string, path: string): Promise<FileEntry[]> {\n const { root, absolute } = await workspacePath(cwd, path)\n const values = await readdir(absolute, { withFileTypes: true })\n const rows = await Promise.all(values.filter(item => item.name !== '.git').map(async item => {\n const full = join(absolute, item.name)\n const info = await lstat(full)\n const kind = item.isDirectory() ? 'directory' as const : 'file' as const\n return { name: item.name, path: relativePath(root, full), kind, size: info.size, mtimeMs: info.mtimeMs }\n }))\n return rows.sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === 'directory' ? -1 : 1)\n}\n\nexport async function readDocument(cwd: string, path: string): Promise<FileDocument> {\n const { root, absolute } = await workspacePath(cwd, path)\n const info = await stat(absolute)\n if (!info.isFile()) throw new WorkbenchError('not-file', 'path is not a file')\n const tooLarge = info.size > TEXT_LIMIT\n const handle = await open(absolute, 'r')\n try {\n const head = Buffer.alloc(Math.min(info.size, tooLarge ? 4096 : TEXT_LIMIT))\n const { bytesRead } = await handle.read(head, 0, head.length, 0)\n const bytes = head.subarray(0, bytesRead)\n const binary = bytes.includes(0)\n return {\n path: relativePath(root, absolute), content: binary || tooLarge ? '' : bytes.toString('utf8'),\n revision: revision(bytes, info.mtimeMs), size: info.size, binary, tooLarge,\n }\n } finally { await handle.close() }\n}\n\nexport async function saveDocument(cwd: string, path: string, content: string, expected: string): Promise<FileDocument> {\n const { absolute } = await workspacePath(cwd, path)\n const current = await readDocument(cwd, path)\n if (current.revision !== expected) throw new WorkbenchError('revision-conflict', 'file changed on disk; reload before saving', 409)\n const temp = join(dirname(absolute), `.${randomUUID()}.dsh-vscode.tmp`)\n await writeFile(temp, content, { encoding: 'utf8', flag: 'wx' })\n try { await rename(temp, absolute) } catch (error) { await rm(temp, { force: true }); throw error }\n return readDocument(cwd, path)\n}\n\nexport async function createEntry(cwd: string, path: string, kind: 'file' | 'directory'): Promise<void> {\n const { absolute } = await workspacePath(cwd, path, true)\n if (kind === 'directory') await mkdir(absolute)\n else { const handle = await open(absolute, 'wx'); await handle.close() }\n}\n\n/** Write an uploaded file into an existing workspace directory without overwriting files. */\nexport async function uploadFile(cwd: string, directory: string, name: string, content: Buffer): Promise<FileEntry> {\n if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\\\\')) throw new WorkbenchError('filename-invalid', 'uploaded file name is invalid')\n const targetPath = directory === '' ? name : `${directory}/${name}`\n const { root, absolute } = await workspacePath(cwd, targetPath, true)\n const handle = await open(absolute, 'wx')\n try { await handle.writeFile(content) } finally { await handle.close() }\n const info = await lstat(absolute)\n return { name, path: relativePath(root, absolute), kind: 'file', size: info.size, mtimeMs: info.mtimeMs }\n}\n\nexport async function renameEntry(cwd: string, path: string, nextPath: string): Promise<void> {\n const source = await workspacePath(cwd, path)\n const target = await workspacePath(cwd, nextPath, true)\n await access(target.absolute, constants.F_OK).then(\n () => { throw new WorkbenchError('target-exists', 'destination already exists', 409) }, () => undefined,\n )\n await rename(source.absolute, target.absolute)\n}\n\nexport async function deleteEntry(cwd: string, path: string): Promise<void> {\n if (path === '') throw new WorkbenchError('root-delete', 'cannot delete workspace root', 403)\n const { absolute } = await workspacePath(cwd, path)\n await rm(absolute, { recursive: true, force: false })\n}\n\nconst IMAGE_TYPES:Record<string,string>={'.avif':'image/avif','.bmp':'image/bmp','.gif':'image/gif','.ico':'image/x-icon','.jpeg':'image/jpeg','.jpg':'image/jpeg','.png':'image/png','.svg':'image/svg+xml','.webp':'image/webp'}\n/** Resolve an image inside the workspace for protected browser preview. */\nexport async function workspaceImage(cwd:string,path:string):Promise<{path:string;type:string}>{const resolved=await workspacePath(cwd,path);const type=IMAGE_TYPES[extname(resolved.absolute).toLowerCase()];if(type===undefined)throw new WorkbenchError('unsupported-media','file is not a supported image',415);return {path:resolved.absolute,type}}\n\ninterface ProcessResult { stdout: string; stderr: string; code: number }\nexport function run(command: string, args: string[], cwd: string, timeout = PROCESS_TIMEOUT, signal?: AbortSignal): Promise<ProcessResult> {\n return new Promise((resolvePromise, reject) => {\n const child = spawn(command, args, { cwd, shell: false, windowsHide: true })\n let stdout = ''; let stderr = ''; let settled = false\n const timer = setTimeout(() => { child.kill(); if (!settled) reject(new WorkbenchError('timeout', `${command} timed out`, 504)) }, timeout)\n const abort = () => { child.kill(); if (!settled) { settled = true; clearTimeout(timer); reject(new WorkbenchError('cancelled', `${command} cancelled`, 499)) } }\n signal?.addEventListener('abort', abort, { once: true })\n child.stdout.on('data', chunk => { stdout += String(chunk) })\n child.stderr.on('data', chunk => { stderr += String(chunk) })\n child.on('error', error => { clearTimeout(timer); signal?.removeEventListener('abort', abort); if (!settled) { settled = true; reject(error) } })\n child.on('close', code => { clearTimeout(timer); signal?.removeEventListener('abort', abort); if (!settled) { settled = true; resolvePromise({ stdout, stderr, code: code ?? 1 }) } })\n })\n}\n\nexport async function searchWorkspace(cwd: string, request: SearchRequest, signal?: AbortSignal): Promise<{ matches: SearchMatch[]; limited: boolean }> {\n if (request.query === '') return { matches: [], limited: false }\n const root = await canonicalRoot(cwd)\n const rg = require('@vscode/ripgrep').rgPath as string\n const args = ['--json', '--line-number', '--column', '--hidden', '--glob', '!.git/**', '--max-count', '2000']\n if (!request.regex) args.push('--fixed-strings')\n if (!request.caseSensitive) args.push('--ignore-case')\n if (request.wholeWord) args.push('--word-regexp')\n if (request.include) args.push('--glob', request.include)\n if (request.exclude) args.push('--glob', `!${request.exclude}`)\n args.push('--', request.query, '.')\n const result = await run(rg, args, root, PROCESS_TIMEOUT, signal)\n if (result.code > 1) throw new WorkbenchError('search-failed', result.stderr || 'ripgrep failed', 500)\n const matches: SearchMatch[] = []\n for (const line of result.stdout.split('\\n')) {\n if (line === '') continue\n const event = JSON.parse(line) as { type: string; data?: { path?: { text?: string }; lines?: { text?: string }; line_number?: number; submatches?: Array<{ start: number; end: number; match?: { text?: string } }> } }\n if (event.type !== 'match' || event.data?.path?.text === undefined || event.data.lines?.text === undefined) continue\n for (const hit of event.data.submatches ?? []) {\n matches.push({ path: event.data.path.text.replace(/^\\.\\//, ''), line: event.data.line_number ?? 1, column: hit.start + 1, preview: event.data.lines.text.trimEnd(), match: hit.match?.text ?? '' })\n if (matches.length >= 2000) return { matches, limited: true }\n }\n }\n return { matches, limited: false }\n}\n\nasync function gitRoot(cwd: string): Promise<string> {\n const result = await run('git', ['rev-parse', '--show-toplevel'], cwd)\n if (result.code !== 0) throw new WorkbenchError('not-repository', 'current workspace is not inside a Git repository', 404)\n return result.stdout.trim()\n}\n\nfunction validateCommitHash(value?: string): string {\n if (!/^[0-9a-f]{7,64}$/i.test(value ?? '')) throw new WorkbenchError('bad-request', 'commit hash is invalid')\n return value!\n}\n\nfunction validateGitPath(repository: string, path?: string): string {\n if (path === undefined || path === '' || isAbsolute(path) || !inside(repository, resolve(repository, path))) throw new WorkbenchError('bad-request', 'repository-relative path is required')\n return path\n}\n\nasync function gitObject(repository: string, spec: string): Promise<string> {\n const result = await run('git', ['show', '--no-textconv', spec], repository)\n return result.code === 0 ? result.stdout : ''\n}\n\nasync function commitFiles(repository: string, hash: string): Promise<GitCommitFile[]> {\n const parent=await run('git',['rev-parse','--verify',`${hash}^1`],repository)\n const result = parent.code===0\n ? await run('git',['diff','--name-status','-z','-M',parent.stdout.trim(),hash,'--'],repository)\n : await run('git', ['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', '-z', '-M', hash], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n const tokens = result.stdout.split('\\0').filter(Boolean); const files: GitCommitFile[] = []\n for (let index = 0; index < tokens.length;) {\n const status = tokens[index++] ?? ''; const code = status[0] ?? ''\n if (code === 'R' || code === 'C') { const previousPath=tokens[index++]??'';const path=tokens[index++]??'';files.push({path,previousPath,status:code}) }\n else { const path=tokens[index++]??'';files.push({path,status:code}) }\n }\n return files\n}\n\nexport async function gitStatus(cwd: string): Promise<GitStatus> {\n const repository = await gitRoot(cwd)\n const result = await run('git', ['status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr, 500)\n let branch = 'HEAD'; let ahead = 0; let behind = 0\n const changes: GitChange[] = []\n for (const record of result.stdout.split('\\0')) {\n if (record.startsWith('# branch.head ')) branch = record.slice(14)\n else if (record.startsWith('# branch.ab ')) {\n const match = /\\+(\\d+) -(\\d+)/.exec(record); ahead = Number(match?.[1] ?? 0); behind = Number(match?.[2] ?? 0)\n } else if (record.startsWith('? ')) changes.push({ path: record.slice(2), index: '?', worktree: '?', staged: false, untracked: true })\n else if (record.startsWith('1 ') || record.startsWith('2 ')) {\n const parts = record.split(' '); const xy = parts[1] ?? '..'; const path = parts.slice(record.startsWith('2 ') ? 9 : 8).join(' ')\n changes.push({ path, index: xy[0] ?? '.', worktree: xy[1] ?? '.', staged: xy[0] !== '.', untracked: false })\n }\n }\n return { repository, branch, ahead, behind, changes }\n}\n\nexport async function gitAction(cwd: string, action: string, path?: string, value?: string): Promise<unknown> {\n const repository = await gitRoot(cwd)\n let args: string[]\n switch (action) {\n case 'stage': args = ['add', '--', path ?? '']; break\n case 'stage.all': args = ['add', '-A']; break\n case 'unstage': args = ['restore', '--staged', '--', path ?? '']; break\n case 'unstage.all': args = ['restore', '--staged', '--', '.']; break\n case 'discard': {\n args = value === 'untracked' ? ['clean', '-f', '--', path ?? ''] : ['restore', '--worktree', '--', path ?? '']\n break\n }\n case 'discard.all': {\n const head = await run('git', ['rev-parse', '--verify', 'HEAD'], repository)\n if (head.code === 0) {\n const restore = await run('git', ['restore', '--source=HEAD', '--staged', '--worktree', '--', '.'], repository)\n if (restore.code !== 0) throw new WorkbenchError('git-failed', restore.stderr || restore.stdout, 409)\n }\n const clean = await run('git', ['clean', '-fd'], repository)\n if (clean.code !== 0) throw new WorkbenchError('git-failed', clean.stderr || clean.stdout, 409)\n return gitStatus(cwd)\n }\n case 'commit': args = ['commit', '-m', value ?? '']; break\n case 'branch.create': args = ['switch', '-c', value ?? '']; break\n case 'branch.switch': args = ['switch', value ?? '']; break\n case 'branches': {\n const result = await run('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr, 500)\n return result.stdout.split('\\n').filter(Boolean)\n }\n case 'log': {\n const head = await run('git', ['rev-parse', '--verify', 'HEAD'], repository)\n if (head.code !== 0) return []\n const result = await run('git', ['log', '--topo-order', '--date=iso-strict', '--decorate=short', '--pretty=format:%H%x1f%P%x1f%an%x1f%aI%x1f%s%x1f%D%x00', '-n', '100'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 500)\n return result.stdout.split('\\0').filter(Boolean).map((record): GitCommit => {\n const [rawHash = '', parents = '', author = '', date = '', subject = '', refs = ''] = record.split('\\x1f');const hash=rawHash.trim()\n return { hash, parents: parents.split(' ').filter(Boolean), author, date, subject, refs: refs.split(',').map(item => item.trim()).filter(Boolean) }\n })\n }\n case 'commit.show': {\n const hash = validateCommitHash(value)\n const result = await run('git', ['show', '--no-ext-diff', '--find-renames', '--format=fuller', '--stat', '--patch', '--no-color', hash], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n return { diff: result.stdout, original: '', modified: result.stdout } satisfies GitDiff\n }\n case 'commit.files': return commitFiles(repository, validateCommitHash(value))\n case 'commit.file.diff': {\n const hash=validateCommitHash(value);const target=validateGitPath(repository,path);const files=await commitFiles(repository,hash);const file=files.find(item=>item.path===target)\n if (file===undefined) throw new WorkbenchError('not-found','file is not part of the commit',404)\n const parent=await run('git',['rev-parse','--verify',`${hash}^1`],repository)\n const shown=parent.code===0\n ? await run('git',['diff','--no-ext-diff','--find-renames','--patch','--no-color',parent.stdout.trim(),hash,'--',file.previousPath??target,target],repository)\n : await run('git',['show','--no-ext-diff','--find-renames','--format=','--patch','--no-color',hash,'--',file.previousPath??target,target],repository)\n if(shown.code!==0)throw new WorkbenchError('git-failed',shown.stderr||shown.stdout,409)\n const original=file.status==='A'||parent.code!==0?'':await gitObject(repository,`${parent.stdout.trim()}:${file.previousPath??target}`)\n const modified=file.status==='D'?'':await gitObject(repository,`${hash}:${target}`)\n return {diff:shown.stdout,original,modified} satisfies GitDiff\n }\n case 'diff': {\n const untracked = value === 'untracked'\n const staged = value === 'staged'\n args = untracked\n ? ['diff', '--no-index', '--', process.platform === 'win32' ? 'NUL' : '/dev/null', path ?? '']\n : ['diff', ...(staged ? ['--cached'] : []), '--', path ?? '']\n break\n }\n default: throw new WorkbenchError('bad-action', `unknown Git action ${action}`)\n }\n if (args.some(arg => arg === '') && action !== 'commit') throw new WorkbenchError('bad-request', 'path or value is required')\n const result = await run('git', args, repository)\n const expectedUntrackedDifference = action === 'diff' && value === 'untracked' && result.code === 1\n if (result.code !== 0 && !expectedUntrackedDifference) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n if(action!=='diff')return gitStatus(cwd)\n const target=validateGitPath(repository,path)\n const original=value==='untracked'?'':await gitObject(repository,`${value==='staged'?'HEAD:':':'}${target}`)\n const modified=value==='staged'?await gitObject(repository,`:${target}`):await readFile(resolve(repository,target),'utf8').catch(()=> '')\n return {diff:result.stdout,original,modified} satisfies GitDiff\n}\n\n/** Resolve the official VS Code Codicon font shipped by @vscode/codicons. */\nexport function codiconAsset(): { path: string; type: string } {\n const packageRoot = dirname(require.resolve('@vscode/codicons/package.json'))\n return { path: resolve(packageRoot, 'dist', 'codicon.ttf'), type: 'font/ttf' }\n}\n\nexport function monacoAsset(raw: string): { path: string; type: string } {\n const packageRoot = dirname(require.resolve('monaco-editor/package.json'))\n const path = resolve(packageRoot, 'min', raw)\n const root = resolve(packageRoot, 'min')\n if (!inside(root, path)) throw new WorkbenchError('asset-invalid', 'invalid Monaco asset path', 403)\n const type = extname(path) === '.css' ? 'text/css' : extname(path) === '.js' ? 'text/javascript' : extname(path) === '.ttf' ? 'font/ttf' : 'application/octet-stream'\n return { path, type }\n}\n","/** DSH host plugin for the VS Code workbench. */\nimport { readFile } from 'node:fs/promises'\nimport type { IncomingMessage } from 'node:http'\nimport type { Duplex } from 'node:stream'\nimport chokidar from 'chokidar'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport type { HostContext } from './context.ts'\nimport { codiconAsset, createEntry, cwdFor, deleteEntry, gitAction, gitStatus, listFiles, monacoAsset, readDocument, renameEntry, saveDocument, searchWorkspace, uploadFile, workspaceImage } from './host.ts'\nimport type { SearchRequest } from './types.ts'\nimport { readBinaryBody, readBody, send, sendError, stringField, trusted, WorkbenchError } from './wire.ts'\n\nexport { WorkbenchError } from './wire.ts'\nexport * from './types.ts'\nexport const name = 'dsh-vscode-workbench'\nexport const inject = ['webServer', 'sessions', 'webRuntime']\n\nasync function dispatch(ctx: HostContext, method: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n const sessionId = stringField(body, 'sessionId'); const cwd = cwdFor(ctx, sessionId)\n switch (method) {\n case 'workspace': return { cwd, name: cwd.split(/[\\\\/]/).at(-1) || cwd }\n case 'fs.list': return listFiles(cwd, stringField(body, 'path'))\n case 'fs.read': return readDocument(cwd, stringField(body, 'path'))\n case 'fs.save': return saveDocument(cwd, stringField(body, 'path'), stringField(body, 'content'), stringField(body, 'revision'))\n case 'fs.create': return createEntry(cwd, stringField(body, 'path'), body.kind === 'directory' ? 'directory' : 'file').then(() => ({ ok: true }))\n case 'fs.rename': return renameEntry(cwd, stringField(body, 'path'), stringField(body, 'nextPath')).then(() => ({ ok: true }))\n case 'fs.delete': return deleteEntry(cwd, stringField(body, 'path')).then(() => ({ ok: true }))\n case 'search': return searchWorkspace(cwd, body as unknown as SearchRequest, signal)\n case 'git.status': return gitStatus(cwd)\n case 'git.action': return gitAction(cwd, stringField(body, 'action'), typeof body.path === 'string' ? body.path : undefined, typeof body.value === 'string' ? body.value : undefined)\n default: throw new WorkbenchError('not-found', `unknown workbench API method ${method}`, 404)\n }\n}\n\nexport function apply(ctx: HostContext): void {\n ctx.effect(() => ctx.webServer.register({\n kind: 'exact', path: '/dsh-vscode/codicons/codicon.ttf', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }\n try {\n const asset = codiconAsset()\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=31536000, immutable' })\n res.end(await readFile(asset.path))\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: Codicon font')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix', path: '/dsh-vscode/api', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) return send(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })\n if (req.method !== 'POST') return send(res, 405, { ok: false, error: { code: 'method', message: 'POST required' } })\n try {\n const controller = new AbortController()\n req.once('aborted', () => controller.abort())\n const path = new URL(req.url ?? '/', 'http://dsh.local').pathname\n const method = path.slice('/dsh-vscode/api/'.length)\n send(res, 200, { ok: true, value: await dispatch(ctx, method, await readBody(req), controller.signal) })\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: API')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'exact', path: '/dsh-vscode/upload', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) return send(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })\n if (req.method !== 'POST') return send(res, 405, { ok: false, error: { code: 'method', message: 'POST required' } })\n try {\n const url = new URL(req.url ?? '/', 'http://dsh.local')\n const sessionId = url.searchParams.get('sessionId'); const directory = url.searchParams.get('directory'); const name = url.searchParams.get('name')\n if (sessionId === null || directory === null || name === null) throw new WorkbenchError('bad-request', 'sessionId, directory and name are required')\n send(res, 201, { ok: true, value: await uploadFile(cwdFor(ctx, sessionId), directory, name, await readBinaryBody(req)) })\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: file upload')\n\n ctx.effect(() => ctx.webServer.register({\n kind:'prefix',path:'/dsh-vscode/file',handler:async(req,res)=>{if(!trusted(req,ctx.webRuntime.trustedHosts)){res.writeHead(403);res.end();return}try{const url=new URL(req.url??'/','http://dsh.local');const sessionId=url.searchParams.get('sessionId');const path=url.searchParams.get('path');if(sessionId===null||path===null)throw new WorkbenchError('bad-request','sessionId and path are required');const asset=await workspaceImage(cwdFor(ctx,sessionId),path);res.writeHead(200,{'content-type':asset.type,'x-content-type-options':'nosniff','cache-control':'no-store'});res.end(await readFile(asset.path))}catch(error){sendError(res,error)}}\n }), 'dsh-vscode-workbench: image preview')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix', path: '/dsh-vscode/monaco', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }\n try {\n const raw = decodeURIComponent(new URL(req.url ?? '/', 'http://dsh.local').pathname.slice('/dsh-vscode/monaco/'.length))\n const asset = monacoAsset(raw)\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=31536000, immutable' })\n res.end(await readFile(asset.path))\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: Monaco assets')\n\n const wss = new WebSocketServer({ noServer: true })\n ctx.effect(() => ctx.webServer.registerUpgrade({\n path: '/dsh-vscode/ws/files', handler(req, socket, head) {\n if (!trusted(req as IncomingMessage, ctx.webRuntime.trustedHosts)) { socket.destroy(); return }\n wss.handleUpgrade(req as IncomingMessage, socket as Duplex, head, (ws) => {\n const sessionId = new URL(req.url ?? '/', 'http://dsh.local').searchParams.get('sessionId')\n if (sessionId === null) { ws.close(1008, 'sessionId required'); return }\n const watcher = chokidar.watch(cwdFor(ctx, sessionId), { ignoreInitial: true, ignored: /(^|[/\\\\])(\\.git|node_modules)([/\\\\]|$)/, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 25 } })\n let paths = new Set<string>(); let timer: ReturnType<typeof setTimeout> | undefined\n watcher.on('all', (_event, path) => {\n paths.add(path); clearTimeout(timer); timer = setTimeout(() => {\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'files.changed', paths: [...paths] }))\n paths = new Set()\n }, 150)\n })\n ws.on('close', () => { clearTimeout(timer); void watcher.close() })\n })\n },\n }), 'dsh-vscode-workbench: file watcher')\n ctx.effect(() => () => { wss.close() }, 'dsh-vscode-workbench: watcher teardown')\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/wire.ts","../src/host.ts","../src/index.ts"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http'\n\nexport class WorkbenchError extends Error {\n constructor(public code: string, message: string, public status = 400) { super(message) }\n}\n\nexport async function readBody(req: IncomingMessage): Promise<Record<string, unknown>> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += value.length\n if (size > 2 * 1024 * 1024) throw new WorkbenchError('too-large', 'request body is too large', 413)\n chunks.push(value)\n }\n try {\n const value: unknown = JSON.parse(Buffer.concat(chunks).toString('utf8') || '{}')\n if (value === null || typeof value !== 'object' || Array.isArray(value)) throw new Error('object expected')\n return value as Record<string, unknown>\n } catch { throw new WorkbenchError('bad-json', 'request body must be a JSON object') }\n}\n\n/** Read a binary upload while enforcing a fixed request limit. */\nexport async function readBinaryBody(req: IncomingMessage, limit = 100 * 1024 * 1024): Promise<Buffer> {\n const chunks: Buffer[] = []\n let size = 0\n for await (const chunk of req) {\n const value = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)\n size += value.length\n if (size > limit) throw new WorkbenchError('too-large', 'uploaded file is too large', 413)\n chunks.push(value)\n }\n return Buffer.concat(chunks)\n}\n\nexport function stringField(body: Record<string, unknown>, key: string): string {\n const value = body[key]\n if (typeof value !== 'string') throw new WorkbenchError('bad-request', `${key} must be a string`)\n return value\n}\n\nexport function send(res: ServerResponse, status: number, value: unknown): void {\n res.writeHead(status, { 'content-type': 'application/json; charset=utf-8', 'cache-control': 'no-store' })\n res.end(JSON.stringify(value))\n}\n\nexport function sendError(res: ServerResponse, error: unknown): void {\n const known = error instanceof WorkbenchError ? error : new WorkbenchError('internal', error instanceof Error ? error.message : String(error), 500)\n send(res, known.status, { ok: false, error: { code: known.code, message: known.message } })\n}\n\nexport function trusted(req: IncomingMessage, allowed: readonly string[]): boolean {\n const host = req.headers.host\n if (host === undefined || req.headers['sec-fetch-site'] === 'cross-site') return false\n let hostUrl: URL\n try { hostUrl = new URL(`http://${host}`) } catch { return false }\n const loopback = hostUrl.hostname === 'localhost' || hostUrl.hostname === '[::1]' || /^127(?:\\.\\d{1,3}){3}$/.test(hostUrl.hostname)\n if (!loopback && !allowed.some(candidate => candidate === host || candidate === hostUrl.hostname)) return false\n const origin = req.headers.origin\n if (origin === undefined) return true\n try { return new URL(origin).host === hostUrl.host } catch { return false }\n}\n","import { createHash, randomUUID } from 'node:crypto'\nimport { spawn } from 'node:child_process'\nimport { createRequire } from 'node:module'\nimport { dirname, extname, isAbsolute, join, normalize, relative, resolve, sep } from 'node:path'\nimport { access, lstat, mkdir, open, readFile, readdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises'\nimport { constants } from 'node:fs'\nimport type { HostContext } from './context.ts'\nimport type { FileDocument, FileEntry, GitChange, GitCommit, GitCommitFile, GitDiff, GitStatus, SearchMatch, SearchRequest } from './types.ts'\nimport { WorkbenchError } from './wire.ts'\n\nconst require = createRequire(import.meta.url)\nconst TEXT_LIMIT = 5 * 1024 * 1024\nconst PROCESS_TIMEOUT = 30_000\n\nexport function cwdFor(ctx: HostContext, sessionId: string): string {\n const cwd = ctx.sessions.get(sessionId)?.header.cwd\n if (cwd === undefined || cwd === '') return process.cwd()\n if (!isAbsolute(cwd)) throw new WorkbenchError('cwd-invalid', 'session working directory is not absolute')\n return normalize(cwd)\n}\n\nfunction inside(root: string, target: string): boolean {\n const rel = relative(root, target)\n return rel === '' || (!rel.startsWith(`..${sep}`) && rel !== '..' && !isAbsolute(rel))\n}\n\nasync function canonicalRoot(cwd: string): Promise<string> {\n return realpath(cwd).catch(() => { throw new WorkbenchError('cwd-missing', 'session working directory does not exist', 404) })\n}\n\n/** Resolve a caller-relative path and reject traversal and symlink escape. */\nexport async function workspacePath(cwd: string, raw: string, create = false): Promise<{ root: string; absolute: string }> {\n if (isAbsolute(raw)) throw new WorkbenchError('path-invalid', 'absolute paths are not accepted')\n const root = await canonicalRoot(cwd)\n const absolute = resolve(root, raw === '' ? '.' : raw)\n if (!inside(root, absolute)) throw new WorkbenchError('path-outside', 'path is outside the workspace', 403)\n if (create) {\n const parent = await realpath(dirname(absolute)).catch(() => { throw new WorkbenchError('parent-missing', 'parent directory does not exist', 404) })\n if (!inside(root, parent)) throw new WorkbenchError('path-outside', 'path follows a symlink outside the workspace', 403)\n } else {\n const actual = await realpath(absolute).catch(() => { throw new WorkbenchError('not-found', 'path does not exist', 404) })\n if (!inside(root, actual)) throw new WorkbenchError('path-outside', 'path follows a symlink outside the workspace', 403)\n }\n return { root, absolute }\n}\n\nfunction relativePath(root: string, absolute: string): string { return relative(root, absolute).split(sep).join('/') }\nfunction revision(content: Buffer, mtimeMs: number): string {\n return createHash('sha256').update(content).update(String(mtimeMs)).digest('hex')\n}\n\nexport async function listFiles(cwd: string, path: string): Promise<FileEntry[]> {\n const { root, absolute } = await workspacePath(cwd, path)\n const values = await readdir(absolute, { withFileTypes: true })\n const rows = await Promise.all(values.filter(item => item.name !== '.git').map(async item => {\n const full = join(absolute, item.name)\n if (!item.isSymbolicLink()) return { name: item.name, path: relativePath(root, full), kind: item.isDirectory() ? 'directory' as const : 'file' as const }\n const actual = await realpath(full).catch(() => undefined)\n if (actual === undefined || !inside(root, actual)) return undefined\n const info = await stat(full)\n const kind = info.isDirectory() ? 'directory' as const : 'file' as const\n return { name: item.name, path: relativePath(root, full), kind, size: info.size, mtimeMs: info.mtimeMs }\n }))\n return rows.filter(row => row !== undefined).sort((a, b) => a.kind === b.kind ? a.name.localeCompare(b.name) : a.kind === 'directory' ? -1 : 1)\n}\n\nexport async function readDocument(cwd: string, path: string): Promise<FileDocument> {\n const { root, absolute } = await workspacePath(cwd, path)\n const info = await stat(absolute)\n if (!info.isFile()) throw new WorkbenchError('not-file', 'path is not a file')\n const tooLarge = info.size > TEXT_LIMIT\n const handle = await open(absolute, 'r')\n try {\n const head = Buffer.alloc(Math.min(info.size, tooLarge ? 4096 : TEXT_LIMIT))\n const { bytesRead } = await handle.read(head, 0, head.length, 0)\n const bytes = head.subarray(0, bytesRead)\n const binary = bytes.includes(0)\n return {\n path: relativePath(root, absolute), content: binary || tooLarge ? '' : bytes.toString('utf8'),\n revision: revision(bytes, info.mtimeMs), size: info.size, binary, tooLarge,\n }\n } finally { await handle.close() }\n}\n\nexport async function saveDocument(cwd: string, path: string, content: string, expected: string): Promise<FileDocument> {\n const { absolute } = await workspacePath(cwd, path)\n const current = await readDocument(cwd, path)\n if (current.revision !== expected) throw new WorkbenchError('revision-conflict', 'file changed on disk; reload before saving', 409)\n const temp = join(dirname(absolute), `.${randomUUID()}.dsh-vscode.tmp`)\n await writeFile(temp, content, { encoding: 'utf8', flag: 'wx' })\n try { await rename(temp, absolute) } catch (error) { await rm(temp, { force: true }); throw error }\n return readDocument(cwd, path)\n}\n\nexport async function createEntry(cwd: string, path: string, kind: 'file' | 'directory'): Promise<void> {\n const { absolute } = await workspacePath(cwd, path, true)\n if (kind === 'directory') await mkdir(absolute)\n else { const handle = await open(absolute, 'wx'); await handle.close() }\n}\n\n/** Write an uploaded file into an existing workspace directory without overwriting files. */\nexport async function uploadFile(cwd: string, directory: string, name: string, content: Buffer): Promise<FileEntry> {\n if (name === '' || name === '.' || name === '..' || name.includes('/') || name.includes('\\\\')) throw new WorkbenchError('filename-invalid', 'uploaded file name is invalid')\n const targetPath = directory === '' ? name : `${directory}/${name}`\n const { root, absolute } = await workspacePath(cwd, targetPath, true)\n const handle = await open(absolute, 'wx')\n try { await handle.writeFile(content) } finally { await handle.close() }\n const info = await lstat(absolute)\n return { name, path: relativePath(root, absolute), kind: 'file', size: info.size, mtimeMs: info.mtimeMs }\n}\n\nexport async function renameEntry(cwd: string, path: string, nextPath: string): Promise<void> {\n const source = await workspacePath(cwd, path)\n const target = await workspacePath(cwd, nextPath, true)\n await access(target.absolute, constants.F_OK).then(\n () => { throw new WorkbenchError('target-exists', 'destination already exists', 409) }, () => undefined,\n )\n await rename(source.absolute, target.absolute)\n}\n\nexport async function deleteEntry(cwd: string, path: string): Promise<void> {\n if (path === '') throw new WorkbenchError('root-delete', 'cannot delete workspace root', 403)\n const { absolute } = await workspacePath(cwd, path)\n await rm(absolute, { recursive: true, force: false })\n}\n\nconst IMAGE_TYPES:Record<string,string>={'.avif':'image/avif','.bmp':'image/bmp','.gif':'image/gif','.ico':'image/x-icon','.jpeg':'image/jpeg','.jpg':'image/jpeg','.png':'image/png','.svg':'image/svg+xml','.webp':'image/webp'}\n/** Resolve an image inside the workspace for protected browser preview. */\nexport async function workspaceImage(cwd:string,path:string):Promise<{path:string;type:string}>{const resolved=await workspacePath(cwd,path);const type=IMAGE_TYPES[extname(resolved.absolute).toLowerCase()];if(type===undefined)throw new WorkbenchError('unsupported-media','file is not a supported image',415);return {path:resolved.absolute,type}}\n\ninterface ProcessResult { stdout: string; stderr: string; code: number }\nexport function run(command: string, args: string[], cwd: string, timeout = PROCESS_TIMEOUT, signal?: AbortSignal): Promise<ProcessResult> {\n return new Promise((resolvePromise, reject) => {\n const child = spawn(command, args, { cwd, shell: false, windowsHide: true })\n let stdout = ''; let stderr = ''; let settled = false\n const timer = setTimeout(() => { child.kill(); if (!settled) reject(new WorkbenchError('timeout', `${command} timed out`, 504)) }, timeout)\n const abort = () => { child.kill(); if (!settled) { settled = true; clearTimeout(timer); reject(new WorkbenchError('cancelled', `${command} cancelled`, 499)) } }\n signal?.addEventListener('abort', abort, { once: true })\n child.stdout.on('data', chunk => { stdout += String(chunk) })\n child.stderr.on('data', chunk => { stderr += String(chunk) })\n child.on('error', error => { clearTimeout(timer); signal?.removeEventListener('abort', abort); if (!settled) { settled = true; reject(error) } })\n child.on('close', code => { clearTimeout(timer); signal?.removeEventListener('abort', abort); if (!settled) { settled = true; resolvePromise({ stdout, stderr, code: code ?? 1 }) } })\n })\n}\n\nexport async function searchWorkspace(cwd: string, request: SearchRequest, signal?: AbortSignal): Promise<{ matches: SearchMatch[]; limited: boolean }> {\n if (request.query === '') return { matches: [], limited: false }\n const root = await canonicalRoot(cwd)\n const rg = require('@vscode/ripgrep').rgPath as string\n const args = ['--json', '--line-number', '--column', '--hidden', '--glob', '!.git/**', '--max-count', '2000']\n if (!request.regex) args.push('--fixed-strings')\n if (!request.caseSensitive) args.push('--ignore-case')\n if (request.wholeWord) args.push('--word-regexp')\n if (request.include) args.push('--glob', request.include)\n if (request.exclude) args.push('--glob', `!${request.exclude}`)\n args.push('--', request.query, '.')\n const result = await run(rg, args, root, PROCESS_TIMEOUT, signal)\n if (result.code > 1) throw new WorkbenchError('search-failed', result.stderr || 'ripgrep failed', 500)\n const matches: SearchMatch[] = []\n for (const line of result.stdout.split('\\n')) {\n if (line === '') continue\n const event = JSON.parse(line) as { type: string; data?: { path?: { text?: string }; lines?: { text?: string }; line_number?: number; submatches?: Array<{ start: number; end: number; match?: { text?: string } }> } }\n if (event.type !== 'match' || event.data?.path?.text === undefined || event.data.lines?.text === undefined) continue\n for (const hit of event.data.submatches ?? []) {\n matches.push({ path: event.data.path.text.replace(/^\\.\\//, ''), line: event.data.line_number ?? 1, column: hit.start + 1, preview: event.data.lines.text.trimEnd(), match: hit.match?.text ?? '' })\n if (matches.length >= 2000) return { matches, limited: true }\n }\n }\n return { matches, limited: false }\n}\n\nasync function gitRoot(cwd: string): Promise<string> {\n const result = await run('git', ['rev-parse', '--show-toplevel'], cwd)\n if (result.code !== 0) throw new WorkbenchError('not-repository', 'current workspace is not inside a Git repository', 404)\n return result.stdout.trim()\n}\n\nfunction validateCommitHash(value?: string): string {\n if (!/^[0-9a-f]{7,64}$/i.test(value ?? '')) throw new WorkbenchError('bad-request', 'commit hash is invalid')\n return value!\n}\n\nfunction validateGitPath(repository: string, path?: string): string {\n if (path === undefined || path === '' || isAbsolute(path) || !inside(repository, resolve(repository, path))) throw new WorkbenchError('bad-request', 'repository-relative path is required')\n return path\n}\n\nasync function gitObject(repository: string, spec: string): Promise<string> {\n const result = await run('git', ['show', '--no-textconv', spec], repository)\n return result.code === 0 ? result.stdout : ''\n}\n\nasync function commitFiles(repository: string, hash: string): Promise<GitCommitFile[]> {\n const parent=await run('git',['rev-parse','--verify',`${hash}^1`],repository)\n const result = parent.code===0\n ? await run('git',['diff','--name-status','-z','-M',parent.stdout.trim(),hash,'--'],repository)\n : await run('git', ['diff-tree', '--root', '--no-commit-id', '--name-status', '-r', '-z', '-M', hash], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n const tokens = result.stdout.split('\\0').filter(Boolean); const files: GitCommitFile[] = []\n for (let index = 0; index < tokens.length;) {\n const status = tokens[index++] ?? ''; const code = status[0] ?? ''\n if (code === 'R' || code === 'C') { const previousPath=tokens[index++]??'';const path=tokens[index++]??'';files.push({path,previousPath,status:code}) }\n else { const path=tokens[index++]??'';files.push({path,status:code}) }\n }\n return files\n}\n\nexport async function gitStatus(cwd: string): Promise<GitStatus> {\n const repository = await gitRoot(cwd)\n const result = await run('git', ['status', '--porcelain=v2', '-z', '--branch', '--untracked-files=all'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr, 500)\n let branch = 'HEAD'; let ahead = 0; let behind = 0\n const changes: GitChange[] = []\n for (const record of result.stdout.split('\\0')) {\n if (record.startsWith('# branch.head ')) branch = record.slice(14)\n else if (record.startsWith('# branch.ab ')) {\n const match = /\\+(\\d+) -(\\d+)/.exec(record); ahead = Number(match?.[1] ?? 0); behind = Number(match?.[2] ?? 0)\n } else if (record.startsWith('? ')) changes.push({ path: record.slice(2), index: '?', worktree: '?', staged: false, untracked: true })\n else if (record.startsWith('1 ') || record.startsWith('2 ')) {\n const parts = record.split(' '); const xy = parts[1] ?? '..'; const path = parts.slice(record.startsWith('2 ') ? 9 : 8).join(' ')\n changes.push({ path, index: xy[0] ?? '.', worktree: xy[1] ?? '.', staged: xy[0] !== '.', untracked: false })\n }\n }\n return { repository, branch, ahead, behind, changes }\n}\n\nexport async function gitAction(cwd: string, action: string, path?: string, value?: string): Promise<unknown> {\n const repository = await gitRoot(cwd)\n let args: string[]\n switch (action) {\n case 'stage': args = ['add', '--', path ?? '']; break\n case 'stage.all': args = ['add', '-A']; break\n case 'unstage': args = ['restore', '--staged', '--', path ?? '']; break\n case 'unstage.all': args = ['restore', '--staged', '--', '.']; break\n case 'discard': {\n args = value === 'untracked' ? ['clean', '-f', '--', path ?? ''] : ['restore', '--worktree', '--', path ?? '']\n break\n }\n case 'discard.all': {\n const head = await run('git', ['rev-parse', '--verify', 'HEAD'], repository)\n if (head.code === 0) {\n const restore = await run('git', ['restore', '--source=HEAD', '--staged', '--worktree', '--', '.'], repository)\n if (restore.code !== 0) throw new WorkbenchError('git-failed', restore.stderr || restore.stdout, 409)\n }\n const clean = await run('git', ['clean', '-fd'], repository)\n if (clean.code !== 0) throw new WorkbenchError('git-failed', clean.stderr || clean.stdout, 409)\n return gitStatus(cwd)\n }\n case 'commit': args = ['commit', '-m', value ?? '']; break\n case 'branch.create': args = ['switch', '-c', value ?? '']; break\n case 'branch.switch': args = ['switch', value ?? '']; break\n case 'branches': {\n const result = await run('git', ['for-each-ref', '--format=%(refname:short)', 'refs/heads'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr, 500)\n return result.stdout.split('\\n').filter(Boolean)\n }\n case 'log': {\n const head = await run('git', ['rev-parse', '--verify', 'HEAD'], repository)\n if (head.code !== 0) return []\n const result = await run('git', ['log', '--topo-order', '--date=iso-strict', '--decorate=short', '--pretty=format:%H%x1f%P%x1f%an%x1f%aI%x1f%s%x1f%D%x00', '-n', '100'], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 500)\n return result.stdout.split('\\0').filter(Boolean).map((record): GitCommit => {\n const [rawHash = '', parents = '', author = '', date = '', subject = '', refs = ''] = record.split('\\x1f');const hash=rawHash.trim()\n return { hash, parents: parents.split(' ').filter(Boolean), author, date, subject, refs: refs.split(',').map(item => item.trim()).filter(Boolean) }\n })\n }\n case 'commit.show': {\n const hash = validateCommitHash(value)\n const result = await run('git', ['show', '--no-ext-diff', '--find-renames', '--format=fuller', '--stat', '--patch', '--no-color', hash], repository)\n if (result.code !== 0) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n return { diff: result.stdout, original: '', modified: result.stdout } satisfies GitDiff\n }\n case 'commit.files': return commitFiles(repository, validateCommitHash(value))\n case 'commit.file.diff': {\n const hash=validateCommitHash(value);const target=validateGitPath(repository,path);const files=await commitFiles(repository,hash);const file=files.find(item=>item.path===target)\n if (file===undefined) throw new WorkbenchError('not-found','file is not part of the commit',404)\n const parent=await run('git',['rev-parse','--verify',`${hash}^1`],repository)\n const shown=parent.code===0\n ? await run('git',['diff','--no-ext-diff','--find-renames','--patch','--no-color',parent.stdout.trim(),hash,'--',file.previousPath??target,target],repository)\n : await run('git',['show','--no-ext-diff','--find-renames','--format=','--patch','--no-color',hash,'--',file.previousPath??target,target],repository)\n if(shown.code!==0)throw new WorkbenchError('git-failed',shown.stderr||shown.stdout,409)\n const original=file.status==='A'||parent.code!==0?'':await gitObject(repository,`${parent.stdout.trim()}:${file.previousPath??target}`)\n const modified=file.status==='D'?'':await gitObject(repository,`${hash}:${target}`)\n return {diff:shown.stdout,original,modified} satisfies GitDiff\n }\n case 'diff': {\n const untracked = value === 'untracked'\n const staged = value === 'staged'\n args = untracked\n ? ['diff', '--no-index', '--', process.platform === 'win32' ? 'NUL' : '/dev/null', path ?? '']\n : ['diff', ...(staged ? ['--cached'] : []), '--', path ?? '']\n break\n }\n default: throw new WorkbenchError('bad-action', `unknown Git action ${action}`)\n }\n if (args.some(arg => arg === '') && action !== 'commit') throw new WorkbenchError('bad-request', 'path or value is required')\n const result = await run('git', args, repository)\n const expectedUntrackedDifference = action === 'diff' && value === 'untracked' && result.code === 1\n if (result.code !== 0 && !expectedUntrackedDifference) throw new WorkbenchError('git-failed', result.stderr || result.stdout, 409)\n if(action!=='diff')return gitStatus(cwd)\n const target=validateGitPath(repository,path)\n const original=value==='untracked'?'':await gitObject(repository,`${value==='staged'?'HEAD:':':'}${target}`)\n const modified=value==='staged'?await gitObject(repository,`:${target}`):await readFile(resolve(repository,target),'utf8').catch(()=> '')\n return {diff:result.stdout,original,modified} satisfies GitDiff\n}\n\n/** Resolve the official VS Code Codicon font shipped by @vscode/codicons. */\nexport function codiconAsset(): { path: string; type: string } {\n const packageRoot = dirname(require.resolve('@vscode/codicons/package.json'))\n return { path: resolve(packageRoot, 'dist', 'codicon.ttf'), type: 'font/ttf' }\n}\n\nexport function monacoAsset(raw: string): { path: string; type: string } {\n const packageRoot = dirname(require.resolve('monaco-editor/package.json'))\n const path = resolve(packageRoot, 'min', raw)\n const root = resolve(packageRoot, 'min')\n if (!inside(root, path)) throw new WorkbenchError('asset-invalid', 'invalid Monaco asset path', 403)\n const type = extname(path) === '.css' ? 'text/css' : extname(path) === '.js' ? 'text/javascript' : extname(path) === '.ttf' ? 'font/ttf' : 'application/octet-stream'\n return { path, type }\n}\n","/** DSH host plugin for the VS Code workbench. */\nimport { readFile } from 'node:fs/promises'\nimport type { IncomingMessage } from 'node:http'\nimport { isAbsolute, relative, sep } from 'node:path'\nimport type { Duplex } from 'node:stream'\nimport chokidar from 'chokidar'\nimport { WebSocket, WebSocketServer } from 'ws'\nimport type { HostContext } from './context.ts'\nimport { codiconAsset, createEntry, cwdFor, deleteEntry, gitAction, gitStatus, listFiles, monacoAsset, readDocument, renameEntry, saveDocument, searchWorkspace, uploadFile, workspaceImage } from './host.ts'\nimport type { SearchRequest } from './types.ts'\nimport { readBinaryBody, readBody, send, sendError, stringField, trusted, WorkbenchError } from './wire.ts'\n\nexport { WorkbenchError } from './wire.ts'\nexport * from './types.ts'\nexport const name = 'dsh-vscode-workbench'\nexport const inject = ['webServer', 'sessions', 'webRuntime']\n\nasync function dispatch(ctx: HostContext, method: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<unknown> {\n const sessionId = stringField(body, 'sessionId'); const cwd = cwdFor(ctx, sessionId)\n switch (method) {\n case 'workspace': return { cwd, name: cwd.split(/[\\\\/]/).at(-1) || cwd }\n case 'fs.list': return listFiles(cwd, stringField(body, 'path'))\n case 'fs.read': return readDocument(cwd, stringField(body, 'path'))\n case 'fs.save': return saveDocument(cwd, stringField(body, 'path'), stringField(body, 'content'), stringField(body, 'revision'))\n case 'fs.create': return createEntry(cwd, stringField(body, 'path'), body.kind === 'directory' ? 'directory' : 'file').then(() => ({ ok: true }))\n case 'fs.rename': return renameEntry(cwd, stringField(body, 'path'), stringField(body, 'nextPath')).then(() => ({ ok: true }))\n case 'fs.delete': return deleteEntry(cwd, stringField(body, 'path')).then(() => ({ ok: true }))\n case 'search': return searchWorkspace(cwd, body as unknown as SearchRequest, signal)\n case 'git.status': return gitStatus(cwd)\n case 'git.action': return gitAction(cwd, stringField(body, 'action'), typeof body.path === 'string' ? body.path : undefined, typeof body.value === 'string' ? body.value : undefined)\n default: throw new WorkbenchError('not-found', `unknown workbench API method ${method}`, 404)\n }\n}\n\nexport function apply(ctx: HostContext): void {\n ctx.effect(() => ctx.webServer.register({\n kind: 'exact', path: '/dsh-vscode/codicons/codicon.ttf', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }\n try {\n const asset = codiconAsset()\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=31536000, immutable' })\n res.end(await readFile(asset.path))\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: Codicon font')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix', path: '/dsh-vscode/api', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) return send(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })\n if (req.method !== 'POST') return send(res, 405, { ok: false, error: { code: 'method', message: 'POST required' } })\n try {\n const controller = new AbortController()\n req.once('aborted', () => controller.abort())\n const path = new URL(req.url ?? '/', 'http://dsh.local').pathname\n const method = path.slice('/dsh-vscode/api/'.length)\n send(res, 200, { ok: true, value: await dispatch(ctx, method, await readBody(req), controller.signal) })\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: API')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'exact', path: '/dsh-vscode/upload', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) return send(res, 403, { ok: false, error: { code: 'forbidden', message: 'forbidden' } })\n if (req.method !== 'POST') return send(res, 405, { ok: false, error: { code: 'method', message: 'POST required' } })\n try {\n const url = new URL(req.url ?? '/', 'http://dsh.local')\n const sessionId = url.searchParams.get('sessionId'); const directory = url.searchParams.get('directory'); const name = url.searchParams.get('name')\n if (sessionId === null || directory === null || name === null) throw new WorkbenchError('bad-request', 'sessionId, directory and name are required')\n send(res, 201, { ok: true, value: await uploadFile(cwdFor(ctx, sessionId), directory, name, await readBinaryBody(req)) })\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: file upload')\n\n ctx.effect(() => ctx.webServer.register({\n kind:'prefix',path:'/dsh-vscode/file',handler:async(req,res)=>{if(!trusted(req,ctx.webRuntime.trustedHosts)){res.writeHead(403);res.end();return}try{const url=new URL(req.url??'/','http://dsh.local');const sessionId=url.searchParams.get('sessionId');const path=url.searchParams.get('path');if(sessionId===null||path===null)throw new WorkbenchError('bad-request','sessionId and path are required');const asset=await workspaceImage(cwdFor(ctx,sessionId),path);res.writeHead(200,{'content-type':asset.type,'x-content-type-options':'nosniff','cache-control':'no-store'});res.end(await readFile(asset.path))}catch(error){sendError(res,error)}}\n }), 'dsh-vscode-workbench: image preview')\n\n ctx.effect(() => ctx.webServer.register({\n kind: 'prefix', path: '/dsh-vscode/monaco', handler: async (req, res) => {\n if (!trusted(req, ctx.webRuntime.trustedHosts)) { res.writeHead(403); res.end(); return }\n try {\n const raw = decodeURIComponent(new URL(req.url ?? '/', 'http://dsh.local').pathname.slice('/dsh-vscode/monaco/'.length))\n const asset = monacoAsset(raw)\n res.writeHead(200, { 'content-type': asset.type, 'cache-control': 'public, max-age=31536000, immutable' })\n res.end(await readFile(asset.path))\n } catch (error) { sendError(res, error) }\n },\n }), 'dsh-vscode-workbench: Monaco assets')\n\n const wss = new WebSocketServer({ noServer: true })\n ctx.effect(() => ctx.webServer.registerUpgrade({\n path: '/dsh-vscode/ws/files', handler(req, socket, head) {\n if (!trusted(req as IncomingMessage, ctx.webRuntime.trustedHosts)) { socket.destroy(); return }\n wss.handleUpgrade(req as IncomingMessage, socket as Duplex, head, (ws) => {\n const sessionId = new URL(req.url ?? '/', 'http://dsh.local').searchParams.get('sessionId')\n if (sessionId === null) { ws.close(1008, 'sessionId required'); return }\n const workspace = cwdFor(ctx, sessionId)\n const watcher = chokidar.watch(workspace, { ignoreInitial: true, ignored: /(^|[/\\\\])(\\.git|node_modules|\\.next|\\.nuxt|\\.turbo|\\.vite|coverage)([/\\\\]|$)/, awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 25 } })\n let paths = new Set<string>(); let timer: ReturnType<typeof setTimeout> | undefined\n watcher.on('all', (_event, path) => {\n const changed = isAbsolute(path) ? relative(workspace, path) : path\n paths.add(changed.split(sep).join('/')); clearTimeout(timer); timer = setTimeout(() => {\n if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'files.changed', paths: [...paths] }))\n paths = new Set()\n }, 150)\n })\n ws.on('close', () => { clearTimeout(timer); void watcher.close() })\n })\n },\n }), 'dsh-vscode-workbench: file watcher')\n ctx.effect(() => () => { wss.close() }, 'dsh-vscode-workbench: watcher teardown')\n}\n"],"mappings":"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"}
|
|
@@ -3,6 +3,7 @@ import type { VscodeWorkbench } from './service.ts';
|
|
|
3
3
|
interface Tab extends FileDocument {
|
|
4
4
|
dirty: boolean;
|
|
5
5
|
draft: string;
|
|
6
|
+
loaded?: boolean;
|
|
6
7
|
line?: number;
|
|
7
8
|
column?: number;
|
|
8
9
|
external?: boolean;
|
|
@@ -13,10 +14,16 @@ interface Tab extends FileDocument {
|
|
|
13
14
|
diffSourcePath?: string;
|
|
14
15
|
imageUrl?: string;
|
|
15
16
|
}
|
|
17
|
+
/** Restore tab headers without reading their file contents. */
|
|
18
|
+
export declare function restoreTabPaths(paths: string[]): Tab[];
|
|
19
|
+
/** Return whether file changes require reloading one visible directory. */
|
|
20
|
+
export declare function directoryAffected(directory: string, changedPaths: string[]): boolean;
|
|
16
21
|
/** Return the basename shown in a tab while preserving its full internal path. */
|
|
17
22
|
export declare function tabLabel(tab: Pick<Tab, 'path' | 'title' | 'diffSourcePath'>): string;
|
|
18
23
|
/** Return the workspace-relative directory used by the file picker. */
|
|
19
24
|
export declare function uploadDestination(selectedDirectory?: string): string;
|
|
25
|
+
/** Join an inline-created entry name to the currently selected Explorer directory. */
|
|
26
|
+
export declare function createdEntryPath(directory: string, name: string): string;
|
|
20
27
|
/** Replace the final path component while preserving its workspace-relative directory. */
|
|
21
28
|
export declare function renamedPath(path: string, name: string): string;
|
|
22
29
|
export interface GraphTrack {
|
package/lib/types/types.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ export interface FileEntry {
|
|
|
3
3
|
name: string;
|
|
4
4
|
path: string;
|
|
5
5
|
kind: 'file' | 'directory';
|
|
6
|
-
size
|
|
7
|
-
mtimeMs
|
|
6
|
+
size?: number;
|
|
7
|
+
mtimeMs?: number;
|
|
8
8
|
}
|
|
9
9
|
/** A text file together with its optimistic-concurrency revision. */
|
|
10
10
|
export interface FileDocument {
|
package/package.json
CHANGED
package/src/client/Workbench.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
|
2
2
|
import type { CSSProperties, DragEvent, ReactNode } from 'react'
|
|
3
3
|
import type { FileDocument, FileEntry, GitChange, GitCommit, GitCommitFile, GitDiff, GitStatus, SearchMatch } from '../types.ts'
|
|
4
|
-
import { api } from './api.ts'
|
|
4
|
+
import { ApiError, api } from './api.ts'
|
|
5
5
|
import type { VscodeWorkbench, WorkbenchView } from './service.ts'
|
|
6
6
|
import { fileIcon, Icon } from './Icon.tsx'
|
|
7
7
|
import { MarkdownPreview, markdownHeadings } from './MarkdownPreview.tsx'
|
|
@@ -11,9 +11,18 @@ import { DshConversationPanel, DshConversationTheme } from './DshConversationPan
|
|
|
11
11
|
import { DshMark } from './DshMark.tsx'
|
|
12
12
|
import { resetWorkspaceDocuments, WorkspaceRequestScope } from './workspaceState.ts'
|
|
13
13
|
|
|
14
|
-
interface Tab extends FileDocument { dirty: boolean; draft: string; line?: number; column?: number; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string }
|
|
14
|
+
interface Tab extends FileDocument { dirty: boolean; draft: string; loaded?: boolean; line?: number; column?: number; external?: boolean; preview?: boolean; title?: string; diff?: GitDiff; diffMode?: 'inline' | 'split'; diffSourcePath?:string; imageUrl?:string }
|
|
15
15
|
type WorkbenchTheme = 'dsh' | 'dark' | 'light'
|
|
16
16
|
interface TreeProps { sessionId: string; path: string; depth: number; expanded: Set<string>; selected?: string; selectedDirectory: string; statuses: Map<string, string>; onToggle(path: string): void; onSelectDirectory(path: string): void; onOpen(path: string): void; onChange(): void; onUpload(directory: string, files: FileList): void }
|
|
17
|
+
interface CreatingEntry { kind: 'file' | 'directory'; directory: string; value: string; error?: string }
|
|
18
|
+
|
|
19
|
+
function unloadedTab(path:string):Tab{return {path,content:'',draft:'',revision:'',size:0,binary:false,tooLarge:false,dirty:false,loaded:false,preview:/\.md$/i.test(path)}}
|
|
20
|
+
|
|
21
|
+
/** Restore tab headers without reading their file contents. */
|
|
22
|
+
export function restoreTabPaths(paths:string[]):Tab[]{return [...new Set(paths)].map(unloadedTab)}
|
|
23
|
+
|
|
24
|
+
/** Return whether file changes require reloading one visible directory. */
|
|
25
|
+
export function directoryAffected(directory:string,changedPaths:string[]):boolean{return changedPaths.some(changed=>{const normalized=changed.replace(/\\/g,'/');const parent=normalized.includes('/')?normalized.slice(0,normalized.lastIndexOf('/')):'';return normalized===directory||parent===directory})}
|
|
17
26
|
|
|
18
27
|
/** Return the basename shown in a tab while preserving its full internal path. */
|
|
19
28
|
export function tabLabel(tab:Pick<Tab,'path'|'title'|'diffSourcePath'>):string{const path=tab.title??tab.diffSourcePath??tab.path.replace(/^diff:/,'');return path.split(/[\\/]/).at(-1)??path}
|
|
@@ -33,28 +42,28 @@ function useDshDarkTheme(): boolean {
|
|
|
33
42
|
return dark
|
|
34
43
|
}
|
|
35
44
|
|
|
36
|
-
function Tree({ sessionId, path, depth, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload }: TreeProps) {
|
|
45
|
+
function Tree({ sessionId, path, depth, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload, creating, onCreatingChange, onCreatingCancel, onCreatingSubmit }: TreeProps & { creating?: CreatingEntry; onCreatingChange(value: string): void; onCreatingCancel(): void; onCreatingSubmit(): void }) {
|
|
37
46
|
const [rows, setRows] = useState<FileEntry[]>([]); const [error, setError] = useState<string>(); const [dropTarget, setDropTarget] = useState(false); const [context, setContext] = useState<{ entry: FileEntry; x: number; y: number }>(); const [renaming, setRenaming] = useState<{ path: string; value: string }>(); const [pendingDelete, setPendingDelete] = useState<FileEntry>()
|
|
38
47
|
const load = useCallback(() => { void api<FileEntry[]>('fs.list', { sessionId, path }).then(setRows).catch(error => setError(String(error))) }, [sessionId, path])
|
|
39
48
|
useEffect(load, [load])
|
|
40
49
|
useEffect(() => {
|
|
41
|
-
const handler = () => load(); window.addEventListener('dvw-files-changed', handler); return () => window.removeEventListener('dvw-files-changed', handler)
|
|
50
|
+
const handler = (event:Event) => { const paths=(event as CustomEvent<string[]>).detail; if(paths===undefined||directoryAffected(path,paths))load() }; window.addEventListener('dvw-files-changed', handler); return () => window.removeEventListener('dvw-files-changed', handler)
|
|
42
51
|
}, [load])
|
|
43
52
|
useEffect(() => { if (context === undefined) return; const close = () => setContext(undefined); const escape = (event: KeyboardEvent) => { if (event.key === 'Escape') close() }; window.addEventListener('pointerdown', close); window.addEventListener('keydown', escape); return () => { window.removeEventListener('pointerdown', close); window.removeEventListener('keydown', escape) } }, [context])
|
|
44
53
|
useEffect(() => { if (pendingDelete === undefined) return; const escape = (event: KeyboardEvent) => { if (event.key === 'Escape') setPendingDelete(undefined) }; window.addEventListener('keydown', escape); return () => window.removeEventListener('keydown', escape) }, [pendingDelete])
|
|
45
54
|
const rename = async (entry: FileEntry) => { const value = renaming?.path === entry.path ? renaming.value.trim() : ''; setRenaming(undefined); if (value === '' || value === entry.name) return; try { await api('fs.rename', { sessionId, path: entry.path, nextPath: renamedPath(entry.path, value) }); load(); onChange() } catch (error) { alert(error instanceof Error ? error.message : String(error)) } }
|
|
46
55
|
const remove = async () => { if (pendingDelete === undefined) return; const entry = pendingDelete; setPendingDelete(undefined); try { await api('fs.delete', { sessionId, path: entry.path }); load(); onChange() } catch (error) { alert(error instanceof Error ? error.message : String(error)) } }
|
|
47
56
|
const drop = (event: DragEvent, directory: string) => { event.preventDefault(); event.stopPropagation(); setDropTarget(false); if (event.dataTransfer.files.length > 0) onUpload(directory, event.dataTransfer.files) }
|
|
48
|
-
return <>{error && <div className="dvw-error">{error}</div>}{rows.map(row => <div key={row.path}>
|
|
57
|
+
return <>{error && <div className="dvw-error">{error}</div>}{creating?.directory === path && <><div className="dvw-tree-row dvw-tree-create-row" style={{ paddingLeft: 6 + depth * 13 }}><span className="dvw-chevron"/><Icon name={creating.kind === 'directory' ? 'folder' : 'new-file'} className={`dvw-file-icon ${creating.kind === 'directory' ? 'folder' : ''}`}/><input className="dvw-tree-create-input" value={creating.value} autoFocus aria-label={creating.kind === 'directory' ? '新建文件夹名称' : '新建文件名称'} aria-invalid={creating.error !== undefined} placeholder={creating.kind === 'directory' ? '文件夹名称' : '文件名称'} onChange={event => onCreatingChange(event.target.value)} onKeyDown={event => { if (event.key === 'Enter') { event.preventDefault(); onCreatingSubmit() } if (event.key === 'Escape') { event.preventDefault(); onCreatingCancel() } }} onBlur={onCreatingCancel}/></div>{creating.error && <div className="dvw-tree-create-error" style={{ paddingLeft: 42 + depth * 13 }}>{creating.error}</div>}</>}{rows.map(row => <div key={row.path}>
|
|
49
58
|
<div className="dvw-tree-row" data-dvw-upload-directory={row.kind === 'directory' ? row.path : undefined} data-selected={selected === row.path || selectedDirectory === row.path} data-drop-target={dropTarget && row.kind === 'directory'} style={{ paddingLeft: 6 + depth * 13 }} onClick={() => row.kind === 'directory' ? (onSelectDirectory(row.path), onToggle(row.path)) : onOpen(row.path)} onContextMenu={(event) => { event.preventDefault(); if (row.kind === 'directory') onSelectDirectory(row.path); setContext({ entry: row, x: event.clientX, y: event.clientY }) }} onDragEnter={event => { if (row.kind === 'directory' && event.dataTransfer.types.includes('Files')) { event.preventDefault(); event.stopPropagation(); setDropTarget(true) } }} onDragOver={event => { if (row.kind === 'directory' && event.dataTransfer.types.includes('Files')) { event.preventDefault(); event.stopPropagation(); setDropTarget(true) } }} onDragLeave={() => setDropTarget(false)} onDrop={event => row.kind === 'directory' && drop(event, row.path)}>
|
|
50
59
|
<span className="dvw-chevron">{row.kind === 'directory' && <Icon name={expanded.has(row.path) ? 'chevron-down' : 'chevron-right'}/>}</span><Icon name={row.kind === 'directory' ? expanded.has(row.path) ? 'folder-opened' : 'folder' : fileIcon(row.name)} className={`dvw-file-icon ${row.kind === 'directory' ? 'folder' : ''}`}/>{renaming?.path === row.path ? <input className="dvw-tree-rename" value={renaming.value} autoFocus onClick={event => event.stopPropagation()} onChange={event => setRenaming({ path: row.path, value: event.target.value })} onKeyDown={event => { if (event.key === 'Enter') void rename(row); if (event.key === 'Escape') setRenaming(undefined) }} onBlur={() => setRenaming(undefined)}/> : <span className="dvw-tree-name">{row.name}</span>}{row.kind === 'file' && statuses.has(row.path) && <span className="dvw-tree-status" data-status={statuses.get(row.path)}>{statuses.get(row.path)}</span>}
|
|
51
60
|
</div>
|
|
52
|
-
{row.kind === 'directory' && expanded.has(row.path) && <Tree {...{ sessionId, path: row.path, depth: depth + 1, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload }} />}
|
|
61
|
+
{row.kind === 'directory' && expanded.has(row.path) && <Tree {...{ sessionId, path: row.path, depth: depth + 1, expanded, selected, selectedDirectory, statuses, onToggle, onSelectDirectory, onOpen, onChange, onUpload, creating, onCreatingChange, onCreatingCancel, onCreatingSubmit }} />}
|
|
53
62
|
</div>)}{context && <div className="dvw-context-menu" role="menu" style={{ left: context.x, top: context.y }} onPointerDown={event => event.stopPropagation()}><button role="menuitem" onClick={() => { setRenaming({ path: context.entry.path, value: context.entry.name }); setContext(undefined) }}><Icon name="edit"/>重命名</button><button className="dvw-context-delete" role="menuitem" onClick={() => { setPendingDelete(context.entry); setContext(undefined) }}><Icon name="trash"/>删除</button></div>}{pendingDelete && <div className="dvw-confirm-backdrop" role="presentation"><section className="dvw-confirm" role="alertdialog" aria-modal="true" aria-labelledby="dvw-delete-title" aria-describedby="dvw-delete-message"><h2 id="dvw-delete-title">删除 {pendingDelete.kind === 'directory' ? '文件夹' : '文件'}</h2><p id="dvw-delete-message">是否删除“{pendingDelete.name}”?</p><p className="dvw-confirm-note">此操作无法撤销。</p><footer><button autoFocus onClick={() => setPendingDelete(undefined)}>取消</button><button className="dvw-danger-button" onClick={() => void remove()}>删除</button></footer></section></div>}</>
|
|
54
63
|
}
|
|
55
64
|
|
|
56
65
|
function Explorer(props: { sessionId: string; cwd?: string; selected?: string; onOpen(path: string): void }) {
|
|
57
|
-
const [expanded, setExpanded] = useState<Set<string>>(new Set()); const [revision, setRevision] = useState(0); const [statuses, setStatuses] = useState<Map<string, string>>(new Map()); const [dropTarget, setDropTarget] = useState(false); const [selectedDirectory, setSelectedDirectory] = useState(''); const picker = useRef<HTMLInputElement>(null)
|
|
66
|
+
const [expanded, setExpanded] = useState<Set<string>>(new Set()); const [revision, setRevision] = useState(0); const [statuses, setStatuses] = useState<Map<string, string>>(new Map()); const [dropTarget, setDropTarget] = useState(false); const [selectedDirectory, setSelectedDirectory] = useState(''); const [creating, setCreating] = useState<CreatingEntry>(); const picker = useRef<HTMLInputElement>(null)
|
|
58
67
|
useEffect(() => { if (props.cwd) { try { setExpanded(new Set(JSON.parse(localStorage.getItem(`dvw:tree:${props.cwd}`) ?? '[]') as string[])) } catch { setExpanded(new Set()) } } }, [props.cwd])
|
|
59
68
|
useEffect(() => { if (props.cwd) localStorage.setItem(`dvw:tree:${props.cwd}`, JSON.stringify([...expanded])) }, [props.cwd, expanded])
|
|
60
69
|
const refreshStatus = useCallback(() => {
|
|
@@ -64,7 +73,8 @@ function Explorer(props: { sessionId: string; cwd?: string; selected?: string; o
|
|
|
64
73
|
}, [props.sessionId])
|
|
65
74
|
const refreshStatusDebounced = useDebouncedCallback(refreshStatus, 500)
|
|
66
75
|
useEffect(() => { refreshStatus(); const handler = () => refreshStatusDebounced(); window.addEventListener('dvw-files-changed', handler); return () => window.removeEventListener('dvw-files-changed', handler) }, [refreshStatus, refreshStatusDebounced])
|
|
67
|
-
const
|
|
76
|
+
const startCreating = (kind: 'file' | 'directory') => { const directory = selectedDirectory; if (directory) setExpanded(current => new Set(current).add(directory)); setCreating({ kind, directory, value: '' }) }
|
|
77
|
+
const submitCreating = async () => { if (creating === undefined) return; const value = creating.value.trim(); if (value === '') { setCreating(current => current && ({ ...current, error: '请输入名称' })); return } const path = createdEntryPath(creating.directory, value); try { await api('fs.create', { sessionId: props.sessionId, path, kind: creating.kind }); const created = path; setCreating(undefined); setSelectedDirectory(creating.kind === 'directory' ? created : creating.directory); setRevision(x => x + 1); window.dispatchEvent(new Event('dvw-files-changed')) } catch (error) { setCreating(current => current && ({ ...current, error: error instanceof Error ? error.message : String(error) })) } }
|
|
68
78
|
const upload = useCallback(async (directory: string, files: FileList) => { try { await Promise.all([...files].map(async file => { const query = new URLSearchParams({ sessionId: props.sessionId, directory, name: file.name }); const response = await fetch(`/dsh-vscode/upload?${query}`, { method: 'POST', headers: { 'content-type': file.type || 'application/octet-stream' }, body: file }); const answer = await response.json() as { ok: boolean; error?: { message?: string } }; if (!response.ok || !answer.ok) throw new Error(answer.error?.message ?? 'upload failed') })); setRevision(value => value + 1); window.dispatchEvent(new Event('dvw-files-changed')) } catch (error) { alert(error instanceof Error ? error.message : String(error)) } }, [props.sessionId])
|
|
69
79
|
useEffect(() => {
|
|
70
80
|
let activeTarget: HTMLElement | undefined
|
|
@@ -96,12 +106,15 @@ function Explorer(props: { sessionId: string; cwd?: string; selected?: string; o
|
|
|
96
106
|
const drop = (event: DragEvent) => { event.preventDefault(); setDropTarget(false); if (event.dataTransfer.files.length > 0) void upload('', event.dataTransfer.files) }
|
|
97
107
|
const destination = uploadDestination(selectedDirectory)
|
|
98
108
|
const treeDrag = (event: DragEvent) => { if (event.dataTransfer.types.includes('Files')) { event.preventDefault(); event.stopPropagation(); setDropTarget(true) } }
|
|
99
|
-
return <><div className="dvw-side-head">资源管理器<div className="dvw-toolbar"><input ref={picker} className="dvw-file-picker" type="file" accept="*/*" multiple onChange={event => { if (event.currentTarget.files) void upload(destination, event.currentTarget.files); event.currentTarget.value = '' }}/><button className="dvw-icon" title={`上传文件到 ${destination || '工作区根目录'}`} onClick={() => picker.current?.click()}><Icon name="cloud-upload"/></button><button className="dvw-icon" title="新建文件" onClick={() =>
|
|
109
|
+
return <><div className="dvw-side-head">资源管理器<div className="dvw-toolbar"><input ref={picker} className="dvw-file-picker" type="file" accept="*/*" multiple onChange={event => { if (event.currentTarget.files) void upload(destination, event.currentTarget.files); event.currentTarget.value = '' }}/><button className="dvw-icon" title={`上传文件到 ${destination || '工作区根目录'}`} onClick={() => picker.current?.click()}><Icon name="cloud-upload"/></button><button className="dvw-icon" title="新建文件" onClick={() => startCreating('file')}><Icon name="new-file"/></button><button className="dvw-icon" title="新建文件夹" onClick={() => startCreating('directory')}><Icon name="new-folder"/></button><button className="dvw-icon" title="刷新" onClick={() => { setRevision(x => x + 1); window.dispatchEvent(new Event('dvw-files-changed')) }}><Icon name="refresh"/></button></div></div><div className="dvw-tree" data-dvw-upload-directory="" data-drop-target={dropTarget} key={revision} onDragEnter={treeDrag} onDragOver={treeDrag} onDragLeave={() => setDropTarget(false)} onDrop={drop}><Tree sessionId={props.sessionId} path="" depth={0} expanded={expanded} selected={props.selected} selectedDirectory={selectedDirectory} statuses={statuses} onToggle={path => setExpanded(current => { const next = new Set(current); next.has(path) ? next.delete(path) : next.add(path); return next })} onSelectDirectory={setSelectedDirectory} onOpen={props.onOpen} onChange={() => setRevision(x => x + 1)} onUpload={(directory, files) => void upload(directory, files)} creating={creating} onCreatingChange={value => setCreating(current => current && ({ ...current, value, error: undefined }))} onCreatingCancel={() => setCreating(undefined)} onCreatingSubmit={() => void submitCreating()} /></div></>
|
|
100
110
|
}
|
|
101
111
|
|
|
102
112
|
/** Return the workspace-relative directory used by the file picker. */
|
|
103
113
|
export function uploadDestination(selectedDirectory?: string): string { return selectedDirectory ?? '' }
|
|
104
114
|
|
|
115
|
+
/** Join an inline-created entry name to the currently selected Explorer directory. */
|
|
116
|
+
export function createdEntryPath(directory: string, name: string): string { return directory ? `${directory}/${name}` : name }
|
|
117
|
+
|
|
105
118
|
/** Replace the final path component while preserving its workspace-relative directory. */
|
|
106
119
|
export function renamedPath(path: string, name: string): string { return [...path.split('/').slice(0, -1), name].filter(Boolean).join('/') }
|
|
107
120
|
|
|
@@ -195,7 +208,7 @@ function GitGroupHeader({ title, count, expanded, onToggle, children }: { title:
|
|
|
195
208
|
|
|
196
209
|
function Git(props: { sessionId: string; onDiff(key:string, diff:GitDiff, title?:string, sourcePath?:string): void }) {
|
|
197
210
|
const [status, setStatus] = useState<GitStatus>(); const [commits,setCommits]=useState<GitCommit[]>([]); const [message, setMessage] = useState(''); const [error, setError] = useState<string>(); const [busy,setBusy]=useState(false); const [expanded,setExpanded]=useState({staged:true,changes:true,graph:true});const [expandedCommit,setExpandedCommit]=useState<string>();const [commitFileMap,setCommitFileMap]=useState<Map<string,GitCommitFile[]>>(new Map())
|
|
198
|
-
const refresh = useCallback(() => { void Promise.all([api<GitStatus>('git.status',{sessionId:props.sessionId}),api<GitCommit[]>('git.action',{sessionId:props.sessionId,action:'log'})]).then(([nextStatus,nextCommits])=>{setStatus(nextStatus);setCommits(nextCommits);setError(undefined)}).catch(error=>setError(String(error))) }, [props.sessionId])
|
|
211
|
+
const refresh = useCallback(() => { void Promise.all([api<GitStatus>('git.status',{sessionId:props.sessionId}),api<GitCommit[]>('git.action',{sessionId:props.sessionId,action:'log'})]).then(([nextStatus,nextCommits])=>{setStatus(nextStatus);setCommits(nextCommits);setError(undefined)}).catch(error=>{setStatus(undefined);setCommits([]);setError(error instanceof ApiError&&error.code==='not-repository'?undefined:String(error))}) }, [props.sessionId])
|
|
199
212
|
const refreshDebounced = useDebouncedCallback(refresh, 500)
|
|
200
213
|
useEffect(refresh,[refresh]); useEffect(()=>{const fn=()=>refreshDebounced();window.addEventListener('dvw-files-changed',fn);return()=>window.removeEventListener('dvw-files-changed',fn)},[refreshDebounced])
|
|
201
214
|
const action = async (name: string, path?: string, value?: string) => { setBusy(true);try { const result = await api<GitStatus | GitDiff>('git.action',{sessionId:props.sessionId,action:name,path,value}); if(name==='diff'&&'diff'in result){const key=path??'';props.onDiff(key,result,key.split('/').at(-1)??key,key)} else {setStatus(result as GitStatus);if(name==='commit')refresh()} } catch(error){setError(String(error))}finally{setBusy(false)} }
|
|
@@ -216,7 +229,7 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
|
|
|
216
229
|
const sessionId = useSessions(state=>state.current) as string|undefined; const cwd = useSessions(state=>sessionId===undefined?undefined:state.byId[sessionId]?.cwd) as string|undefined
|
|
217
230
|
const [workspace,setWorkspace]=useState<{cwd:string;name:string}>(); const [tabs,setTabs]=useState<Tab[]>([]); const [active,setActive]=useState<string>(); const [error,setError]=useState<string>(); const [panelHeight,setPanelHeight]=useState(260); const [sideWidth,setSideWidth]=useState(210); const [primarySideVisible,setPrimarySideVisible]=useState(()=>localStorage.getItem('dvw:primary-side-visible')!=='closed');const [statusVisible,setStatusVisible]=useState(()=>localStorage.getItem('dvw:status-visible')!=='closed');const [settingsOpen,setSettingsOpen]=useState(false);const [chatWidth,setChatWidth]=useState(()=>Math.max(400,Math.min(720,Math.round(Number(localStorage.getItem('dvw:dsh-chat-width'))||420))));const [chatVisible,setChatVisible]=useState(()=>localStorage.getItem('dvw:dsh-chat-visible')!=='closed');const [outlineVisible,setOutlineVisible]=useState(()=>localStorage.getItem('dvw:outline')!=='closed')
|
|
218
231
|
const tabsRef=useRef(tabs);tabsRef.current=tabs
|
|
219
|
-
const workspaceRequests=useRef(new WorkspaceRequestScope())
|
|
232
|
+
const workspaceRequests=useRef(new WorkspaceRequestScope());const loadingTabs=useRef(new Set<string>())
|
|
220
233
|
const dshDark = useDshDarkTheme()
|
|
221
234
|
const [theme,setTheme]=useState<WorkbenchTheme>(()=>{const saved=localStorage.getItem('dvw:theme');return saved==='dark'||saved==='light'?saved:'dsh'})
|
|
222
235
|
const colorScheme: 'dark' | 'light' = theme === 'dsh' ? dshDark ? 'dark' : 'light' : theme
|
|
@@ -227,11 +240,12 @@ export function Workbench({ service, useSessions }: { service: VscodeWorkbench;
|
|
|
227
240
|
useEffect(()=>localStorage.setItem('dvw:primary-side-visible',primarySideVisible?'open':'closed'),[primarySideVisible])
|
|
228
241
|
useEffect(()=>localStorage.setItem('dvw:status-visible',statusVisible?'open':'closed'),[statusVisible])
|
|
229
242
|
useEffect(()=>{if(!snapshot.visible)return;const onKey=(event:KeyboardEvent)=>{if(event.isComposing||!event.ctrlKey||!event.altKey||event.shiftKey||event.code!=='KeyB')return;event.preventDefault();setChatVisible(value=>!value)};window.addEventListener('keydown',onKey,true);return()=>window.removeEventListener('keydown',onKey,true)},[snapshot.visible])
|
|
230
|
-
const openForWorkspace=
|
|
231
|
-
useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const epoch=workspaceRequests.current.begin();const cleared=resetWorkspaceDocuments<Tab>();setWorkspace(undefined);setTabs(cleared.tabs);tabsRef.current=cleared.tabs;setActive(cleared.active);setError(cleared.error);const controller=new AbortController();void api<{cwd:string;name:string}>('workspace',{sessionId},controller.signal).then(value=>{if(!workspaceRequests.current.isCurrent(epoch))return;setWorkspace(value);const saved=localStorage.getItem(`dvw:${value.cwd}`);if(saved){try{const parsed=JSON.parse(saved) as {view?:WorkbenchView;paths?:string[];panelHeight?:number;sideWidth?:number};if(parsed.view)service.show(parsed.view);if(typeof parsed.panelHeight==='number')setPanelHeight(parsed.panelHeight);if(typeof parsed.sideWidth==='number')setSideWidth(Math.max(210,Math.min(640,Math.round(parsed.sideWidth))));
|
|
232
|
-
useEffect(()=>{if(workspace) localStorage.setItem(`dvw:${workspace.cwd}`,JSON.stringify({view:snapshot.view,paths:tabs.filter(t=>!t.path.startsWith('diff:')).map(t=>t.path),panelHeight,sideWidth}))},[workspace,snapshot.view,tabs,panelHeight,sideWidth])
|
|
233
|
-
useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const protocol=location.protocol==='https:'?'wss':'ws';const ws=new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);ws.onmessage=()
|
|
234
|
-
const
|
|
243
|
+
const openForWorkspace=(path:string,line?:number,column?:number)=>{const existing=tabsRef.current.find(tab=>tab.path===path);if(existing)setTabs(rows=>rows.map(tab=>tab.path===path?{...tab,line,column}:tab));else setTabs(rows=>[...rows,{...unloadedTab(path),line,column}]);setActive(path)}
|
|
244
|
+
useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const epoch=workspaceRequests.current.begin();const cleared=resetWorkspaceDocuments<Tab>();loadingTabs.current.clear();setWorkspace(undefined);setTabs(cleared.tabs);tabsRef.current=cleared.tabs;setActive(cleared.active);setError(cleared.error);const controller=new AbortController();void api<{cwd:string;name:string}>('workspace',{sessionId},controller.signal).then(value=>{if(!workspaceRequests.current.isCurrent(epoch))return;setWorkspace(value);const saved=localStorage.getItem(`dvw:${value.cwd}`);if(saved){try{const parsed=JSON.parse(saved) as {view?:WorkbenchView;paths?:string[];active?:string;panelHeight?:number;sideWidth?:number};if(parsed.view)service.show(parsed.view);if(typeof parsed.panelHeight==='number')setPanelHeight(parsed.panelHeight);if(typeof parsed.sideWidth==='number')setSideWidth(Math.max(210,Math.min(640,Math.round(parsed.sideWidth))));const restored=restoreTabPaths(parsed.paths??[]);setTabs(restored);setActive(restored.some(tab=>tab.path===parsed.active)?parsed.active:restored.at(-1)?.path)}catch{}}}).catch(error=>{if(!controller.signal.aborted&&workspaceRequests.current.isCurrent(epoch))setError(String(error))});return()=>{controller.abort();loadingTabs.current.clear();workspaceRequests.current.invalidate(epoch)}},[snapshot.visible,sessionId])
|
|
245
|
+
useEffect(()=>{if(workspace) localStorage.setItem(`dvw:${workspace.cwd}`,JSON.stringify({view:snapshot.view,paths:tabs.filter(t=>!t.path.startsWith('diff:')).map(t=>t.path),active,panelHeight,sideWidth}))},[workspace,snapshot.view,tabs,active,panelHeight,sideWidth])
|
|
246
|
+
useEffect(()=>{if(!snapshot.visible||sessionId===undefined)return;const protocol=location.protocol==='https:'?'wss':'ws';const ws=new WebSocket(`${protocol}://${location.host}/dsh-vscode/ws/files?sessionId=${encodeURIComponent(sessionId)}`);ws.onmessage=event=>{let paths:string[]=[];try{paths=(JSON.parse(String(event.data)) as {paths?:string[]}).paths??[]}catch{}window.dispatchEvent(new CustomEvent('dvw-files-changed',{detail:paths}));setTabs(current=>current.map(tab=>tab.dirty?{...tab,external:true}:tab))};return()=>ws.close()},[snapshot.visible,sessionId])
|
|
247
|
+
useEffect(()=>{if(sessionId===undefined||active===undefined)return;const tab=tabsRef.current.find(row=>row.path===active);if(tab===undefined||tab.loaded!==false||loadingTabs.current.has(active))return;const epoch=workspaceRequests.current.current();loadingTabs.current.add(active);if(/\.(?:avif|bmp|gif|ico|jpe?g|png|svg|webp)$/i.test(active)){const imageUrl=`/dsh-vscode/file?sessionId=${encodeURIComponent(sessionId)}&path=${encodeURIComponent(active)}`;setTabs(rows=>rows.map(row=>row.path===active?{...row,loaded:true,imageUrl}:row));loadingTabs.current.delete(active);return}void api<FileDocument>('fs.read',{sessionId,path:active}).then(doc=>{if(!workspaceRequests.current.isCurrent(epoch))return;if(doc.binary||doc.tooLarge){setTabs(rows=>rows.filter(row=>row.path!==active));setActive(current=>current===active?undefined:current);setError(doc.binary?'二进制文件不能在文本编辑器中打开':`文件超过 5 MiB:${active}`);return}setTabs(rows=>rows.map(row=>row.path===active?{...row,...doc,draft:doc.content,loaded:true}:row))}).catch(error=>{if(workspaceRequests.current.isCurrent(epoch))setError(String(error))}).finally(()=>loadingTabs.current.delete(active))},[active,sessionId,tabs])
|
|
248
|
+
const open=(path:string,line?:number,column?:number)=>{if(sessionId!==undefined)openForWorkspace(path,line,column)}
|
|
235
249
|
useEffect(()=>{const listener=(event:Event)=>void open((event as CustomEvent<string>).detail);window.addEventListener('dvw-open-workspace-path',listener);return()=>window.removeEventListener('dvw-open-workspace-path',listener)},[sessionId,tabs])
|
|
236
250
|
const save=async(tab:Tab)=>{if(sessionId===undefined||!tab.dirty)return;try{const doc=await api<FileDocument>('fs.save',{sessionId,path:tab.path,content:tab.draft,revision:tab.revision});setTabs(rows=>rows.map(row=>row.path===tab.path?{...row,...doc,draft:doc.content,dirty:false,external:false}:row))}catch(error){setError(String(error))}}
|
|
237
251
|
const close=(tab:Tab)=>{if(tab.dirty&&!confirm(`${tab.path} 有未保存更改,确定关闭?`))return;setTabs(rows=>rows.filter(row=>row.path!==tab.path));if(active===tab.path)setActive(tabs.filter(row=>row.path!==tab.path).at(-1)?.path)}
|
package/src/client/api.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
constructor(public code: string, message: string) { super(message) }
|
|
3
|
+
}
|
|
4
|
+
|
|
1
5
|
export async function api<T>(method: string, body: Record<string, unknown>, signal?: AbortSignal): Promise<T> {
|
|
2
6
|
const response = await fetch(`/dsh-vscode/api/${method}`, {
|
|
3
7
|
method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(body), signal,
|
|
4
8
|
})
|
|
5
|
-
const answer = await response.json() as { ok: boolean; value?: T; error?: { message?: string } }
|
|
6
|
-
if (!response.ok || !answer.ok) throw new
|
|
9
|
+
const answer = await response.json() as { ok: boolean; value?: T; error?: { code?: string; message?: string } }
|
|
10
|
+
if (!response.ok || !answer.ok) throw new ApiError(answer.error?.code ?? 'request-failed', answer.error?.message ?? `request failed (${response.status})`)
|
|
7
11
|
return answer.value as T
|
|
8
12
|
}
|