dsh-vscode-mode 0.9.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -3
- package/lib/client.js +1587 -301
- package/lib/client.js.map +1 -1
- package/lib/index.js +2163 -1042
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/addToConversation.ts +74 -28
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +25 -0
- package/src/client/luaReservedWords.ts +10 -0
- package/src/client/monaco/lsp/keywords.ts +121 -0
- package/src/client/monaco/lsp/lspClient.ts +68 -6
- package/src/client/monaco/lsp/memberScope.ts +90 -0
- package/src/client/monaco/lsp/providers.ts +14 -4
- package/src/client/settingsDocRouter.ts +52 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/SvnPanel.ts +1 -1
- package/src/client/sidebar/types.ts +2 -0
- package/src/client/styles/editor.css +143 -37
- package/src/client/svnActions.ts +4 -4
- package/src/client/ui/ContextMenu.ts +4 -2
- package/src/client/ui/EditorView.ts +132 -52
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/index.ts +2 -0
- package/src/lsp/completionCap.ts +80 -0
- package/src/lsp/manager.ts +42 -10
- package/src/lsp/rpc.ts +8 -5
- package/src/lsp/server.ts +18 -4
- package/src/rpc.ts +74 -2
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/svn.ts +38 -0
- package/src/svn.ts +102 -7
- package/src/svnText.ts +22 -13
- package/src/svnUpdateJob.ts +588 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode host — 补全结果的前缀感知截断(纯函数,node 可测)。
|
|
3
|
+
* 背景:RPC 层按固定上限硬截断补全条目,而本仓库的 xLua 绑定桩
|
|
4
|
+
* (Assets/Scripts/EmmyLuaDefine,形如 CS_IslandSplash_Extensions_Common_ArrayExtension)
|
|
5
|
+
* 有上万个全局,字母序会把项目自身全局(ECommon / EActivity 等)挤到上限之外;
|
|
6
|
+
* 截断发生在 Monaco 前缀过滤之前,因此**之后无论输入什么都取不回**该条目。
|
|
7
|
+
* 改为按当前输入前缀分桶后截断:优先保留真实命中的条目;未超限时零行为变化。
|
|
8
|
+
* 作者 ddj 2026年09月24号
|
|
9
|
+
*/
|
|
10
|
+
import type { LspCompletionItem } from '../shared/lsp.js'
|
|
11
|
+
|
|
12
|
+
/** 单次补全返回条目上限(RPC 载荷护栏,与 references 同口径)。 */
|
|
13
|
+
export const COMPLETION_ITEM_CAP = 500
|
|
14
|
+
|
|
15
|
+
/** 截断结果:items 恒不超上限;truncated 标记本轮是否发生了丢弃。 */
|
|
16
|
+
export interface CappedCompletions {
|
|
17
|
+
items: LspCompletionItem[]
|
|
18
|
+
truncated: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* 判断 needle 是否为 haystack 的子序列(两侧均已小写)。
|
|
23
|
+
*
|
|
24
|
+
* 与 Monaco 候选列表的模糊匹配同口径,保证「分桶判为命中」的条目确实是
|
|
25
|
+
* 用户输入能筛出来的那些,避免分桶顺序与前端过滤结果互相矛盾。
|
|
26
|
+
* @author ddj 2026年09月24号
|
|
27
|
+
* @param haystack 候选文本(已小写)
|
|
28
|
+
* @param needle 输入前缀(已小写、非空)
|
|
29
|
+
* @returns 是否为子序列
|
|
30
|
+
*/
|
|
31
|
+
function isSubsequence(haystack: string, needle: string): boolean {
|
|
32
|
+
let matched = 0
|
|
33
|
+
for (let i = 0; i < haystack.length && matched < needle.length; i++) {
|
|
34
|
+
if (haystack[i] === needle[matched]) matched++
|
|
35
|
+
}
|
|
36
|
+
return matched === needle.length
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 取补全项的匹配文本(Monaco 口径:filterText 优先,回落 label;统一小写)。
|
|
41
|
+
* @author ddj 2026年09月24号
|
|
42
|
+
* @param item 补全项
|
|
43
|
+
* @returns 小写匹配文本;无可用文本时为空串
|
|
44
|
+
*/
|
|
45
|
+
function textOfItem(item: LspCompletionItem): string {
|
|
46
|
+
const raw = typeof item?.filterText === 'string' && item.filterText ? item.filterText : item?.label
|
|
47
|
+
return typeof raw === 'string' ? raw.toLowerCase() : ''
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 按输入前缀分桶后截断补全列表。
|
|
52
|
+
*
|
|
53
|
+
* 分桶优先级:startsWith > includes > 子序列 > 其余;桶内保持服务器原始顺序
|
|
54
|
+
* (服务器已按相关性排序,不重新打分)。无前缀或未超限时与旧行为完全一致。
|
|
55
|
+
* @author ddj 2026年09月24号
|
|
56
|
+
* @param items 服务器归一化后的补全条目
|
|
57
|
+
* @param prefix 当前输入前缀(Monaco getWordUntilPosition 的词)
|
|
58
|
+
* @param cap 条目上限(缺省 COMPLETION_ITEM_CAP;测试可注入小值)
|
|
59
|
+
* @returns 有界条目与截断标记
|
|
60
|
+
*/
|
|
61
|
+
export function capCompletions(
|
|
62
|
+
items: LspCompletionItem[],
|
|
63
|
+
prefix?: string,
|
|
64
|
+
cap: number = COMPLETION_ITEM_CAP,
|
|
65
|
+
): CappedCompletions {
|
|
66
|
+
if (!Array.isArray(items)) return { items: [], truncated: false }
|
|
67
|
+
if (items.length <= cap) return { items, truncated: false }
|
|
68
|
+
const needle = String(prefix ?? '').toLowerCase()
|
|
69
|
+
if (!needle) return { items: items.slice(0, cap), truncated: true }
|
|
70
|
+
const buckets: LspCompletionItem[][] = [[], [], [], []]
|
|
71
|
+
for (const item of items) {
|
|
72
|
+
const text = textOfItem(item)
|
|
73
|
+
if (text.startsWith(needle)) buckets[0].push(item)
|
|
74
|
+
else if (text.includes(needle)) buckets[1].push(item)
|
|
75
|
+
else if (isSubsequence(text, needle)) buckets[2].push(item)
|
|
76
|
+
else buckets[3].push(item)
|
|
77
|
+
}
|
|
78
|
+
const ordered = buckets[0].concat(buckets[1], buckets[2], buckets[3])
|
|
79
|
+
return { items: ordered.slice(0, cap), truncated: true }
|
|
80
|
+
}
|
package/src/lsp/manager.ts
CHANGED
|
@@ -12,11 +12,13 @@ interface Entry {
|
|
|
12
12
|
server: LspServer
|
|
13
13
|
refs: number
|
|
14
14
|
failures: number
|
|
15
|
+
readyAt: number
|
|
15
16
|
restartTimer: ReturnType<typeof setTimeout> | null
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
const BACKOFF_MS = [1000, 5000, 30000]
|
|
19
20
|
const MAX_FAILURES = 3
|
|
21
|
+
const STABLE_MS = 60000
|
|
20
22
|
|
|
21
23
|
export interface LspManager {
|
|
22
24
|
/** 获取(或惰性创建)某工作区+语言的服务器;refs+1。 */
|
|
@@ -51,6 +53,19 @@ export function createLspManager(logger?: (line: string) => void): LspManager {
|
|
|
51
53
|
|
|
52
54
|
const log = (line: string): void => logger?.('[lsp-manager] ' + line)
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 重试耗尽后保留退出根因,并让 UI 不再误报“未启动”。
|
|
58
|
+
* @author ddj 2026年09月24号
|
|
59
|
+
* @param entry 服务器条目
|
|
60
|
+
* @returns 当前可见状态
|
|
61
|
+
*/
|
|
62
|
+
const statusOf = (entry: Entry): LspServerStatus => {
|
|
63
|
+
const status = entry.server.status()
|
|
64
|
+
return entry.failures >= MAX_FAILURES
|
|
65
|
+
? { ...status, phase: 'unavailable', reason: (status.reason ?? '服务器已退出') + '(已停止自动重试)' }
|
|
66
|
+
: status
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** 同步摘除注册表项并后台回收(refs 归零/工作区清理用;statusAll 立即可见归零)。 */
|
|
55
70
|
const dropEntry = (entry: Entry): void => {
|
|
56
71
|
if (entry.restartTimer) {
|
|
@@ -62,20 +77,22 @@ export function createLspManager(logger?: (line: string) => void): LspManager {
|
|
|
62
77
|
}
|
|
63
78
|
|
|
64
79
|
const scheduleRestart = (entry: Entry): void => {
|
|
65
|
-
if (entry.restartTimer) return
|
|
80
|
+
if (entry.restartTimer || entry.refs === 0 || entries.get(entry.key) !== entry) return
|
|
66
81
|
if (entry.failures >= MAX_FAILURES) {
|
|
67
82
|
log('unavailable after ' + entry.failures + ' failures: ' + entry.key)
|
|
83
|
+
manager.onStatusChange?.(statusOf(entry))
|
|
68
84
|
return
|
|
69
85
|
}
|
|
70
|
-
const delay = BACKOFF_MS[Math.min(entry.failures, BACKOFF_MS.length - 1)]
|
|
86
|
+
const delay = BACKOFF_MS[Math.min(Math.max(entry.failures - 1, 0), BACKOFF_MS.length - 1)]
|
|
71
87
|
entry.restartTimer = setTimeout(() => {
|
|
72
88
|
entry.restartTimer = null
|
|
73
|
-
if (
|
|
89
|
+
if (entries.get(entry.key) !== entry || entry.refs === 0) return
|
|
74
90
|
void (async () => {
|
|
75
91
|
const ok = await entry.server.start()
|
|
76
|
-
if (!ok) entry.
|
|
77
|
-
|
|
78
|
-
|
|
92
|
+
if (!ok && entries.get(entry.key) === entry && !entry.restartTimer) {
|
|
93
|
+
entry.failures++
|
|
94
|
+
scheduleRestart(entry)
|
|
95
|
+
}
|
|
79
96
|
})()
|
|
80
97
|
}, delay)
|
|
81
98
|
}
|
|
@@ -87,13 +104,28 @@ export function createLspManager(logger?: (line: string) => void): LspManager {
|
|
|
87
104
|
if (!entry) {
|
|
88
105
|
const spec = resolveSpec(languageId)
|
|
89
106
|
const server = createLspServer(spec, root, languageId, log)
|
|
90
|
-
|
|
91
|
-
entry = { key, server, refs: 0, failures: 0, restartTimer: null }
|
|
107
|
+
entry = { key, server, refs: 0, failures: 0, readyAt: 0, restartTimer: null }
|
|
92
108
|
entries.set(key, entry)
|
|
109
|
+
server.onStateChange = (status) => {
|
|
110
|
+
const live = entries.get(key)
|
|
111
|
+
if (live?.server === server) {
|
|
112
|
+
if (status.phase === 'ready' && live.readyAt === 0) live.readyAt = Date.now()
|
|
113
|
+
if (status.phase === 'idle' && status.reason?.startsWith('服务器已退出')) {
|
|
114
|
+
if (live.readyAt && Date.now() - live.readyAt >= STABLE_MS) live.failures = 0
|
|
115
|
+
live.failures++
|
|
116
|
+
live.readyAt = 0
|
|
117
|
+
scheduleRestart(live)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
manager.onStatusChange?.(live?.server === server ? statusOf(live) : status)
|
|
121
|
+
}
|
|
93
122
|
// 惰性启动:若 spec ready 才启动,否则保持 idle(供 status 展示 reason)
|
|
94
123
|
if (spec.ready) {
|
|
95
124
|
void server.start().then((ok) => {
|
|
96
|
-
if (!ok && entries.
|
|
125
|
+
if (!ok && entries.get(key) === entry && server.phase === 'idle' && !entry!.restartTimer) {
|
|
126
|
+
entry!.failures++
|
|
127
|
+
scheduleRestart(entry!)
|
|
128
|
+
}
|
|
97
129
|
})
|
|
98
130
|
}
|
|
99
131
|
}
|
|
@@ -126,7 +158,7 @@ export function createLspManager(logger?: (line: string) => void): LspManager {
|
|
|
126
158
|
},
|
|
127
159
|
|
|
128
160
|
statusAll(): LspServerStatus[] {
|
|
129
|
-
return [...entries.values()].map(
|
|
161
|
+
return [...entries.values()].map(statusOf)
|
|
130
162
|
},
|
|
131
163
|
|
|
132
164
|
peek(root: string, languageId: string): LspServer | undefined {
|
package/src/lsp/rpc.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { searchRoot } from '../search/ripgrep.js'
|
|
|
10
10
|
import { sessionOf, cwdOf } from '../registry.js'
|
|
11
11
|
import { resolveProviderSpec, langOfPath, configFromPlugin, mergeConfig, sanitizeLang, LSP_LANGUAGES, type LspConfig, type LspLangConfig } from './config.js'
|
|
12
12
|
import { clearProviderCache, candidatesFor } from './providers.js'
|
|
13
|
+
import { capCompletions } from './completionCap.js'
|
|
13
14
|
import { onRuntimeProvisioned, envInstallStates } from './dotnetProvision.js'
|
|
14
15
|
import { envRequirementsFor, installRequirement } from './envRequirements.js'
|
|
15
16
|
import type { LspManager } from './manager.js'
|
|
@@ -451,11 +452,13 @@ export function createLspRpc(deps: LspRpcDeps): { handlers: Partial<RpcHandlerMa
|
|
|
451
452
|
args.context,
|
|
452
453
|
)
|
|
453
454
|
if (!completions) return { ok: true, completions: undefined }
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
455
|
+
// 前缀感知截断:超限时按当前输入前缀分桶,保证正在打的符号不被挤出上限
|
|
456
|
+
// (本仓库上万个 CS_* 绑定桩会把 ECommon 等项目全局顶到 500 条之外)。
|
|
457
|
+
const capped = capCompletions(completions.items, args.prefix)
|
|
458
|
+
const payload = { ...completions, items: capped.items }
|
|
459
|
+
if (capped.truncated) payload.truncated = true
|
|
460
|
+
else delete payload.truncated
|
|
461
|
+
return { ok: true, completions: payload }
|
|
459
462
|
} catch (error) {
|
|
460
463
|
return { ok: false, error: 'LSP 补全查询失败:' + String(error) }
|
|
461
464
|
}
|
package/src/lsp/server.ts
CHANGED
|
@@ -57,6 +57,19 @@ interface OpenDoc {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
const RESTARTABLE = true
|
|
60
|
+
const MAX_DOC_VERSION = 2147483647
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 使用合法、递增的 Int32 文档版本;旧客户端的毫秒时间戳不能直发给 Roslyn。
|
|
64
|
+
* @author ddj 2026年09月24号
|
|
65
|
+
* @param incoming 客户端传来的版本
|
|
66
|
+
* @param previous 当前服务器记录的版本
|
|
67
|
+
* @returns 可发送给语言服务器的版本
|
|
68
|
+
*/
|
|
69
|
+
export function lspDocVersion(incoming: number, previous?: number): number {
|
|
70
|
+
const next = Math.min(MAX_DOC_VERSION, (previous ?? 0) + 1)
|
|
71
|
+
return Number.isInteger(incoming) && incoming >= next && incoming <= MAX_DOC_VERSION ? incoming : next
|
|
72
|
+
}
|
|
60
73
|
|
|
61
74
|
/**
|
|
62
75
|
* 创建语言服务器会话。
|
|
@@ -237,21 +250,22 @@ export function createLspServer(spec: LspProviderSpec, root: string, languageId:
|
|
|
237
250
|
sync(path: string, text: string, version: number): void {
|
|
238
251
|
const existing = docs.get(path)
|
|
239
252
|
const uri = existing?.uri ?? docUriOf(path)
|
|
253
|
+
const safeVersion = lspDocVersion(version, existing?.version)
|
|
240
254
|
if (existing) {
|
|
241
|
-
existing.version =
|
|
255
|
+
existing.version = safeVersion
|
|
242
256
|
existing.text = text
|
|
243
257
|
if (phase === 'ready' || phase === 'indexing') {
|
|
244
258
|
client?.notify('textDocument/didChange', {
|
|
245
|
-
textDocument: { uri, version },
|
|
259
|
+
textDocument: { uri, version: safeVersion },
|
|
246
260
|
contentChanges: [{ text }],
|
|
247
261
|
})
|
|
248
262
|
}
|
|
249
263
|
return
|
|
250
264
|
}
|
|
251
|
-
docs.set(path, { path, uri, version, text })
|
|
265
|
+
docs.set(path, { path, uri, version: safeVersion, text })
|
|
252
266
|
if (phase === 'ready' || phase === 'indexing') {
|
|
253
267
|
client?.notify('textDocument/didOpen', {
|
|
254
|
-
textDocument: { uri, languageId, version, text },
|
|
268
|
+
textDocument: { uri, languageId, version: safeVersion, text },
|
|
255
269
|
})
|
|
256
270
|
}
|
|
257
271
|
},
|
package/src/rpc.ts
CHANGED
|
@@ -51,6 +51,7 @@ import { clearDebugLog, debugRecord, isDebugLogName, listDebugLogs, readDebugLog
|
|
|
51
51
|
import { markActiveSessions, markOfficialFlags, moveOutSessions, planMoveOut, purgeArchive, restoreSession, scanSessionInventory, sessionsArchiveRoot, sessionSizeOf, sidecarSummaryOf, unarchiveOfficial } from './perf.js'
|
|
52
52
|
import { patchHasPerfConfig, patchInsertPerfConfig, patchRemovePerfConfig, perfConfigBlock } from './perfPatch.js'
|
|
53
53
|
import type { FileVersions } from './fileVersions.js'
|
|
54
|
+
import { settingsDocPath } from './settingsDocPath.js'
|
|
54
55
|
|
|
55
56
|
/** cwd → 上次 stale 自动清理时间:全量轮询(EditorView/DiffBadge/Dock 各自 5s)节流,避免每轮都读文件算指纹。 */
|
|
56
57
|
const staleCheckedAt = new Map<string, number>()
|
|
@@ -109,6 +110,23 @@ function snippetTargetOf(path: string): string | null {
|
|
|
109
110
|
return isSnippetFilePath(path) ? path.replace(/\\/g, '/') : null
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* 返回与当前 settings provider 文档路径精确匹配的授权目标。
|
|
116
|
+
* @author ddj 2026年10月02号
|
|
117
|
+
* @param ctx DSH host 上下文
|
|
118
|
+
* @param path 编辑器请求路径
|
|
119
|
+
* @returns provider 当前配置文档的绝对路径;未匹配返回 null
|
|
120
|
+
*/
|
|
121
|
+
function settingsTargetOf(ctx: Ctx, path: string): string | null {
|
|
122
|
+
try {
|
|
123
|
+
const settings = ctx.get('settings') as { documentPath?: unknown } | undefined
|
|
124
|
+
return settingsDocPath(settings?.documentPath, path)
|
|
125
|
+
} catch {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
112
130
|
/** readTargetOf 成功形态:解析后的目标路径、stat 信息与 fs 服务句柄。 */
|
|
113
131
|
interface ReadTargetOk {
|
|
114
132
|
target: string
|
|
@@ -473,6 +491,18 @@ export function buildHandlers(
|
|
|
473
491
|
return { ok: true, results }
|
|
474
492
|
},
|
|
475
493
|
'edrv.read': async (args) => {
|
|
494
|
+
const settingsTarget = settingsTargetOf(ctx, args.path)
|
|
495
|
+
if (settingsTarget && args.encoding !== 'base64') {
|
|
496
|
+
try {
|
|
497
|
+
const info = await stat(settingsTarget)
|
|
498
|
+
if (!info.isFile()) return { ok: false, error: '设置配置文件不存在' }
|
|
499
|
+
if (info.size > READ_CAP) return { ok: false, error: '设置配置文件过大(>8MB)' }
|
|
500
|
+
const content = await readFile(settingsTarget, 'utf8')
|
|
501
|
+
return { ok: true, content, size: content.length, version: fileVersionOf(info) }
|
|
502
|
+
} catch (error) {
|
|
503
|
+
return { ok: false, error: '读取设置配置文件失败:' + String(error), resolvedPath: settingsTarget }
|
|
504
|
+
}
|
|
505
|
+
}
|
|
476
506
|
// 全局片段文件(~/.dsh/snippets/*.code-snippets)在工作区之外:直读,不走会话 cwd 解析
|
|
477
507
|
const snippetTarget = snippetTargetOf(args.path)
|
|
478
508
|
if (snippetTarget && args.encoding !== 'base64') {
|
|
@@ -522,9 +552,26 @@ export function buildHandlers(
|
|
|
522
552
|
}
|
|
523
553
|
},
|
|
524
554
|
'edrv.versions': async (args) => {
|
|
525
|
-
//
|
|
555
|
+
// 工作区外的 settings 文档按同一精确授权路径直接观察,其余路径保持工作区版本服务策略。
|
|
556
|
+
const items = []
|
|
557
|
+
const regularPaths: string[] = []
|
|
558
|
+
for (const path of args.paths) {
|
|
559
|
+
const target = settingsTargetOf(ctx, path)
|
|
560
|
+
if (!target) {
|
|
561
|
+
regularPaths.push(path)
|
|
562
|
+
continue
|
|
563
|
+
}
|
|
564
|
+
try {
|
|
565
|
+
const info = await stat(target)
|
|
566
|
+
items.push({ path, version: fileVersionOf(info), size: info.size, type: 'file' as const })
|
|
567
|
+
} catch (error) {
|
|
568
|
+
items.push({ path, version: '', type: 'missing' as const, error: String(error) })
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (regularPaths.length === 0) return { ok: true, items }
|
|
526
572
|
if (!fileVersions) return { ok: false, error: '文件版本观察器未装配' }
|
|
527
|
-
|
|
573
|
+
const regular = await fileVersions.versions({ ...args, paths: regularPaths })
|
|
574
|
+
return { ...regular, items: [...items, ...regular.items] }
|
|
528
575
|
},
|
|
529
576
|
'edrv.original': async (args) => {
|
|
530
577
|
// 重建"本批次修改前"内容:DiffEditor 原始侧。仅反解 pending 块;
|
|
@@ -553,6 +600,20 @@ export function buildHandlers(
|
|
|
553
600
|
}
|
|
554
601
|
},
|
|
555
602
|
'edrv.save': async (args) => {
|
|
603
|
+
const settingsTarget = settingsTargetOf(ctx, args.path)
|
|
604
|
+
if (settingsTarget) {
|
|
605
|
+
try {
|
|
606
|
+
const info = await stat(settingsTarget)
|
|
607
|
+
const currentRev = fileVersionOf(info)
|
|
608
|
+
if (args.rev && args.rev !== currentRev) return { ok: false, conflict: true, error: '设置配置文件已被外部修改,请先重新加载' }
|
|
609
|
+
if (settingsTargetOf(ctx, args.path) !== settingsTarget) return { ok: false, error: '设置配置文件路径已变化,拒绝保存' }
|
|
610
|
+
await writeFile(settingsTarget, args.content, 'utf8')
|
|
611
|
+
const saved = await stat(settingsTarget).catch(() => null)
|
|
612
|
+
return { ok: true, rev: fileVersionOf(saved) || currentRev }
|
|
613
|
+
} catch (error) {
|
|
614
|
+
return { ok: false, error: '保存设置配置文件失败:' + String(error) }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
556
617
|
// 全局片段文件:直写(工作区外),且不进差异审查(片段变更不是 agent 编辑产物)
|
|
557
618
|
const snippetTarget = snippetTargetOf(args.path)
|
|
558
619
|
if (snippetTarget) {
|
|
@@ -925,6 +986,17 @@ export function buildHandlers(
|
|
|
925
986
|
const value = section?.value as { fileOpenTool?: unknown } | undefined
|
|
926
987
|
return { ok: true, fileOpenTool: normalizeFileOpenTool(value?.fileOpenTool ?? FILE_OPEN_DEFAULT), integrationBaseUrl: await integrationBaseUrlOf(ctx), revision: section?.revision }
|
|
927
988
|
},
|
|
989
|
+
'vscode.settingsDocumentPath': async () => {
|
|
990
|
+
try {
|
|
991
|
+
const settings = ctx.get('settings') as { prepareDocument?: () => Promise<string> } | undefined
|
|
992
|
+
if (typeof settings?.prepareDocument !== 'function') return { ok: false, error: '设置配置文件不可用' }
|
|
993
|
+
const path = await settings.prepareDocument()
|
|
994
|
+
if (typeof path !== 'string' || !path.trim()) return { ok: false, error: '设置配置文件路径为空' }
|
|
995
|
+
return { ok: true, path }
|
|
996
|
+
} catch (error) {
|
|
997
|
+
return { ok: false, error: '准备设置配置文件失败:' + String(error) }
|
|
998
|
+
}
|
|
999
|
+
},
|
|
928
1000
|
'vscode.fileOpenSettingsUpdate': async (args) => {
|
|
929
1001
|
const settings = ctx.get('settings')
|
|
930
1002
|
const patch: Record<string, unknown> = { fileOpenTool: normalizeFileOpenTool(args.fileOpenTool) }
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { posix, win32 } from 'node:path'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the settings document only when the request names its exact configured path.
|
|
5
|
+
*
|
|
6
|
+
* Both sides are resolved with the path rules of `platform` (win32 → `C:`-style paths and
|
|
7
|
+
* case-insensitive comparison; otherwise POSIX rules and case-sensitive comparison), so the
|
|
8
|
+
* function keeps its meaning when the host platform is passed explicitly instead of implied.
|
|
9
|
+
* @author ddj 2026年10月02号
|
|
10
|
+
* @param configuredPath Path exposed by the active settings provider
|
|
11
|
+
* @param requestedPath Path requested by the editor RPC
|
|
12
|
+
* @param platform Platform whose path rules resolve both sides and drive case comparison
|
|
13
|
+
* @returns The configured absolute path when authorized; otherwise null
|
|
14
|
+
*/
|
|
15
|
+
export function settingsDocPath(
|
|
16
|
+
configuredPath: unknown,
|
|
17
|
+
requestedPath: string,
|
|
18
|
+
platform = process.platform,
|
|
19
|
+
): string | null {
|
|
20
|
+
if (typeof configuredPath !== 'string' || !configuredPath.trim() || !requestedPath) return null
|
|
21
|
+
try {
|
|
22
|
+
const api = platform === 'win32' ? win32 : posix
|
|
23
|
+
const configured = api.resolve(configuredPath)
|
|
24
|
+
const requested = api.resolve(requestedPath)
|
|
25
|
+
const fold = (value: string): string => platform === 'win32' ? value.toLowerCase() : value
|
|
26
|
+
return fold(configured) === fold(requested) ? configured : null
|
|
27
|
+
} catch {
|
|
28
|
+
return null
|
|
29
|
+
}
|
|
30
|
+
}
|
package/src/shared/rpc.ts
CHANGED
|
@@ -19,7 +19,7 @@ import type { RuleInfo, RuleProject, RuleRefInput, RuleSaveInput } from './rules
|
|
|
19
19
|
import type { SnippetEntry, SnippetInfo, SnippetProject, SnippetRefInput, SnippetSaveInput } from './snippets.js'
|
|
20
20
|
import type { LspCompletionItem, LspCompletionList, LspEnvInstallState, LspExtInfo, LspExtUpdate, LspHover, LspLocation, LspMarketItem, LspPosition, LspSemanticTokens, LspServerStatus, LspSignatureHelp, LspSymbol } from './lsp.js'
|
|
21
21
|
import type { AiConfigPatch, AiConfigView, AiDirectoryView, AiInlineRequest, AiInlineResult } from './ai.js'
|
|
22
|
-
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdateResult } from './svn.js'
|
|
22
|
+
import type { SvnAction, SvnAiPlan, SvnChangeEntry, SvnConflictArtifact, SvnDiffRevResult, SvnIgnoreItem, SvnLogEntry, SvnRemoteOutdatedEntry, SvnStatusPayload, SvnSumEntry, SvnUpdatePoll, SvnUpdateResult, SvnUpdateState } from './svn.js'
|
|
23
23
|
import type { DapAction, DapAdapterInfo, DapBreakpointAck, DapBreakpointInput, DapConfigSnippet, DapConfigSource, DapDebugConfig, DapFrameView, DapPhase, DapPollResult, DapProcessInfo, DapScopeView, DapVariableView } from './dap.js'
|
|
24
24
|
import { DAP_ACTIONS } from './dap.js'
|
|
25
25
|
import type { LoggerLevel } from './logger.js'
|
|
@@ -371,6 +371,7 @@ export interface RpcRequestMap {
|
|
|
371
371
|
'mcp.projectToggle': { workspacePath: string; serverName: string; enabled: boolean }
|
|
372
372
|
'mcp.projectRefresh': { workspacePath: string; serverName: string }
|
|
373
373
|
'vscode.fileOpenSettingsGet': {}
|
|
374
|
+
'vscode.settingsDocumentPath': {}
|
|
374
375
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; integrationBaseUrl?: string; expectedRevision?: number }
|
|
375
376
|
'edrv.integration.status': {}
|
|
376
377
|
'edrv.integration.register': {}
|
|
@@ -396,7 +397,7 @@ export interface RpcRequestMap {
|
|
|
396
397
|
'edrv.lsp.workspaceSymbol': { sessionId?: string; query: string }
|
|
397
398
|
'edrv.lsp.hover': { sessionId?: string; path: string; position: LspPosition }
|
|
398
399
|
'edrv.lsp.semanticTokens': { sessionId?: string; path: string }
|
|
399
|
-
'edrv.lsp.completion': { sessionId?: string; path: string; position: LspPosition; context?: { triggerKind?: number; triggerCharacter?: string } }
|
|
400
|
+
'edrv.lsp.completion': { sessionId?: string; path: string; position: LspPosition; context?: { triggerKind?: number; triggerCharacter?: string }; prefix?: string }
|
|
400
401
|
'edrv.lsp.resolveCompletion': { sessionId?: string; path: string; item: LspCompletionItem }
|
|
401
402
|
'edrv.lsp.signatureHelp': { sessionId?: string; path: string; position: LspPosition }
|
|
402
403
|
'edrv.lsp.redetect': { languageId: string }
|
|
@@ -447,6 +448,10 @@ export interface RpcRequestMap {
|
|
|
447
448
|
'svn.wcRev': { sessionId?: string; path?: string }
|
|
448
449
|
'svn.cleanup': { sessionId?: string; removeUnversioned?: boolean; removeIgnored?: boolean }
|
|
449
450
|
'svn.update': { sessionId?: string; path?: string }
|
|
451
|
+
'svn.updateStart': { sessionId?: string; path?: string }
|
|
452
|
+
'svn.updatePoll': { sessionId?: string; jobId: string; since: number }
|
|
453
|
+
'svn.updateCancel': { sessionId?: string; jobId: string }
|
|
454
|
+
'svn.updateActive': { sessionId?: string }
|
|
450
455
|
'svn.tortoise': { sessionId?: string; action: SvnAction; path?: string }
|
|
451
456
|
'svn.patchText': { sessionId?: string; path: string; whitespace?: 'none' | 'b' | 'w'; ignoreEol?: boolean; unified?: number }
|
|
452
457
|
'svn.diffSum': { sessionId?: string; path?: string; revA: number; revB: number }
|
|
@@ -519,6 +524,7 @@ export interface RpcOkMap {
|
|
|
519
524
|
'mcp.projectToggle': { project: MpcProject }
|
|
520
525
|
'mcp.projectRefresh': { project: MpcProject }
|
|
521
526
|
'vscode.fileOpenSettingsGet': { fileOpenTool: string; integrationBaseUrl: string; revision?: number }
|
|
527
|
+
'vscode.settingsDocumentPath': { path: string }
|
|
522
528
|
'vscode.fileOpenSettingsUpdate': { fileOpenTool: string; integrationBaseUrl: string; revision?: number }
|
|
523
529
|
'edrv.integration.status': ShellIntegrationStatus
|
|
524
530
|
'edrv.integration.register': ShellIntegrationStatus
|
|
@@ -595,6 +601,10 @@ export interface RpcOkMap {
|
|
|
595
601
|
'svn.wcRev': { revision: number | null }
|
|
596
602
|
'svn.cleanup': { summary: string; output: string }
|
|
597
603
|
'svn.update': SvnUpdateResult
|
|
604
|
+
'svn.updateStart': SvnUpdateState
|
|
605
|
+
'svn.updatePoll': SvnUpdatePoll
|
|
606
|
+
'svn.updateCancel': SvnUpdateState
|
|
607
|
+
'svn.updateActive': { job: SvnUpdateState | null }
|
|
598
608
|
'svn.tortoise': { launched: string }
|
|
599
609
|
'svn.patchText': { text: string; truncated: boolean; binary: boolean }
|
|
600
610
|
'svn.diffSum': { entries: SvnSumEntry[]; truncated: boolean }
|
package/src/shared/svn.ts
CHANGED
|
@@ -99,6 +99,44 @@ export interface SvnUpdateResult {
|
|
|
99
99
|
output: string
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
/** 原生更新窗口的真实执行阶段。 */
|
|
103
|
+
export type SvnUpdatePhase = 'starting' | 'running' | 'cancelling' | 'completed' | 'failed' | 'cancelled'
|
|
104
|
+
|
|
105
|
+
/** 流式更新行;relPath 仅在目标位于当前工作区内时可操作。 */
|
|
106
|
+
export interface SvnUpdateRow {
|
|
107
|
+
seq: number
|
|
108
|
+
action: SvnUpdateAction | null
|
|
109
|
+
path: string
|
|
110
|
+
relPath: string | null
|
|
111
|
+
label: string
|
|
112
|
+
raw: string
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** 分类数量按 svn 输出中的条目行计,不猜文件总数或网络字节。 */
|
|
116
|
+
export type SvnUpdateCounts = Record<SvnUpdateAction, number>
|
|
117
|
+
|
|
118
|
+
/** 更新任务快照(窗口第一次连接和每次增量 poll 共用)。 */
|
|
119
|
+
export interface SvnUpdateState {
|
|
120
|
+
jobId: string
|
|
121
|
+
target: string
|
|
122
|
+
phase: SvnUpdatePhase
|
|
123
|
+
counts: SvnUpdateCounts
|
|
124
|
+
revision: number | null
|
|
125
|
+
elapsedMs: number
|
|
126
|
+
error: string
|
|
127
|
+
rawTail: string
|
|
128
|
+
truncated: boolean
|
|
129
|
+
nextSeq: number
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 增量读取结果:rows 只含 since 之后的一页,offset 表示目前可重放的首条序号。 */
|
|
133
|
+
export interface SvnUpdatePoll extends SvnUpdateState {
|
|
134
|
+
rows: SvnUpdateRow[]
|
|
135
|
+
offset: number
|
|
136
|
+
/** 当前后台总序号;nextSeq 仅是本次前端已经拿到的最后一条。 */
|
|
137
|
+
totalSeq: number
|
|
138
|
+
}
|
|
139
|
+
|
|
102
140
|
/** revert/add 等批量动作结果(计数 + 摘要 + 原文)。 */
|
|
103
141
|
export interface SvnActionResult {
|
|
104
142
|
/** 处理条目数(宽松计数)。 */
|