dsh-vscode-mode 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +148 -112
- package/assets/shell/dsh-open.cs +65 -149
- package/assets/shell/dsh-open.mjs +241 -0
- package/assets/shell/dsh-open.ps1 +28 -47
- package/assets/shell/dsh-open.sh +25 -66
- package/lib/client.js +30185 -29711
- package/lib/client.js.map +1 -1
- package/lib/index.js +3889 -2336
- package/lib/index.js.map +1 -1
- package/package.json +9 -8
- package/skills/dsh-vscodemode-mcp/SKILL.md +94 -132
- package/src/childEnv.ts +19 -0
- package/src/client/addToConversation.ts +4 -2
- package/src/client/ai/inlineProvider.ts +1 -8
- package/src/client/commandBridge.ts +6 -2
- package/src/client/externalOpen.ts +40 -19
- package/src/client/index.ts +17 -15
- package/src/client/keybindings.ts +18 -1
- package/src/client/monaco/loader.ts +3 -1
- package/src/client/openFlow.ts +36 -28
- package/src/client/openReceipt.ts +64 -0
- package/src/client/panelCommands.ts +101 -0
- package/src/client/pdf/pdfLoader.ts +4 -2
- package/src/client/receiptState.ts +120 -0
- package/src/client/rpc.ts +30 -4
- package/src/client/shortcutsOfficial.ts +39 -4
- package/src/client/sidebarBridge.ts +7 -0
- package/src/client/styles/mcp.css +1 -19
- package/src/client/ui/DiffBadge.ts +2 -1
- package/src/client/ui/EditorView.ts +237 -55
- package/src/client/ui/McpSettings.ts +32 -7
- package/src/client/ui/OfficialSideTab.ts +5 -2
- package/src/client/ui/QuickOpen.ts +10 -9
- package/src/client/ui/commandCatalog.ts +5 -4
- package/src/client/useOpenReceipt.ts +128 -0
- package/src/compat.ts +5 -3
- package/src/dap/manager.ts +46 -13
- package/src/devForm.ts +252 -92
- package/src/externalHandoff.ts +93 -52
- package/src/fileOpenSettings.ts +1 -24
- package/src/hostImport.ts +19 -0
- package/src/httpGuard.ts +101 -0
- package/src/index.ts +106 -97
- package/src/integrate.ts +56 -50
- package/src/lsp/transport.ts +84 -127
- package/src/mcp.ts +58 -9
- package/src/mcpIsolation.ts +211 -102
- package/src/mcpProject.ts +220 -133
- package/src/mcpRuntime.ts +343 -0
- package/src/openBridge.ts +69 -0
- package/src/openInbox.ts +207 -0
- package/src/paths.ts +8 -5
- package/src/processTree.ts +63 -0
- package/src/reveal.ts +5 -3
- package/src/routes.ts +156 -154
- package/src/rpc.ts +14 -13
- package/src/shared/appUrl.ts +19 -0
- package/src/shared/externalOpen.ts +71 -0
- package/src/shared/integration.ts +2 -0
- package/src/shared/keybindings.ts +8 -5
- package/src/shared/mcp.ts +4 -1
- package/src/shared/rpc.ts +7 -4
- package/src/shared/svn.ts +6 -8
- package/src/svnPlanTool.ts +51 -0
- package/src/unityBridge.ts +26 -3
- package/unity/com.dsh.editor/Editor/DshCodeEditor.cs +99 -203
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs +166 -0
- package/unity/com.dsh.editor/Editor/DshOpenWorker.cs.meta +2 -0
- package/unity/com.dsh.editor/README.md +16 -25
- package/unity/com.dsh.editor/package.json +2 -2
- package/src/client/ui/ShortcutSettings.ts +0 -294
|
@@ -6,7 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { applyOfficial, registerThemes } from './theme.js'
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
import { appUrl } from '../../shared/appUrl.js'
|
|
10
|
+
|
|
11
|
+
export const MONACO_BASE = appUrl('/edrv/vendor/monaco/vs')
|
|
10
12
|
let monacoPromise = null
|
|
11
13
|
let monacoStage = { phase: 'idle', progress: 0, message: '准备加载 Monaco…' }
|
|
12
14
|
const stageListeners = new Set()
|
package/src/client/openFlow.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { rpc } from './rpc.js'
|
|
10
10
|
import { chooseWorkspace } from './chooseDialog.js'
|
|
11
|
-
import {
|
|
11
|
+
import { requestOpen } from './openReceipt.js'
|
|
12
12
|
import { toastDom } from './externalOpen.js'
|
|
13
13
|
import { createAddToConversation } from './addToConversation.js'
|
|
14
14
|
import type { AddOutcome, CtxLike } from './addToConversation.js'
|
|
@@ -47,7 +47,7 @@ export interface OpenDeps {
|
|
|
47
47
|
openSession(id: string): void
|
|
48
48
|
reference(sessionId: string, path: string, appearance: 'file' | 'folder'): Promise<boolean>
|
|
49
49
|
choose(title: string, folder: string): Promise<'recent' | 'create' | null>
|
|
50
|
-
openEditor(path: string | null, line?: number, column?: number):
|
|
50
|
+
openEditor(path: string | null, line?: number, column?: number, sessionId?: string): Promise<boolean>
|
|
51
51
|
schedule(fn: () => void, ms: number): void
|
|
52
52
|
notify(text: string): void
|
|
53
53
|
}
|
|
@@ -57,8 +57,6 @@ const READY_TIMEOUT_MS = 15000
|
|
|
57
57
|
const READY_TICK_MS = 300
|
|
58
58
|
const REF_RETRY_MAX = 20
|
|
59
59
|
const REF_RETRY_MS = 300
|
|
60
|
-
const REST_STEP_MS = 150
|
|
61
|
-
const LINE_AT_DELAY_MS = 200
|
|
62
60
|
|
|
63
61
|
/**
|
|
64
62
|
* 路径规范化:`\→/`、小写、去尾分隔符(Windows 大小写不敏感前缀匹配用)。
|
|
@@ -124,11 +122,11 @@ export function pickRule(kind: PathKind, matched: boolean, hasSession: boolean):
|
|
|
124
122
|
* @param params 深链参数(首路径驱动规则)
|
|
125
123
|
* @param overrides 依赖覆盖(测试注入)
|
|
126
124
|
*/
|
|
127
|
-
export async function openDeepLink(ctx: unknown, params: OpenParams, overrides: Partial<OpenDeps> = {}): Promise<
|
|
125
|
+
export async function openDeepLink(ctx: unknown, params: OpenParams, overrides: Partial<OpenDeps> = {}): Promise<boolean> {
|
|
128
126
|
const deps = realDeps(ctx, overrides)
|
|
129
127
|
if (!(await waitListsReady(deps))) {
|
|
130
128
|
deps.notify('DSH 会话列表未就绪,请稍后重试')
|
|
131
|
-
return
|
|
129
|
+
return false
|
|
132
130
|
}
|
|
133
131
|
const primary = params.paths[0]
|
|
134
132
|
const kind = await deps.stat(primary).catch(() => 'missing' as PathKind)
|
|
@@ -137,14 +135,16 @@ export async function openDeepLink(ctx: unknown, params: OpenParams, overrides:
|
|
|
137
135
|
try {
|
|
138
136
|
if (rule === 'abort') {
|
|
139
137
|
deps.notify('路径不存在:' + primary)
|
|
140
|
-
return
|
|
138
|
+
return false
|
|
141
139
|
}
|
|
142
|
-
if (rule === 'choose')
|
|
143
|
-
if (rule === 'folderNew')
|
|
144
|
-
if (rule === 'fileRecent')
|
|
145
|
-
|
|
140
|
+
if (rule === 'choose') await runChoose(deps, primary, params)
|
|
141
|
+
else if (rule === 'folderNew') await runFolderNew(deps, primary, params)
|
|
142
|
+
else if (rule === 'fileRecent') await runFileRecent(deps, primary, params)
|
|
143
|
+
else await runFileBootstrap(deps, primary, params)
|
|
144
|
+
return true
|
|
146
145
|
} catch (error) {
|
|
147
146
|
deps.notify('打开失败:' + String((error as Error)?.message ?? error))
|
|
147
|
+
return false
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -162,12 +162,12 @@ async function runChoose(deps: OpenDeps, folder: string, params: OpenParams): Pr
|
|
|
162
162
|
const matches = matchWorkspaces(deps.workspaces().items, folder)
|
|
163
163
|
const best = bestMatch(deps, matches)
|
|
164
164
|
const choice = await deps.choose(best?.title ?? '工作区', folder)
|
|
165
|
-
if (choice === null)
|
|
165
|
+
if (choice === null) throw new Error('用户取消打开')
|
|
166
166
|
if (choice === 'create') return runFolderNew(deps, folder, params)
|
|
167
167
|
const ws = best ?? deps.workspaces().items[0]
|
|
168
168
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
169
169
|
await settleRef(deps, sessionId, folder, 'folder')
|
|
170
|
-
deps
|
|
170
|
+
await openChecked(deps, sessionId, null)
|
|
171
171
|
await openRest(deps, sessionId, params, 1)
|
|
172
172
|
}
|
|
173
173
|
|
|
@@ -178,21 +178,21 @@ async function runFileRecent(deps: OpenDeps, file: string, params: OpenParams):
|
|
|
178
178
|
const anchor = recentSessionIn(deps, ws)
|
|
179
179
|
if (anchor) {
|
|
180
180
|
deps.openSession(anchor.id)
|
|
181
|
-
deps.
|
|
181
|
+
await openChecked(deps, anchor.id, file, params.line, params.column)
|
|
182
182
|
await openRest(deps, anchor.id, params, 1)
|
|
183
183
|
return
|
|
184
184
|
}
|
|
185
185
|
// 文件所属工作区存在但无会话 → 在该工作区新建对话 + 文件引用
|
|
186
186
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
187
187
|
await settleRef(deps, sessionId, file, 'file')
|
|
188
|
-
deps
|
|
188
|
+
await openChecked(deps, sessionId, file, params.line, params.column)
|
|
189
189
|
await openRest(deps, sessionId, params, 1)
|
|
190
190
|
return
|
|
191
191
|
}
|
|
192
192
|
const anchor = recentSession(deps.sessions().byId, deps.sessions().ids)
|
|
193
193
|
if (!anchor) return runFileBootstrap(deps, file, params)
|
|
194
194
|
deps.openSession(anchor.id)
|
|
195
|
-
deps.
|
|
195
|
+
await openChecked(deps, anchor.id, file, params.line, params.column)
|
|
196
196
|
await openRest(deps, anchor.id, params, 1)
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -228,7 +228,7 @@ async function runFileBootstrap(deps: OpenDeps, file: string, params: OpenParams
|
|
|
228
228
|
const ws = await deps.createWorkspace(dirnameOf(file))
|
|
229
229
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
230
230
|
await settleRef(deps, sessionId, file, 'file')
|
|
231
|
-
deps
|
|
231
|
+
await openChecked(deps, sessionId, file, params.line, params.column)
|
|
232
232
|
await openRest(deps, sessionId, params, 1)
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -237,7 +237,7 @@ async function runFolderNew(deps: OpenDeps, folder: string, params: OpenParams):
|
|
|
237
237
|
const ws = await deps.createWorkspace(folder)
|
|
238
238
|
const sessionId = await spawnInWorkspace(deps, ws.workspaceId, undefined)
|
|
239
239
|
await settleRef(deps, sessionId, folder, 'folder')
|
|
240
|
-
deps
|
|
240
|
+
await openChecked(deps, sessionId, null)
|
|
241
241
|
await openRest(deps, sessionId, params, 1)
|
|
242
242
|
}
|
|
243
243
|
|
|
@@ -264,17 +264,20 @@ async function settleRef(deps: OpenDeps, sessionId: string, path: string, appear
|
|
|
264
264
|
if (await deps.reference(sessionId, path, appearance)) return
|
|
265
265
|
await sleep(deps, REF_RETRY_MS)
|
|
266
266
|
}
|
|
267
|
+
throw new Error('引用插入未完成')
|
|
267
268
|
}
|
|
268
269
|
|
|
269
270
|
/** 其余路径:文件进编辑器(步进),文件夹补文件夹引用。 */
|
|
270
271
|
async function openRest(deps: OpenDeps, sessionId: string | null, params: OpenParams, from: number): Promise<void> {
|
|
271
272
|
const rest = params.paths.slice(from)
|
|
272
|
-
for (const
|
|
273
|
+
for (const path of rest) {
|
|
273
274
|
const kind = await deps.stat(path).catch(() => 'missing' as PathKind)
|
|
274
275
|
if (kind === 'file') {
|
|
275
|
-
|
|
276
|
+
await openChecked(deps, sessionId ?? undefined, path)
|
|
276
277
|
} else if (kind === 'directory' && sessionId) {
|
|
277
278
|
await settleRef(deps, sessionId, path, 'folder')
|
|
279
|
+
} else {
|
|
280
|
+
throw new Error('路径不存在:' + path)
|
|
278
281
|
}
|
|
279
282
|
}
|
|
280
283
|
}
|
|
@@ -316,16 +319,21 @@ function realDeps(ctx: unknown, overrides: Partial<OpenDeps> = {}): OpenDeps {
|
|
|
316
319
|
return outcome === 'ok' || outcome === 'busy'
|
|
317
320
|
},
|
|
318
321
|
choose: (title, folder) => chooseWorkspace(title, folder),
|
|
319
|
-
openEditor:
|
|
320
|
-
if (path == null) {
|
|
321
|
-
openEditorView(null)
|
|
322
|
-
return
|
|
323
|
-
}
|
|
324
|
-
openEditorView(path)
|
|
325
|
-
if (line != null) setTimeout(() => emitOpenAt(path, line, column), LINE_AT_DELAY_MS)
|
|
326
|
-
},
|
|
322
|
+
openEditor: requestOpen,
|
|
327
323
|
schedule: (fn, ms) => setTimeout(fn, ms),
|
|
328
324
|
notify: (text) => toastDom(text),
|
|
329
325
|
}
|
|
330
326
|
return { ...base, ...overrides }
|
|
331
327
|
}
|
|
328
|
+
|
|
329
|
+
/** @private @author ddj 2026年09月28号
|
|
330
|
+
* Await actual editor completion and propagate failure to the request result.
|
|
331
|
+
* @param deps Open dependencies.
|
|
332
|
+
* @param sessionId Target session.
|
|
333
|
+
* @param path Target file or editor mount.
|
|
334
|
+
* @param line Optional line.
|
|
335
|
+
* @param column Optional column.
|
|
336
|
+
*/
|
|
337
|
+
async function openChecked(deps: OpenDeps, sessionId: string | undefined, path: string | null, line?: number, column?: number): Promise<void> {
|
|
338
|
+
if (!(await deps.openEditor(path, line, column, sessionId))) throw new Error('编辑器打开未完成:' + (path ?? '工作区'))
|
|
339
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { openEditorView } from './events.js'
|
|
2
|
+
|
|
3
|
+
export interface EditorRequest { requestId: string; sessionId: string; path: string | null; line?: number; column?: number }
|
|
4
|
+
interface Waiter { request: EditorRequest; finish: (success: boolean) => void; claimed?: boolean }
|
|
5
|
+
const waiters = new Map<string, Waiter>()
|
|
6
|
+
|
|
7
|
+
/** @public @author ddj 2026年09月28号
|
|
8
|
+
* Atomically claim the pending request for this editor's session on mount.
|
|
9
|
+
* @param sessionId Owning session.
|
|
10
|
+
* @returns Unclaimed request, if any.
|
|
11
|
+
*/
|
|
12
|
+
export function peekOpen(sessionId: string): EditorRequest | undefined {
|
|
13
|
+
const row = [...waiters.values()].find((item) => item.request.sessionId === sessionId && !item.claimed)
|
|
14
|
+
if (!row) return undefined
|
|
15
|
+
row.claimed = true
|
|
16
|
+
return row.request
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @public @author ddj 2026年09月28号
|
|
20
|
+
* Complete an actual editor open by its unguessable local request ID.
|
|
21
|
+
* @param requestId Claimed request identity.
|
|
22
|
+
* @param success Whether content and navigation are ready.
|
|
23
|
+
* @param error Optional diagnostic for the caller.
|
|
24
|
+
*/
|
|
25
|
+
export function finishOpen(requestId: string, success: boolean, error?: string): void {
|
|
26
|
+
const row = waiters.get(requestId)
|
|
27
|
+
if (row?.claimed) row.finish(success === true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** @public @author ddj 2026年09月28号
|
|
31
|
+
* Route an editor open and await a correlated model-ready completion.
|
|
32
|
+
* @param path File path, or null to mount the editor.
|
|
33
|
+
* @param line Optional target line.
|
|
34
|
+
* @param column Optional target column.
|
|
35
|
+
* @param sessionId Owning session, required for routing.
|
|
36
|
+
* @returns Actual completion; timeout and disposal are false.
|
|
37
|
+
*/
|
|
38
|
+
export function requestOpen(path: string | null, line?: number, column?: number, sessionId = ''): Promise<boolean> {
|
|
39
|
+
if (typeof window === 'undefined' || !sessionId) return Promise.resolve(false)
|
|
40
|
+
const request: EditorRequest = { requestId: crypto.randomUUID(), sessionId, path, line, column }
|
|
41
|
+
return new Promise((resolve) => {
|
|
42
|
+
let timer: ReturnType<typeof setTimeout>
|
|
43
|
+
/** @private @author ddj 2026年09月28号 Complete once and release the deadline. */
|
|
44
|
+
function finish(success: boolean): void {
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
waiters.delete(request.requestId)
|
|
47
|
+
resolve(success)
|
|
48
|
+
}
|
|
49
|
+
waiters.set(request.requestId, { request, finish })
|
|
50
|
+
timer = setTimeout(() => finish(false), 20_000)
|
|
51
|
+
openEditorView(null)
|
|
52
|
+
window.dispatchEvent(new CustomEvent('edrv:open-request', { detail: request }))
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** @public @author ddj 2026年09月28号
|
|
57
|
+
* Check that an editor receipt is still awaited after timeout or disposal.
|
|
58
|
+
* @param requestId Local request identity.
|
|
59
|
+
* @returns Whether this request may still be completed.
|
|
60
|
+
*/
|
|
61
|
+
export function hasOpen(requestId: string): boolean { return waiters.has(requestId) }
|
|
62
|
+
|
|
63
|
+
/** @public @author ddj 2026年09月28号 Cancel outstanding receipts when the client plugin unloads. */
|
|
64
|
+
export function disposeOpenWait(): void { for (const waiter of [...waiters.values()]) waiter.finish(false) }
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { SessionScope } from './sessionScope.js'
|
|
2
|
+
|
|
3
|
+
export type PanelAction = 'quickOpen' | 'searchInFiles'
|
|
4
|
+
export interface PanelRoute {
|
|
5
|
+
readScope(): SessionScope
|
|
6
|
+
canOpen(): boolean
|
|
7
|
+
open(): boolean
|
|
8
|
+
}
|
|
9
|
+
interface PendingPanel extends SessionScope { action: PanelAction; expires: number }
|
|
10
|
+
interface PanelHost { action: PanelAction; scope: SessionScope; run(): void }
|
|
11
|
+
let route: PanelRoute | null = null
|
|
12
|
+
const hosts = new Set<PanelHost>()
|
|
13
|
+
const storage = globalThis as typeof globalThis & { __edrvPanelCommand__?: PendingPanel }
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compare the command's session and workspace identity.
|
|
17
|
+
* @author ddj 2026年09月28号
|
|
18
|
+
* @param left Requested scope.
|
|
19
|
+
* @param right Mounted scope.
|
|
20
|
+
* @returns Whether both refer to the same session and known workspace.
|
|
21
|
+
*/
|
|
22
|
+
function sameScope(left: SessionScope, right: SessionScope): boolean {
|
|
23
|
+
return Boolean(left.sessionId && left.sessionId === right.sessionId
|
|
24
|
+
&& (!left.cwd || !right.cwd || left.cwd === right.cwd))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Find the most recently mounted matching command receiver.
|
|
29
|
+
* @author ddj 2026年09月28号
|
|
30
|
+
* @param action Panel action.
|
|
31
|
+
* @param scope Active session scope.
|
|
32
|
+
* @returns Receiver if mounted.
|
|
33
|
+
*/
|
|
34
|
+
function panelHost(action: PanelAction, scope: SessionScope): PanelHost | undefined {
|
|
35
|
+
return [...hosts].reverse().find((host) => host.action === action && sameScope(scope, host.scope))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Install the existing sidebar route and current-session reader.
|
|
40
|
+
* @author ddj 2026年09月28号
|
|
41
|
+
* @param next Route capabilities; no side effects during availability checks.
|
|
42
|
+
* @returns Identity-safe disposal; pending intent survives plugin reload.
|
|
43
|
+
*/
|
|
44
|
+
export function setPanelRoute(next: PanelRoute): () => void {
|
|
45
|
+
route = next
|
|
46
|
+
return () => { if (route === next) route = null }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Check that a command has a mounted receiver or a usable editor route.
|
|
51
|
+
* @author ddj 2026年09月28号
|
|
52
|
+
* @param action Panel action.
|
|
53
|
+
* @returns False when genuinely unavailable, allowing official key dispatch to pass.
|
|
54
|
+
*/
|
|
55
|
+
export function panelAvailable(action: PanelAction): boolean {
|
|
56
|
+
const scope = route?.readScope()
|
|
57
|
+
if (!scope?.sessionId) return false
|
|
58
|
+
return Boolean(panelHost(action, scope) || route?.canOpen())
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Deliver now or preserve an intent until the editor mounts.
|
|
63
|
+
* @author ddj 2026年09月28号
|
|
64
|
+
* @param action Panel action.
|
|
65
|
+
* @returns Whether the mounted host or sidebar accepted the request.
|
|
66
|
+
*/
|
|
67
|
+
export function runPanelCommand(action: PanelAction): boolean {
|
|
68
|
+
const scope = route?.readScope()
|
|
69
|
+
if (!scope?.sessionId) return false
|
|
70
|
+
const host = panelHost(action, scope)
|
|
71
|
+
if (host) { host.run(); return true }
|
|
72
|
+
if (!route?.canOpen()) return false
|
|
73
|
+
const pending = { ...scope, action, expires: Date.now() + 30_000 }
|
|
74
|
+
storage.__edrvPanelCommand__ = pending
|
|
75
|
+
try {
|
|
76
|
+
if (route.open()) return true
|
|
77
|
+
} catch { /* A disappearing sidebar leaves the command unhandled. */ }
|
|
78
|
+
if (storage.__edrvPanelCommand__ === pending) delete storage.__edrvPanelCommand__
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Attach one scoped receiver and consume a still-current pending intent once.
|
|
84
|
+
* @author ddj 2026年09月28号
|
|
85
|
+
* @param action Panel action.
|
|
86
|
+
* @param scope Mounted session/workspace.
|
|
87
|
+
* @param run Action using the mounted component's state.
|
|
88
|
+
* @returns Receiver disposal.
|
|
89
|
+
*/
|
|
90
|
+
export function bindPanelCommand(action: PanelAction, scope: SessionScope, run: () => void): () => void {
|
|
91
|
+
const host = { action, scope, run }
|
|
92
|
+
hosts.add(host)
|
|
93
|
+
const pending = storage.__edrvPanelCommand__
|
|
94
|
+
if (pending && (pending.expires < Date.now() || !sameScope(pending, route?.readScope() ?? {}))) {
|
|
95
|
+
delete storage.__edrvPanelCommand__
|
|
96
|
+
} else if (pending?.action === action && sameScope(pending, scope)) {
|
|
97
|
+
delete storage.__edrvPanelCommand__
|
|
98
|
+
run()
|
|
99
|
+
}
|
|
100
|
+
return () => { hosts.delete(host) }
|
|
101
|
+
}
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* 作者 ddj 2026-09-22
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
import { appUrl } from '../../shared/appUrl.js'
|
|
11
|
+
|
|
12
|
+
/** vendor 根,保留官方桌面协议及反向代理前缀。 */
|
|
13
|
+
export const PDFJS_BASE = appUrl('/edrv/vendor/pdfjs')
|
|
12
14
|
/** pdf.js Worker 地址(GlobalWorkerOptions.workerSrc)。 */
|
|
13
15
|
export const PDFJS_WORKER_SRC = PDFJS_BASE + '/build/pdf.worker.mjs'
|
|
14
16
|
/** CJK 字符映射目录(中文 PDF 渲染必需,尾斜杠固定)。 */
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { absoluteOf } from './tabActions.js'
|
|
2
|
+
import type { EditorRequest } from './openReceipt.js'
|
|
3
|
+
|
|
4
|
+
/** Identity of one load, committed together with its text or preview state. */
|
|
5
|
+
export interface ReceiptLoad {
|
|
6
|
+
sessionId: string
|
|
7
|
+
cwd: string | null
|
|
8
|
+
path: string
|
|
9
|
+
sequence: number
|
|
10
|
+
status: 'loading' | 'ready' | 'error'
|
|
11
|
+
error?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Current rendered facts; generic save/SVN errors intentionally have no receipt field. */
|
|
15
|
+
export interface ReceiptView {
|
|
16
|
+
sessionId: string
|
|
17
|
+
cwd?: string | null
|
|
18
|
+
active: string | null
|
|
19
|
+
sequence: number
|
|
20
|
+
load: ReceiptLoad | null
|
|
21
|
+
mounted: boolean
|
|
22
|
+
blocked: boolean
|
|
23
|
+
kind: 'text' | 'image' | 'pdf' | 'markdown'
|
|
24
|
+
contentPath: string | null
|
|
25
|
+
contentReady: boolean
|
|
26
|
+
modelMatches: boolean
|
|
27
|
+
positionMatches: boolean
|
|
28
|
+
imageLoaded: boolean
|
|
29
|
+
pdfLoaded: boolean
|
|
30
|
+
engineError?: string | null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Normalize identity using the workspace's path syntax, never the browser OS.
|
|
35
|
+
* @public @author ddj 2026年09月28号
|
|
36
|
+
* @param path File path.
|
|
37
|
+
* @param cwd Workspace root, including remote POSIX roots on Windows clients.
|
|
38
|
+
* @returns Absolute normalized identity, case folded only for Windows paths.
|
|
39
|
+
*/
|
|
40
|
+
export function receiptPath(path: string, cwd?: string | null): string {
|
|
41
|
+
const value = absoluteOf(path, cwd)
|
|
42
|
+
return /^(?:[a-z]:\/|\/\/)/i.test(value) ? value.toLowerCase() : value
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Preserve POSIX case when converting an external path into a tab path.
|
|
47
|
+
* @public @author ddj 2026年09月28号
|
|
48
|
+
* @param path External file path.
|
|
49
|
+
* @param cwd Workspace root.
|
|
50
|
+
* @returns Relative path only when it is inside the same correctly cased root.
|
|
51
|
+
*/
|
|
52
|
+
export function receiptTabPath(path: string, cwd?: string | null): string {
|
|
53
|
+
const value = path.replace(/\\/g, '/')
|
|
54
|
+
const base = (cwd ?? '').replace(/\\/g, '/').replace(/\/+$/, '')
|
|
55
|
+
if (base && receiptPath(value).startsWith(receiptPath(base) + '/')) return value.slice(base.length + 1)
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Match a load to the current request, rendered path, and live load sequence.
|
|
61
|
+
* @private @author ddj 2026年09月28号
|
|
62
|
+
* @param request Claimed request.
|
|
63
|
+
* @param view Current render and load metadata.
|
|
64
|
+
* @returns Whether load facts belong to this exact target and generation.
|
|
65
|
+
*/
|
|
66
|
+
function ownsLoad(request: EditorRequest, view: ReceiptView): boolean {
|
|
67
|
+
const load = view.load
|
|
68
|
+
if (!request.path || !view.active || !load) return false
|
|
69
|
+
const wanted = receiptPath(request.path, view.cwd)
|
|
70
|
+
return request.sessionId === view.sessionId && load.sessionId === view.sessionId
|
|
71
|
+
&& load.sequence === view.sequence && receiptPath(load.path, load.cwd) === wanted
|
|
72
|
+
&& receiptPath(view.active, view.cwd) === wanted
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Confirm actual content/preview/navigation readiness for a correlated generation.
|
|
77
|
+
* @public @author ddj 2026年09月28号
|
|
78
|
+
* @param request Claimed external open.
|
|
79
|
+
* @param view Rendered state, including model identity and DOM readiness.
|
|
80
|
+
* @returns True only when the requested target is genuinely presented.
|
|
81
|
+
*/
|
|
82
|
+
export function receiptReadyFor(request: EditorRequest, view: ReceiptView): boolean {
|
|
83
|
+
if (request.sessionId !== view.sessionId) return false
|
|
84
|
+
if (!request.path) return view.mounted
|
|
85
|
+
if (!ownsLoad(request, view) || view.load?.status !== 'ready' || view.blocked || view.engineError) return false
|
|
86
|
+
if (view.kind === 'image') return view.imageLoaded
|
|
87
|
+
if (view.kind === 'pdf') return view.pdfLoaded
|
|
88
|
+
if (!view.contentReady || !view.contentPath || receiptPath(view.contentPath, view.cwd) !== receiptPath(request.path, view.cwd)) return false
|
|
89
|
+
const positioned = request.line != null || request.column != null
|
|
90
|
+
if (view.kind === 'markdown') return !positioned
|
|
91
|
+
return view.modelMatches && (!positioned || view.positionMatches)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return only errors from this target's current load/render generation.
|
|
96
|
+
* @public @author ddj 2026年09月28号
|
|
97
|
+
* @param request Claimed request.
|
|
98
|
+
* @param view Rendered metadata and editor-engine startup failure.
|
|
99
|
+
* @returns Correlated error, otherwise null; prior files and save errors are ignored.
|
|
100
|
+
*/
|
|
101
|
+
export function receiptErrorFor(request: EditorRequest, view: ReceiptView): string | null {
|
|
102
|
+
if (!ownsLoad(request, view)) return null
|
|
103
|
+
if (view.engineError) return view.engineError
|
|
104
|
+
return view.load?.status === 'error' ? view.load.error ?? '文件加载失败' : null
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Guard delayed load/render callbacks against a replacement generation.
|
|
109
|
+
* @public @author ddj 2026年09月28号
|
|
110
|
+
* @param load Current committed metadata.
|
|
111
|
+
* @param sessionId Callback owner.
|
|
112
|
+
* @param path Callback target, already normalized as a tab path.
|
|
113
|
+
* @param sequence Callback load sequence.
|
|
114
|
+
* @param currentSequence Live loader sequence.
|
|
115
|
+
* @returns Whether this callback may update the receipt status.
|
|
116
|
+
*/
|
|
117
|
+
export function acceptsLoad(load: ReceiptLoad | null, sessionId: string, path: string, sequence: number, currentSequence: number): boolean {
|
|
118
|
+
return Boolean(load && load.sessionId === sessionId && load.path === path
|
|
119
|
+
&& load.sequence === sequence && sequence === currentSequence)
|
|
120
|
+
}
|
package/src/client/rpc.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* 诊断开关运行时化:内存缓存 + localStorage 持久(日志弹窗开关即改即生效,免刷新)。
|
|
5
5
|
* 作者 ddj 2026-08-20 / 2026-09-17
|
|
6
6
|
*/
|
|
7
|
+
import { appUrl } from '../shared/appUrl.js'
|
|
7
8
|
import { RPC_PATH } from '../shared/rpc.js'
|
|
8
9
|
import type { RpcMethod, RpcRequestMap, RpcResult } from '../shared/rpc.js'
|
|
9
10
|
import type { LoggerLevel } from '../shared/logger.js'
|
|
@@ -35,13 +36,38 @@ export function setDebugEnabled(on: boolean): void {
|
|
|
35
36
|
} catch (e) { /* 无 localStorage 环境:仅内存生效 */ }
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Call the authenticated RPC route using the current application base.
|
|
41
|
+
* @author ddj 2026年09月28号
|
|
42
|
+
* @param method Typed business method.
|
|
43
|
+
* @param args Business arguments.
|
|
44
|
+
* @param signal Optional cancellation signal.
|
|
45
|
+
* @returns A validated business result; transport failures reject with a readable error.
|
|
46
|
+
*/
|
|
47
|
+
export async function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M], signal?: AbortSignal): Promise<RpcResult<M>> {
|
|
48
|
+
const res = await fetch(appUrl(RPC_PATH), {
|
|
41
49
|
method: 'POST',
|
|
50
|
+
credentials: 'same-origin',
|
|
42
51
|
headers: { 'content-type': 'application/json' },
|
|
43
52
|
body: JSON.stringify({ method, args }),
|
|
44
|
-
|
|
53
|
+
signal,
|
|
54
|
+
})
|
|
55
|
+
if (!res.ok) {
|
|
56
|
+
const message = res.status === 401 ? 'DSH 登录已失效,请重新打开已登录的应用页面'
|
|
57
|
+
: res.status === 403 ? 'DSH 拒绝了当前页面的访问'
|
|
58
|
+
: res.status === 413 ? '请求超过支持的文件大小'
|
|
59
|
+
: res.status === 503 ? 'DSH 认证服务尚未就绪'
|
|
60
|
+
: 'DSH 请求失败(HTTP ' + res.status + ')'
|
|
61
|
+
throw new Error(message)
|
|
62
|
+
}
|
|
63
|
+
if (!/^application\/json(?:\s*;|\s*$)/i.test(res.headers.get('content-type') ?? '')) {
|
|
64
|
+
throw new Error('DSH 返回了非 JSON 响应,请检查应用地址和代理配置')
|
|
65
|
+
}
|
|
66
|
+
const value: unknown = await res.json()
|
|
67
|
+
if (!value || typeof value !== 'object' || typeof (value as { ok?: unknown }).ok !== 'boolean') {
|
|
68
|
+
throw new Error('DSH RPC 响应格式不正确')
|
|
69
|
+
}
|
|
70
|
+
return value as RpcResult<M>
|
|
45
71
|
}
|
|
46
72
|
|
|
47
73
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* 作者 ddj 2026年10月
|
|
9
9
|
*/
|
|
10
10
|
import { bindingToChord, chordToBinding, defaultKeybindings, normalizeKeybindings } from '../shared/keybindings.js'
|
|
11
|
-
import { matchEvent, parseChords } from './keybindings.js'
|
|
11
|
+
import { applyKeyAria, matchEvent, parseChords } from './keybindings.js'
|
|
12
12
|
import type { OfficialBinding, ShortcutProfileKey } from '../shared/keybindings.js'
|
|
13
13
|
import { log } from './log.js'
|
|
14
14
|
|
|
@@ -193,7 +193,9 @@ export function installLegacyKeys(
|
|
|
193
193
|
): () => void {
|
|
194
194
|
if (!target) return () => {}
|
|
195
195
|
const cache = new Map<string, ReturnType<typeof parseChords>>()
|
|
196
|
+
let disposed = false
|
|
196
197
|
const onKey = (event: Event): void => {
|
|
198
|
+
if (disposed) return
|
|
197
199
|
const key = event as KeyboardEvent
|
|
198
200
|
const current = chords()
|
|
199
201
|
for (const def of defs) {
|
|
@@ -211,8 +213,12 @@ export function installLegacyKeys(
|
|
|
211
213
|
return
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
|
-
target.addEventListener('keydown', onKey, true)
|
|
215
|
-
return () =>
|
|
216
|
+
target.addEventListener('keydown', onKey, { capture: true })
|
|
217
|
+
return () => {
|
|
218
|
+
if (disposed) return
|
|
219
|
+
disposed = true
|
|
220
|
+
target.removeEventListener('keydown', onKey, { capture: true })
|
|
221
|
+
}
|
|
216
222
|
}
|
|
217
223
|
|
|
218
224
|
/**
|
|
@@ -283,22 +289,51 @@ export function registerOfficialShortcuts(
|
|
|
283
289
|
export function bindCatalogChords(service: ShortcutsServiceLike, onChange: (chords: Readonly<Record<string, string>>) => void): () => void {
|
|
284
290
|
const sync = (): void => {
|
|
285
291
|
const chords: Record<string, string> = {}
|
|
292
|
+
const aria: Record<string, string | undefined> = {}
|
|
286
293
|
try {
|
|
287
294
|
for (const row of service.catalog.getSnapshot()) {
|
|
288
295
|
if (!row.id.startsWith('edrv.') || row.binding === null) continue
|
|
289
296
|
if (row.issue !== null) continue
|
|
290
|
-
const chord = bindingToChord(row.binding)
|
|
297
|
+
const chord = row.keys.length ? row.keys.join(' ') : bindingToChord(row.binding)
|
|
291
298
|
if (chord !== null) chords[row.id] = chord
|
|
299
|
+
aria[row.id] = row.aria
|
|
292
300
|
}
|
|
293
301
|
} catch (error) {
|
|
294
302
|
bridgeLog.warn('官方目录弦表重建失败:' + String(error))
|
|
295
303
|
}
|
|
304
|
+
applyKeyAria(aria)
|
|
296
305
|
onChange(chords)
|
|
297
306
|
}
|
|
298
307
|
sync()
|
|
299
308
|
return service.catalog.subscribe(sync)
|
|
300
309
|
}
|
|
301
310
|
|
|
311
|
+
/**
|
|
312
|
+
* 唤起官方快捷键弹窗(VSCodeMode 通用页的跳转入口用;插件不再自建录键界面)。
|
|
313
|
+
* 官方入口不可用(旧版 DSH / registry 缺失)或唤起失败时返回可读说明,
|
|
314
|
+
* 供设置页原样展示:不抛错、不阻断设置渲染。
|
|
315
|
+
* @author ddj 2026年09月28号
|
|
316
|
+
* @param service 官方 shortcuts 服务(可为 null:运行时探测未就绪)
|
|
317
|
+
* @returns 空串 = 已成功唤起;否则为给用户看的失败说明
|
|
318
|
+
*/
|
|
319
|
+
export function openOfficialShortcuts(service: ShortcutsServiceLike | null): string {
|
|
320
|
+
const manual = '可按 Ctrl+/ 或到 DSH 设置 → 通用 → 快捷键 打开'
|
|
321
|
+
if (!service || !service.registry || typeof service.registry.invoke !== 'function') {
|
|
322
|
+
return '官方快捷键弹窗入口不可用(当前 DSH 版本或外壳未提供):' + manual
|
|
323
|
+
}
|
|
324
|
+
try {
|
|
325
|
+
service.registry.invoke('shortcuts.open', {
|
|
326
|
+
region: 'page',
|
|
327
|
+
modal: null,
|
|
328
|
+
target: typeof document === 'undefined' ? null : document.activeElement,
|
|
329
|
+
})
|
|
330
|
+
return ''
|
|
331
|
+
} catch (error) {
|
|
332
|
+
bridgeLog.warn('打开官方快捷键弹窗失败:' + String(error))
|
|
333
|
+
return '打开官方快捷键弹窗失败:' + String(error) + ';' + manual
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
302
337
|
//#region 旧设置迁移
|
|
303
338
|
|
|
304
339
|
/** 旧设置 scope 最小形状(compat settingsBridge 绑定产物的最小面,结构与 SettingsScopeLike 兼容)。 */
|
|
@@ -133,6 +133,13 @@ export function takePendingSideOpen(sessionId?: string): { path: string | null;
|
|
|
133
133
|
* @param focusDiff 是否聚焦首个差异
|
|
134
134
|
* @returns 是否路由成功
|
|
135
135
|
*/
|
|
136
|
+
/**
|
|
137
|
+
* Check the installed editor route without opening any UI.
|
|
138
|
+
* @author ddj 2026年09月28号
|
|
139
|
+
* @returns Whether a mounted sidebar adapter owns editor navigation.
|
|
140
|
+
*/
|
|
141
|
+
export function hasSideRoute(): boolean { return ensureSideEditor !== null }
|
|
142
|
+
|
|
136
143
|
export function routeSideEditor(path: string | null, focusDiff: boolean): boolean {
|
|
137
144
|
if (typeof ensureSideEditor !== 'function') return false
|
|
138
145
|
try {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
.vsm-mcp-subtabs button.active{color:var(--dsw-alias-label-primary,#fff);font-weight:500}
|
|
19
19
|
.vsm-mcp-subtabs button.active::after{content:"";position:absolute;left:0;right:0;bottom:-1px;height:2px;background:var(--dsw-alias-brand-primary,#6d55ff);border-radius:2px}
|
|
20
20
|
|
|
21
|
-
/* 顶层 Tab:通用 /
|
|
21
|
+
/* 顶层 Tab:通用 / MCP 管理 / 语言服务器 / AI 补全 / 性能优化 / 兼容性 */
|
|
22
22
|
.vsm-mcp-tabs{display:flex;gap:24px;border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
|
|
23
23
|
.vsm-mcp-tabs button{border:0;background:none;color:var(--dsw-alias-label-tertiary,#9aa5b1);padding:9px 2px 11px;cursor:pointer;font-size:14px;position:relative}
|
|
24
24
|
.vsm-mcp-tabs button:hover{color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
@@ -128,24 +128,6 @@
|
|
|
128
128
|
.vsm-general-row button:disabled{opacity:.55;cursor:default}
|
|
129
129
|
.vsm-general-row button.vsm-devform-close{color:var(--dsw-alias-state-error-primary,#f07878);border-color:var(--dsw-alias-state-error-secondary,#d9534f55)}
|
|
130
130
|
|
|
131
|
-
/* keyboard shortcuts page */
|
|
132
|
-
.vsm-kb-list{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
133
|
-
.vsm-kb-row{display:flex;align-items:center;gap:10px;padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#161616)}
|
|
134
|
-
.vsm-kb-row.rec{border-color:var(--dsw-alias-brand-primary,#4f8cff)}
|
|
135
|
-
.vsm-kb-label{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;color:var(--dsw-alias-label-primary,#eee);font-size:13px}
|
|
136
|
-
.vsm-kb-warn{color:var(--dsw-alias-state-warn-primary,#e5b93f);font-size:11px}
|
|
137
|
-
.vsm-kb-key{min-width:96px;text-align:center;padding:3px 10px;border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-primary,#eee);font-size:12px;font-family:var(--ds-font-family-code,ui-monospace,monospace)}
|
|
138
|
-
.vsm-kb-key.warn{border-color:var(--dsw-alias-state-warn-primary,#e5b93f);color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
139
|
-
.vsm-kb-key.invalid{border-color:var(--dsw-alias-state-error-primary,#d9534f);color:var(--dsw-alias-state-error-primary,#d9534f)}
|
|
140
|
-
.vsm-kb-rec{font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}
|
|
141
|
-
.vsm-kb-actions{display:flex;gap:6px}
|
|
142
|
-
.vsm-kb-actions button{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:4px 10px;background:none;color:var(--dsw-alias-label-primary,#eee);cursor:pointer;font-size:12px;white-space:nowrap}
|
|
143
|
-
.vsm-kb-actions button:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
144
|
-
.vsm-kb-actions button:disabled{opacity:.55;cursor:default}
|
|
145
|
-
.vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}
|
|
146
|
-
.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
147
|
-
.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}
|
|
148
|
-
|
|
149
131
|
/* 语言服务器(LSP)设置子 Tab */
|
|
150
132
|
.vsm-lsp-form{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
151
133
|
.vsm-lsp-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}
|