dsh-vscode-mode 0.10.0 → 0.12.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 +62 -11
- package/lib/client.js +2879 -1030
- package/lib/client.js.map +1 -1
- package/lib/index.js +2227 -1056
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/activePathStore.ts +28 -0
- package/src/client/commandBridge.ts +9 -41
- package/src/client/dap/hover.ts +2 -7
- package/src/client/index.ts +116 -14
- package/src/client/keybindings.ts +19 -73
- 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/shortcutsOfficial.ts +451 -0
- package/src/client/sidebar/menuItems.ts +1 -0
- package/src/client/sidebar/panels/RulesPanel.ts +15 -11
- 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 +142 -135
- package/src/client/ui/McpSettings.ts +4 -5
- package/src/client/ui/QuickOpen.ts +10 -11
- package/src/client/ui/ShortcutSettings.ts +294 -0
- package/src/client/ui/SvnLogDialog.ts +200 -69
- package/src/client/ui/SvnUpdateDialog.ts +472 -0
- package/src/client/ui/commandCatalog.ts +36 -41
- package/src/client/ui/svnUpdateModel.ts +135 -0
- package/src/fileOpenSettings.ts +3 -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/rules.ts +100 -44
- package/src/settingsDocPath.ts +30 -0
- package/src/shared/keybindings.ts +253 -0
- package/src/shared/rpc.ts +12 -2
- package/src/shared/rules.ts +9 -1
- 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
- package/src/client/ui/KeybindingsPanel.ts +0 -190
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 编辑器指令目录(纯元数据)。
|
|
3
|
-
* 「一条命令 =
|
|
4
|
-
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS
|
|
3
|
+
* 「一条命令 = 一条注册数据」:命令栏、快捷键设置页与 Monaco 右键菜单
|
|
4
|
+
* 全部从本表读取;新增一条能力只需在 EDITOR_COMMANDS 追加一项(默认键位见 shared/keybindings SHORTCUT_PROFILES)。
|
|
5
5
|
* run 只派发 `edrv.command.*` 窗口事件,不直接触碰 React/Monaco(`edrv.command.` 前缀
|
|
6
6
|
* 与既有 `edrv:` 刷新/主题事件分属不同命名空间,互不干扰)。
|
|
7
7
|
* 作者 ddj 2026年09月10号
|
|
8
8
|
*/
|
|
9
9
|
import { hasEditorModel, hasOpenTabs } from '../editorModelState.js'
|
|
10
|
+
import { activeEditorPath } from '../activePathStore.js'
|
|
11
|
+
import { isMarkdownPath } from '../markdownPreview.js'
|
|
10
12
|
import { svnCurrentStatus } from '../svnStatus.js'
|
|
11
13
|
import { dapStore } from '../dap/store.js'
|
|
12
14
|
import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
|
|
@@ -22,8 +24,6 @@ export interface CommandDef {
|
|
|
22
24
|
category: string
|
|
23
25
|
/** 组内排序(小者优先)。 */
|
|
24
26
|
order?: number
|
|
25
|
-
/** 默认键位弦(可选;可含 `|` 多候选)。声明后由快捷键设置页展示与录制。 */
|
|
26
|
-
keybinding?: string
|
|
27
27
|
/** 运行体:派发窗口事件或直接执行。 */
|
|
28
28
|
run: () => void
|
|
29
29
|
/** 可用性判定(缺省视为始终可用;返回 false 时命令栏隐藏且 run 被拒)。 */
|
|
@@ -62,7 +62,7 @@ function alwaysAvailable(): boolean {
|
|
|
62
62
|
function nextEditorRowDef(): CommandDef {
|
|
63
63
|
return {
|
|
64
64
|
id: 'edrv.nextEditorRow', label: '下一编辑行(光标整行下移)', category: '导航', order: 50,
|
|
65
|
-
|
|
65
|
+
available: needsModel,
|
|
66
66
|
run: () => emit('nextEditorRow'),
|
|
67
67
|
}
|
|
68
68
|
}
|
|
@@ -75,7 +75,7 @@ function nextEditorRowDef(): CommandDef {
|
|
|
75
75
|
function prevEditorRowDef(): CommandDef {
|
|
76
76
|
return {
|
|
77
77
|
id: 'edrv.prevEditorRow', label: '上一编辑行(光标整行上移)', category: '导航', order: 60,
|
|
78
|
-
|
|
78
|
+
available: needsModel,
|
|
79
79
|
run: () => emit('prevEditorRow'),
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -88,7 +88,7 @@ function prevEditorRowDef(): CommandDef {
|
|
|
88
88
|
function gotoLineDef(): CommandDef {
|
|
89
89
|
return {
|
|
90
90
|
id: 'edrv.goToLine', label: '转到行', category: '导航', order: 45,
|
|
91
|
-
|
|
91
|
+
available: needsModel,
|
|
92
92
|
run: () => emit('goToLine'),
|
|
93
93
|
}
|
|
94
94
|
}
|
|
@@ -102,7 +102,7 @@ function gotoLineDef(): CommandDef {
|
|
|
102
102
|
function addSelectionRefDef(): CommandDef {
|
|
103
103
|
return {
|
|
104
104
|
id: 'edrv.addSelectionRef', label: '添加选中内容为引用', category: '编辑', order: 10,
|
|
105
|
-
|
|
105
|
+
available: needsModel,
|
|
106
106
|
run: () => emit('addSelectionRef'),
|
|
107
107
|
}
|
|
108
108
|
}
|
|
@@ -117,7 +117,7 @@ function addSelectionRefDef(): CommandDef {
|
|
|
117
117
|
function closeTabDef(): CommandDef {
|
|
118
118
|
return {
|
|
119
119
|
id: 'edrv.closeTab', label: '关闭当前页签', category: '文件', order: 40,
|
|
120
|
-
|
|
120
|
+
available: hasOpenTabs,
|
|
121
121
|
run: () => emit('closeTab'),
|
|
122
122
|
}
|
|
123
123
|
}
|
|
@@ -188,34 +188,39 @@ function svnEventOf(action: SvnActionDef): string {
|
|
|
188
188
|
|
|
189
189
|
/**
|
|
190
190
|
* 编辑器内置指令目录(顺序 = 快捷键设置页展示顺序)。
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* 键位派发已归官方 shortcuts 机制:默认键位表在 shared/keybindings.ts 的 SHORTCUT_PROFILES,
|
|
192
|
+
* 由 client/index.ts 经 shortcutsOfficial.registerOfficialShortcuts 注册,本目录只管
|
|
193
|
+
* 「标签/可用性/执行」。无默认键位的命令同样注册(官方 UI/插件子页内可自定义绑定)。
|
|
194
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
194
195
|
*/
|
|
195
196
|
export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
196
197
|
{
|
|
197
198
|
id: 'edrv.save', label: '保存文件', category: '文件', order: 10,
|
|
198
|
-
|
|
199
|
+
available: needsModel,
|
|
199
200
|
run: () => emit('save'),
|
|
200
201
|
},
|
|
201
202
|
{
|
|
202
203
|
id: 'edrv.quickOpen', label: '快速打开文件', category: '文件', order: 20,
|
|
203
|
-
|
|
204
|
+
available: alwaysAvailable,
|
|
204
205
|
run: () => emit('quickOpen'),
|
|
205
206
|
},
|
|
206
207
|
{
|
|
207
208
|
id: 'edrv.toggleSidebar', label: '切换侧边栏', category: '视图', order: 10,
|
|
208
|
-
|
|
209
|
+
available: alwaysAvailable,
|
|
209
210
|
run: () => emit('toggleSidebar'),
|
|
210
211
|
},
|
|
211
212
|
{
|
|
212
213
|
id: 'edrv.searchInFiles', label: '在工作区中搜索', category: '视图', order: 20,
|
|
213
|
-
|
|
214
|
+
available: alwaysAvailable,
|
|
214
215
|
run: () => emit('searchInFiles'),
|
|
215
216
|
},
|
|
216
217
|
{
|
|
217
218
|
id: 'edrv.toggleMarkdownPreview', label: '切换 Markdown 预览', category: '视图', order: 25,
|
|
218
|
-
|
|
219
|
+
// 仅活动文件是 .md 时可用(不可用时官方派发返回 pass 不吞键,保留浏览器「无格式粘贴」语义)
|
|
220
|
+
available: () => {
|
|
221
|
+
const path = activeEditorPath()
|
|
222
|
+
return typeof path === 'string' && isMarkdownPath(path)
|
|
223
|
+
},
|
|
219
224
|
run: () => emit('toggleMarkdownPreview'),
|
|
220
225
|
},
|
|
221
226
|
{
|
|
@@ -225,22 +230,22 @@ export const EDITOR_COMMANDS: readonly CommandDef[] = [
|
|
|
225
230
|
},
|
|
226
231
|
{
|
|
227
232
|
id: 'edrv.navigateBack', label: '后退(导航历史)', category: '导航', order: 10,
|
|
228
|
-
|
|
233
|
+
available: needsModel,
|
|
229
234
|
run: () => emit('navigateBack'),
|
|
230
235
|
},
|
|
231
236
|
{
|
|
232
237
|
id: 'edrv.navigateForward', label: '前进(导航历史)', category: '导航', order: 20,
|
|
233
|
-
|
|
238
|
+
available: needsModel,
|
|
234
239
|
run: () => emit('navigateForward'),
|
|
235
240
|
},
|
|
236
241
|
{
|
|
237
242
|
id: 'edrv.nextTab', label: '下一个页签', category: '导航', order: 30,
|
|
238
|
-
|
|
243
|
+
available: needsModel,
|
|
239
244
|
run: () => emit('nextTab'),
|
|
240
245
|
},
|
|
241
246
|
{
|
|
242
247
|
id: 'edrv.prevTab', label: '上一个页签', category: '导航', order: 40,
|
|
243
|
-
|
|
248
|
+
available: needsModel,
|
|
244
249
|
run: () => emit('prevTab'),
|
|
245
250
|
},
|
|
246
251
|
{
|
|
@@ -291,41 +296,41 @@ function debugPaletteDefs(): CommandDef[] {
|
|
|
291
296
|
return [
|
|
292
297
|
{
|
|
293
298
|
id: 'edrv.debugToggleBreakpoint', label: '调试:切换断点(光标行)', category: '调试', order: 10,
|
|
294
|
-
|
|
299
|
+
available: needsModel,
|
|
295
300
|
run: () => emit('debugToggleBreakpoint'),
|
|
296
301
|
},
|
|
297
302
|
{
|
|
298
303
|
id: 'edrv.debugStartContinue', label: '调试:启动 / 继续', category: '调试', order: 20,
|
|
299
|
-
|
|
304
|
+
available: canStart,
|
|
300
305
|
run: () => emit('debugStartContinue'),
|
|
301
306
|
},
|
|
302
307
|
{
|
|
303
308
|
id: 'edrv.debugStepOver', label: '调试:单步跳过', category: '调试', order: 30,
|
|
304
|
-
|
|
309
|
+
available: paused,
|
|
305
310
|
run: () => emit('debugStepOver'),
|
|
306
311
|
},
|
|
307
312
|
{
|
|
308
313
|
id: 'edrv.debugStepInto', label: '调试:单步步入', category: '调试', order: 40,
|
|
309
|
-
|
|
314
|
+
available: paused,
|
|
310
315
|
run: () => emit('debugStepInto'),
|
|
311
316
|
},
|
|
312
317
|
{
|
|
313
318
|
id: 'edrv.debugStepOut', label: '调试:单步步出', category: '调试', order: 50,
|
|
314
|
-
|
|
319
|
+
available: paused,
|
|
315
320
|
run: () => emit('debugStepOut'),
|
|
316
321
|
},
|
|
317
322
|
{
|
|
318
323
|
id: 'edrv.debugStop', label: '调试:停止', category: '调试', order: 60,
|
|
319
|
-
|
|
324
|
+
available: canStop,
|
|
320
325
|
run: () => emit('debugStop'),
|
|
321
326
|
},
|
|
322
327
|
]
|
|
323
328
|
}
|
|
324
329
|
|
|
325
330
|
/**
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
-
* @author ddj 2026年09月10号
|
|
331
|
+
* 非编辑器原生路径的指令(键位统一由官方 shortcuts 机制派发;历史「桥接」分组名保留)。
|
|
332
|
+
* 新增「只填目录」的编辑器指令一律放这里:命令栏与官方快捷键目录自动可用。
|
|
333
|
+
* @author ddj 2026年09月10号 / 2026年10月
|
|
329
334
|
*/
|
|
330
335
|
export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
331
336
|
nextEditorRowDef(),
|
|
@@ -345,16 +350,6 @@ export const BRIDGE_COMMANDS: readonly CommandDef[] = [
|
|
|
345
350
|
export function showCommandsDef(run: () => void): CommandDef {
|
|
346
351
|
return {
|
|
347
352
|
id: 'edrv.showCommands', label: '显示所有命令', category: '视图', order: 1,
|
|
348
|
-
|
|
353
|
+
available: alwaysAvailable, run,
|
|
349
354
|
}
|
|
350
355
|
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* 需要全局键位派发的指令(桥接类 + 命令栏自身;不含 EditorView/QuickOpen 原生监听的那些)。
|
|
354
|
-
* @author ddj 2026年09月10号
|
|
355
|
-
* @param showCommands 命令栏命令定义
|
|
356
|
-
* @returns 派发集合
|
|
357
|
-
*/
|
|
358
|
-
export function dispatchedCommands(showCommands: CommandDef): readonly CommandDef[] {
|
|
359
|
-
return [...BRIDGE_COMMANDS, showCommands]
|
|
360
|
-
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — SVN 更新窗口的纯视图逻辑。
|
|
3
|
+
* @author ddj 2026年09月24号
|
|
4
|
+
*/
|
|
5
|
+
import type { SvnUpdatePhase, SvnUpdateRow } from '../../shared/svn.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 只有真实终态允许直接关闭窗口。
|
|
9
|
+
* @author ddj 2026年09月24号
|
|
10
|
+
* @param phase 任务阶段
|
|
11
|
+
* @returns 是否结束
|
|
12
|
+
*/
|
|
13
|
+
export function updateDone(phase: SvnUpdatePhase | undefined): boolean {
|
|
14
|
+
return phase === 'completed' || phase === 'failed' || phase === 'cancelled'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 后端结束并不等于前端已取完全部结果页。
|
|
19
|
+
* @author ddj 2026年09月24号
|
|
20
|
+
* @param phase 后台状态
|
|
21
|
+
* @param pending 尚有未加载的明细
|
|
22
|
+
* @returns 结果界面是否完成
|
|
23
|
+
*/
|
|
24
|
+
export function updateReady(phase: SvnUpdatePhase | undefined, pending: boolean): boolean {
|
|
25
|
+
return updateDone(phase) && !pending
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 取消或异常终止会在工作副本上留下 SVN 锁(E155037/E155004);
|
|
30
|
+
* 只按失败态原文判定,不推测其他失败的原因。
|
|
31
|
+
* @author ddj 2026年09月28号
|
|
32
|
+
* @param state 任务快照
|
|
33
|
+
* @returns 是否需要先执行 cleanup
|
|
34
|
+
*/
|
|
35
|
+
export function updateLocked(state: { phase?: SvnUpdatePhase; error?: string; rawTail?: string } | null | undefined): boolean {
|
|
36
|
+
if (!state || state.phase !== 'failed') return false
|
|
37
|
+
const text = (state.error ?? '') + '\n' + (state.rawTail ?? '')
|
|
38
|
+
return /E155037|E155004|Previous operation has not finished|run 'cleanup'|Working copy .*locked|工作副本.*锁/i.test(text)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 菜单/二级对话框按 Esc 只关闭自己;连接中断后允许显式暂时关闭。
|
|
43
|
+
* @author ddj 2026年09月24号
|
|
44
|
+
* @param ready 已取完后台结果
|
|
45
|
+
* @param error 连接错误
|
|
46
|
+
* @param hasJob 是否知道后台 job
|
|
47
|
+
* @param overlay 子弹窗或菜单是否打开
|
|
48
|
+
* @returns 自动/显式关闭及确认要求
|
|
49
|
+
*/
|
|
50
|
+
export function updateCloseMode(ready: boolean, error: string, hasJob: boolean, overlay: boolean) {
|
|
51
|
+
const disconnected = Boolean(error && hasJob && !ready)
|
|
52
|
+
return {
|
|
53
|
+
passive: (ready || Boolean(error && !hasJob)) && !overlay,
|
|
54
|
+
explicit: ready || Boolean(error),
|
|
55
|
+
confirm: disconnected,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 窗口只能在发起它的同一会话、同一工作区作用域中挂载。
|
|
61
|
+
* @author ddj 2026年09月24号
|
|
62
|
+
* @param dialog 发起窗口时记录的身份
|
|
63
|
+
* @param sessionId 当前会话
|
|
64
|
+
* @param scope 当前工作区作用域
|
|
65
|
+
* @returns 是否允许挂载
|
|
66
|
+
*/
|
|
67
|
+
export function sameUpdateOwner(dialog: { sessionId?: string; scope?: string | null } | null,
|
|
68
|
+
sessionId?: string, scope?: string | null): boolean {
|
|
69
|
+
return Boolean(dialog && dialog.sessionId === sessionId && dialog.scope === scope)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 为数千条结果取可视区和小幅缓冲,行高由界面固定约束。
|
|
74
|
+
* @author ddj 2026年09月24号
|
|
75
|
+
* @param count 当前记录数
|
|
76
|
+
* @param scrollTop 列表滚动偏移
|
|
77
|
+
* @param height 列表高度
|
|
78
|
+
* @param rowHeight 固定行高
|
|
79
|
+
* @returns 可见的起止索引(尾部不含)
|
|
80
|
+
*/
|
|
81
|
+
export function updateRange(count: number, scrollTop: number, height: number, rowHeight: number): { start: number; end: number } {
|
|
82
|
+
const start = Math.min(count, Math.max(0, Math.floor(Math.max(0, scrollTop) / rowHeight) - 8))
|
|
83
|
+
const end = Math.min(count, Math.ceil((Math.max(0, scrollTop) + Math.max(0, height)) / rowHeight) + 8)
|
|
84
|
+
return { start, end }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* 输出不等于文件真实类型;只给已知扩展名提供可辨识的类型推断。
|
|
89
|
+
* @author ddj 2026年09月24号
|
|
90
|
+
* @param path SVN 输出路径
|
|
91
|
+
* @returns 推断类型,未知为空
|
|
92
|
+
*/
|
|
93
|
+
export function updateType(path: string): string {
|
|
94
|
+
const ext = path.split('.').pop()?.toLowerCase()
|
|
95
|
+
const known: Record<string, string> = {
|
|
96
|
+
bytes: 'application/octet-stream', png: 'image/png', jpg: 'image/jpeg',
|
|
97
|
+
jpeg: 'image/jpeg', webp: 'image/webp', gif: 'image/gif', pdf: 'application/pdf',
|
|
98
|
+
}
|
|
99
|
+
return ext ? known[ext] ?? '' : ''
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* 行动作是否可用取决于更新是否结束、路径是否在工作区且磁盘是否有文件。
|
|
104
|
+
* @author ddj 2026年09月24号
|
|
105
|
+
* @param row 更新行
|
|
106
|
+
* @param phase 任务阶段
|
|
107
|
+
* @param kind 磁盘判型;未读到时仅开放父目录/复制
|
|
108
|
+
* @param revision 完成修订号
|
|
109
|
+
* @returns 菜单动作集合
|
|
110
|
+
*/
|
|
111
|
+
export function updateActs(row: SvnUpdateRow, phase: SvnUpdatePhase, kind: string, revision: number | null) {
|
|
112
|
+
const valid = updateDone(phase) && Boolean(row.relPath)
|
|
113
|
+
const file = valid && kind === 'file' && row.action !== 'D'
|
|
114
|
+
const binary = Boolean(updateType(row.path))
|
|
115
|
+
return {
|
|
116
|
+
compare: file && Boolean(revision) && !binary,
|
|
117
|
+
log: file,
|
|
118
|
+
open: file,
|
|
119
|
+
openWith: file,
|
|
120
|
+
parent: valid,
|
|
121
|
+
copy: Boolean(row.path),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 目录上下文使用工作区相对路径,根文件的父目录表示为工作区根。
|
|
127
|
+
* @author ddj 2026年09月24号
|
|
128
|
+
* @param path 工作区相对路径
|
|
129
|
+
* @returns 父目录相对路径
|
|
130
|
+
*/
|
|
131
|
+
export function updateParent(path: string): string {
|
|
132
|
+
const normalized = path.replace(/\\/g, '/')
|
|
133
|
+
const at = normalized.lastIndexOf('/')
|
|
134
|
+
return at < 0 ? '' : normalized.slice(0, at)
|
|
135
|
+
}
|
package/src/fileOpenSettings.ts
CHANGED
|
@@ -407,6 +407,9 @@ export async function updateSection(provider: SettingsProvider | undefined, ns:
|
|
|
407
407
|
* 构建设置 section schema(install 路径与插件 Config 声明共用,保证两代形状同源)。
|
|
408
408
|
* 字段集 = fileOpenTool/keybindings/sidebarMinWidth/maxOpenEditors/integrationBaseUrl/
|
|
409
409
|
* aiInline/aiProvider/aiModel/aiEffort/svnPath/tortoisePath/nativeOpenExts,全部带默认值
|
|
410
|
+
* (keybindings 自 2026-10 起废弃停写:键位持久化归官方 shortcuts 存储
|
|
411
|
+
* userData/keybindings.json / dsh.keybindings.v1;字段保留仅为旧档迁移读取与降级回滚,
|
|
412
|
+
* 见 client/shortcutsOfficial.ts 的 migrateLegacyKeybindings。)
|
|
410
413
|
* (Config 启动校验在 undefined/空配置下自动填充,rc/alpha 两代 cordis 均通过)。
|
|
411
414
|
* options.volatile:DSH 0.1.7 线的 Config 导出专用——SettingsForms.describe 只下发
|
|
412
415
|
* 含 volatile 字段的 entry(volatileForm 门槛),不标记则 ns 永不出现在 describe、
|
package/src/index.ts
CHANGED
|
@@ -123,6 +123,8 @@ export function apply(ctx: Ctx, config?: unknown): void {
|
|
|
123
123
|
})
|
|
124
124
|
// 卸载时清空文件版本基准表(观察器为模块内单例,不清会跨装配残留陈旧版本)
|
|
125
125
|
ctx.effect(() => () => fileVersions.dispose())
|
|
126
|
+
// 卸载时结束仍在运行的 SVN 更新,避免无主工作副本更新继续写入
|
|
127
|
+
ctx.effect(() => () => svnRpc.dispose())
|
|
126
128
|
// 卸载/重启时结束调试会话并强杀适配器子进程(防残留注入器/适配器孤儿)
|
|
127
129
|
ctx.effect(() => () => dapRpc.dispose())
|
|
128
130
|
// 宿主进程退出回收:ctx.effect 清理不覆盖进程退出,缺此注册会留下跨重启的孤儿服务器
|
|
@@ -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
|
},
|