dsh-vscode-mode 0.4.6 → 0.5.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 +32 -2
- package/lib/client.js +8854 -4023
- package/lib/client.js.map +1 -1
- package/lib/index.js +1842 -65
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/client/index.ts +3 -0
- package/src/client/monaco/lsp/providers.ts +28 -6
- package/src/client/navHighlight.ts +96 -0
- package/src/client/paths.ts +2 -0
- package/src/client/rpc.ts +32 -12
- package/src/client/sidebar/SidebarView.ts +6 -2
- package/src/client/sidebar/menuItems.ts +93 -2
- package/src/client/sidebar/panels/FileExplorer.ts +18 -1
- package/src/client/sidebar/panels/SvnPanel.ts +303 -0
- package/src/client/sidebar/panels/index.ts +44 -3
- package/src/client/sidebar/types.ts +20 -0
- package/src/client/styles/editor.css +177 -0
- package/src/client/svnActions.ts +153 -0
- package/src/client/svnLog.ts +356 -0
- package/src/client/svnStatus.ts +363 -0
- package/src/client/svnStore.ts +150 -0
- package/src/client/tabMenu.ts +51 -0
- package/src/client/ui/EditorView.ts +583 -24
- package/src/client/ui/LogDialog.ts +333 -0
- package/src/client/ui/McpSettings.ts +98 -0
- package/src/client/ui/ModalShell.ts +79 -0
- package/src/client/ui/SideBySideDiff.tsx +139 -0
- package/src/client/ui/SnippetsPicker.ts +70 -58
- package/src/client/ui/SvnDiffPanel.ts +50 -0
- package/src/client/ui/SvnLogDialog.ts +1141 -0
- package/src/client/ui/SvnLogStats.ts +243 -0
- package/src/client/ui/commandCatalog.ts +72 -0
- package/src/debugLog.ts +81 -12
- package/src/fileOpenSettings.ts +40 -3
- package/src/index.ts +5 -1
- package/src/rpc.ts +41 -4
- package/src/shared/rpc.ts +53 -1
- package/src/shared/svn.ts +377 -0
- package/src/shared/svnActions.ts +296 -0
- package/src/svn.ts +870 -0
- package/src/svnLog.ts +315 -0
- package/src/svnText.ts +175 -0
- package/src/svnXml.ts +110 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 DSH 0.1.5+ 官方右侧 Sidebar 与对话同屏)+ 命令栏(Ctrl+Shift+P)与可扩展指令系统 + VS Code 兼容代码片段(.code-snippets 全局/项目,IntelliSense 展开)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ SVN 集成(侧栏变更面板/日志弹窗/并排差异)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"dsh-plugin",
|
package/src/client/index.ts
CHANGED
|
@@ -40,6 +40,7 @@ import { createSidebarPanelRegistry } from './sidebar/registry.js'
|
|
|
40
40
|
import { createFilePanel } from './sidebar/panels/index.js'
|
|
41
41
|
import { createSearchPanel } from './sidebar/panels/index.js'
|
|
42
42
|
import { createRulesPanel } from './sidebar/panels/index.js'
|
|
43
|
+
import { createSvnPanel } from './sidebar/panels/index.js'
|
|
43
44
|
import { createTreeMenuRegistry } from './sidebar/contextMenu.js'
|
|
44
45
|
import { createDefaultFileMenuItems } from './sidebar/menuItems.js'
|
|
45
46
|
import { createOutlinePanel } from './outline/index.js'
|
|
@@ -196,6 +197,8 @@ export function apply(ctx: any): void {
|
|
|
196
197
|
ctx.effect(() => sidebarPanels.register(createSearchPanel()), 'vscode-mode: sidebar panel search')
|
|
197
198
|
// 规则面板:活动栏「规则」页签(Codebuddy 规则管理形态:用户/项目规则 + 启用开关,host 注入生效)
|
|
198
199
|
ctx.effect(() => sidebarPanels.register(createRulesPanel()), 'vscode-mode: sidebar panel rules')
|
|
200
|
+
// SVN 变更面板:活动栏「SVN 变更」页签(工作副本状态列表;visible 守卫使非 SVN 工作区不出现)
|
|
201
|
+
ctx.effect(() => sidebarPanels.register(createSvnPanel()), 'vscode-mode: sidebar panel svn')
|
|
199
202
|
// 文件右键菜单项注册表(对外 provide,供本插件/第三方注册;内置「在文件浏览器中打开」)
|
|
200
203
|
const fileMenuItems = createTreeMenuRegistry()
|
|
201
204
|
ctx.provide('edrvFileContextMenuItems', fileMenuItems)
|
|
@@ -24,8 +24,33 @@ let registered = false
|
|
|
24
24
|
const disposables = []
|
|
25
25
|
|
|
26
26
|
/** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
|
|
27
|
-
function openAt(path,
|
|
28
|
-
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: { path
|
|
27
|
+
function openAt(path, target) {
|
|
28
|
+
window.dispatchEvent(new CustomEvent('edrv:open-editor', { detail: Object.assign({ path }, target) }))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 解析 Monaco opener 第三参(目标位置)为 1-based 区间。
|
|
33
|
+
*
|
|
34
|
+
* 为什么要兼容多形态:Monaco 经官方桥 `registerEditorOpener` 传出的是
|
|
35
|
+
* `options.selection`(Range,含 getStartPosition)或其**降级形态**裸
|
|
36
|
+
* `{ lineNumber, column }`(官方桥在 Range 缺 endLineNumber/endColumn 时降级),
|
|
37
|
+
* 另有 `editor.action.goToLocations` 完全不传 options(第三参 undefined)。
|
|
38
|
+
* 三种形态都出现过,逐形态取值并对缺字段兜底,避免落点退化成第 1 行。
|
|
39
|
+
* @author ddj 2026年09月17号
|
|
40
|
+
* @param selectionOrPosition opener 第三参(Range / 裸位置 / undefined)
|
|
41
|
+
* @returns 1-based 目标区间(end 缺省等于 start,且保证 end >= start)
|
|
42
|
+
*/
|
|
43
|
+
export function navTargetOf(selectionOrPosition) {
|
|
44
|
+
const src = selectionOrPosition ?? {}
|
|
45
|
+
const start = typeof src.getStartPosition === 'function' ? src.getStartPosition() : src
|
|
46
|
+
const end = typeof src.getEndPosition === 'function' ? src.getEndPosition() : null
|
|
47
|
+
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
48
|
+
const column = Math.max(1, start?.column ?? 1)
|
|
49
|
+
const rawEndLine = end?.lineNumber ?? src.endLineNumber
|
|
50
|
+
const rawEndColumn = end?.column ?? src.endColumn
|
|
51
|
+
const endLine = Math.max(line, rawEndLine != null ? Math.max(1, rawEndLine) : line)
|
|
52
|
+
const endColumn = rawEndColumn != null ? Math.max(1, rawEndColumn) : undefined
|
|
53
|
+
return endColumn === undefined ? { line, column, endLine } : { line, column, endLine, endColumn }
|
|
29
54
|
}
|
|
30
55
|
|
|
31
56
|
/** 注册全部 Monaco LSP provider 与文档跟踪(幂等)。 */
|
|
@@ -74,10 +99,7 @@ export function registerLspProviders(monaco) {
|
|
|
74
99
|
openCodeEditor: (source, resource, selectionOrPosition) => {
|
|
75
100
|
const path = lspUriToAbs(resource)
|
|
76
101
|
if (!path) return false
|
|
77
|
-
|
|
78
|
-
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
79
|
-
const column = Math.max(1, start?.column ?? 1)
|
|
80
|
-
openAt(path, line, column)
|
|
102
|
+
openAt(path, navTargetOf(selectionOrPosition))
|
|
81
103
|
return true
|
|
82
104
|
},
|
|
83
105
|
}),
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 跳转目标高亮区间推导(纯函数,不依赖 monaco/DOM)。
|
|
3
|
+
*
|
|
4
|
+
* 背景:Monaco 原生跳转在 resolve 出定义后会把选区 `collapseToStart` 再交给 opener,
|
|
5
|
+
* 于是插件拿到的是**零宽区间**(start === end)。零宽 range 挂装饰渲染不出任何可见
|
|
6
|
+
* 高亮(实测 Ctrl+点击枚举成员后 `.edrv-nav-target` 存在但无视觉效果)。因此必须把
|
|
7
|
+
* 零宽识别为「无区间」,并逐级回落:opener 区间 → 光标处单词 → 整行。
|
|
8
|
+
* 抽成纯函数便于单测(EditorView 是 React 组件,node 侧不可直接引入)。
|
|
9
|
+
* 作者 ddj 2026-09-17
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** 推导所需的最小 model 能力(Monaco ITextModel 子集,测试可桩)。 */
|
|
13
|
+
export interface NavFlashModel {
|
|
14
|
+
getLineCount(): number
|
|
15
|
+
getLineMaxColumn(lineNumber: number): number
|
|
16
|
+
/**
|
|
17
|
+
* Monaco 实返 `{ word, startColumn, endColumn }` —— **不含** startLineNumber/endLineNumber。
|
|
18
|
+
* 类型里把行号标为可选,正是为了强制调用方用当前行补齐(曾因直接透传 undefined
|
|
19
|
+
* 行号产出非法装饰区间,装饰已挂但渲染不出任何高亮)。
|
|
20
|
+
*/
|
|
21
|
+
getWordAtPosition(pos: { lineNumber: number; column: number }):
|
|
22
|
+
| {
|
|
23
|
+
word?: string
|
|
24
|
+
startColumn: number
|
|
25
|
+
endColumn: number
|
|
26
|
+
startLineNumber?: number
|
|
27
|
+
endLineNumber?: number
|
|
28
|
+
}
|
|
29
|
+
| null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 跳转目标(1-based;endLine/endColumn 缺省表示未提供区间)。 */
|
|
33
|
+
export interface NavFlashTarget {
|
|
34
|
+
line?: number
|
|
35
|
+
column?: number
|
|
36
|
+
endLine?: number | null
|
|
37
|
+
endColumn?: number | null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 高亮区间(Monaco IRange 形状,1-based)。 */
|
|
41
|
+
export interface NavFlashRange {
|
|
42
|
+
startLineNumber: number
|
|
43
|
+
startColumn: number
|
|
44
|
+
endLineNumber: number
|
|
45
|
+
endColumn: number
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 推导跳转目标的高亮区间。
|
|
50
|
+
*
|
|
51
|
+
* 逐级回落:① opener 提供的**非零宽**区间(原样夹到文档范围);
|
|
52
|
+
* ② 光标处单词(LSP 折叠选区的常态,高亮标识符本身最易辨认);
|
|
53
|
+
* ③ 整行(符号位置无单词可取时的兜底,保证一定可见)。
|
|
54
|
+
* @author ddj 2026年09月17号
|
|
55
|
+
* @param model 文本模型(或等价桩)
|
|
56
|
+
* @param target 跳转目标
|
|
57
|
+
* @returns 可渲染的高亮区间;model 不可用时返回 null
|
|
58
|
+
*/
|
|
59
|
+
export function navFlashRangeOf(model: NavFlashModel | null | undefined, target: NavFlashTarget | null | undefined): NavFlashRange | null {
|
|
60
|
+
if (!model || !target) return null
|
|
61
|
+
const lineCount = model.getLineCount()
|
|
62
|
+
if (!Number.isFinite(lineCount) || lineCount < 1) return null
|
|
63
|
+
const startLine = Math.max(1, Math.min(lineCount, Math.floor(target.line ?? 1) || 1))
|
|
64
|
+
const startColumn = Math.min(Math.max(1, Math.floor(target.column ?? 1) || 1), model.getLineMaxColumn(startLine))
|
|
65
|
+
|
|
66
|
+
const endLineRaw = target.endLine == null ? null : Math.floor(target.endLine)
|
|
67
|
+
const endColumnRaw = target.endColumn == null ? null : Math.floor(target.endColumn)
|
|
68
|
+
// 零宽(start === end)与缺字段一并视为「无区间」:零宽装饰不可见,必须回落
|
|
69
|
+
const hasRange = endLineRaw != null && endColumnRaw != null &&
|
|
70
|
+
(endLineRaw > startLine || endColumnRaw > startColumn)
|
|
71
|
+
if (hasRange) {
|
|
72
|
+
const endLineNumber = Math.max(startLine, Math.min(lineCount, endLineRaw))
|
|
73
|
+
const endColumn = Math.max(1, Math.min(model.getLineMaxColumn(endLineNumber), endColumnRaw))
|
|
74
|
+
return { startLineNumber: startLine, startColumn, endLineNumber, endColumn }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const word = model.getWordAtPosition({ lineNumber: startLine, column: startColumn })
|
|
78
|
+
if (word && word.endColumn > word.startColumn) {
|
|
79
|
+
// ⚠️ Monaco getWordAtPosition 不返回行号,必须用当前行补齐;直接透传 undefined
|
|
80
|
+
// 会产出 startLineNumber=undefined 的非法区间——装饰挂上了却渲染不出高亮(实测踩坑)。
|
|
81
|
+
const wordStartLine = word.startLineNumber ?? startLine
|
|
82
|
+
const wordEndLine = word.endLineNumber ?? startLine
|
|
83
|
+
return {
|
|
84
|
+
startLineNumber: wordStartLine,
|
|
85
|
+
startColumn: word.startColumn,
|
|
86
|
+
endLineNumber: wordEndLine,
|
|
87
|
+
endColumn: word.endColumn,
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
startLineNumber: startLine,
|
|
92
|
+
startColumn: 1,
|
|
93
|
+
endLineNumber: startLine,
|
|
94
|
+
endColumn: model.getLineMaxColumn(startLine),
|
|
95
|
+
}
|
|
96
|
+
}
|
package/src/client/paths.ts
CHANGED
|
@@ -28,6 +28,8 @@ export const CACHE_KEY = {
|
|
|
28
28
|
workspaceFold: 'edrv.ws-fold.v1.',
|
|
29
29
|
/** 规则面板 UI 状态(RulesPanel v1:Tab 与项目工作区选择,全局不按会话)。 */
|
|
30
30
|
rules: 'edrv.rules.v1.',
|
|
31
|
+
/** SVN 变更面板 UI 状态(SvnPanel v1:未版本控制/忽略项显示开关,按作用域隔离)。 */
|
|
32
|
+
svn: 'edrv.svn.v1.',
|
|
31
33
|
} as const
|
|
32
34
|
|
|
33
35
|
/** 按前缀拼作用域键(scopeStore 产出作用域;侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|
package/src/client/rpc.ts
CHANGED
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 类型化 RPC 包装 + 诊断日志。
|
|
3
3
|
* 迁移自原 src/client/index.ts 的 rpc/dbg,语义不改。
|
|
4
|
-
*
|
|
4
|
+
* 诊断开关运行时化:内存缓存 + localStorage 持久(日志弹窗开关即改即生效,免刷新)。
|
|
5
|
+
* 作者 ddj 2026-08-20 / 2026-09-17
|
|
5
6
|
*/
|
|
6
7
|
import { RPC_PATH } from '../shared/rpc.js'
|
|
7
8
|
import type { RpcMethod, RpcRequestMap, RpcResult } from '../shared/rpc.js'
|
|
9
|
+
import type { LoggerLevel } from '../shared/logger.js'
|
|
8
10
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
* host 端 debug 文件全量读改写(上限 512KB),高频渲染下开销可观。
|
|
12
|
-
* 排查问题时在浏览器控制台执行 localStorage.setItem('edrv.debug','1') 后刷新开启。
|
|
13
|
-
* @author ddj 2026年08月26号
|
|
14
|
-
*/
|
|
15
|
-
const DEBUG = (() => {
|
|
11
|
+
/** 诊断开关内存缓存(localStorage 'edrv.debug' 持久;热路径读内存,零 localStorage 开销)。 */
|
|
12
|
+
let debugEnabled = (() => {
|
|
16
13
|
try { return localStorage.getItem('edrv.debug') === '1' } catch (e) { return false }
|
|
17
14
|
})()
|
|
18
15
|
|
|
16
|
+
/**
|
|
17
|
+
* 诊断日志开关当前值。
|
|
18
|
+
* @author ddj 2026年09月17号
|
|
19
|
+
* @returns 是否开启
|
|
20
|
+
*/
|
|
21
|
+
export function isDebug(): boolean {
|
|
22
|
+
return debugEnabled
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 设置诊断日志开关(立即生效;localStorage 持久,写失败仅内存生效不抛错)。
|
|
27
|
+
* @author ddj 2026年09月17号
|
|
28
|
+
* @param on 是否开启
|
|
29
|
+
*/
|
|
30
|
+
export function setDebugEnabled(on: boolean): void {
|
|
31
|
+
debugEnabled = on
|
|
32
|
+
try {
|
|
33
|
+
if (on) localStorage.setItem('edrv.debug', '1')
|
|
34
|
+
else localStorage.removeItem('edrv.debug')
|
|
35
|
+
} catch (e) { /* 无 localStorage 环境:仅内存生效 */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
19
38
|
/** 同源 RPC 调用(host /edrv/rpc 精确路由)。 */
|
|
20
39
|
export function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Promise<RpcResult<M>> {
|
|
21
40
|
return fetch(RPC_PATH, {
|
|
@@ -27,13 +46,14 @@ export function rpc<M extends RpcMethod>(method: M, args: RpcRequestMap[M]): Pro
|
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
48
|
* 诊断日志:开关关闭时直接返回(零 RPC 零落盘);开启时走 host edrv.debug 落盘。
|
|
30
|
-
* @author ddj 2026年08月26号
|
|
49
|
+
* @author ddj 2026年08月26号 / 2026年09月17号
|
|
31
50
|
* @param sessionId 会话 id
|
|
32
51
|
* @param text 日志文本
|
|
52
|
+
* @param level 日志级别(缺省 debug;落盘行与 host 终端回显均带此级别)
|
|
33
53
|
*/
|
|
34
|
-
export function dbg(sessionId: string | undefined, text: string): void {
|
|
35
|
-
if (!
|
|
54
|
+
export function dbg(sessionId: string | undefined, text: string, level: LoggerLevel = 'debug'): void {
|
|
55
|
+
if (!debugEnabled) return
|
|
36
56
|
try {
|
|
37
|
-
rpc('edrv.debug', { sessionId, text }).catch(() => {})
|
|
57
|
+
rpc('edrv.debug', { sessionId, text, level }).catch(() => {})
|
|
38
58
|
} catch (e) { /* 日志失败忽略 */ }
|
|
39
59
|
}
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* 右侧详情面板区可拖拽调宽/隐藏:visible=false 时仅保留图标列(宽 = 图标列宽),
|
|
6
6
|
* 点击任一图标重新展开面板(onShow),底部按钮在 ◀ 收起 / ▶ 展开间切换。
|
|
7
7
|
* 拖拽调宽下限 = minWidth prop(通用设置 sidebarMinWidth,默认 300),低于下限直接收起面板区。
|
|
8
|
-
*
|
|
8
|
+
* 面板可按 visible(ctx) 声明可用性(如「SVN 变更」仅在受 SVN 管理时出现,非 SVN 工作区零变化)。
|
|
9
|
+
* 作者 ddj 2026-08-26 / 2026-09-04 / 2026-09-16
|
|
9
10
|
*/
|
|
10
11
|
import React from 'react'
|
|
11
12
|
import type { SidebarCtx } from './types.js'
|
|
@@ -47,8 +48,11 @@ function railIconEl(icon) {
|
|
|
47
48
|
* @param props.minWidth 最小宽度(通用设置 sidebarMinWidth;拖拽低于它触发隐藏)
|
|
48
49
|
*/
|
|
49
50
|
export function SidebarView(props) {
|
|
50
|
-
const panels = props.registry?.list?.() ?? []
|
|
51
51
|
const ctx = props.ctx
|
|
52
|
+
// 按 visible(ctx) 过滤活动栏图标与面板(能力相关面板:SVN 变更仅在受 SVN 管理时出现)
|
|
53
|
+
const panels = (props.registry?.list?.() ?? []).filter((panel) => (
|
|
54
|
+
typeof panel.visible !== 'function' || panel.visible(ctx) === true
|
|
55
|
+
))
|
|
52
56
|
const activePanel = props.activePanel
|
|
53
57
|
const onActive = props.onActive
|
|
54
58
|
const visible = props.visible !== false
|
|
@@ -1,13 +1,103 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 文件管理右键菜单内置项。
|
|
3
3
|
* 内置项:「在文件浏览器中打开」(文件→OS Explorer 定位选中、目录→打开目录);
|
|
4
|
-
*
|
|
4
|
+
* 「添加引用到对话」(文件/文件夹引用注入当前会话对话输入框);
|
|
5
|
+
* 「SVN 组」——动作清单与显隐规则来自 shared/svnActions.ts(三入口共用一份,不再各写一遍)。
|
|
5
6
|
* 反馈统一走 ctx.notify(由 EditorView 提供,落到编辑区路径栏状态)。
|
|
6
|
-
* 作者 ddj 2026-08-27 / 2026-09-03
|
|
7
|
+
* 作者 ddj 2026-08-27 / 2026-09-03 / 2026-09-16
|
|
7
8
|
*/
|
|
9
|
+
import { isSvnDiffable } from '../../shared/svn.js'
|
|
10
|
+
import type { SvnChangeEntry } from '../../shared/svn.js'
|
|
11
|
+
import { svnActionOn, svnActionsFor } from '../../shared/svnActions.js'
|
|
12
|
+
import type { SvnActionContext, SvnTargetType } from '../../shared/svnActions.js'
|
|
13
|
+
import { runSvnAction } from '../svnActions.js'
|
|
14
|
+
import type { SvnActionRunCtx } from '../svnActions.js'
|
|
8
15
|
import { revealInExplorer } from '../fileReveal.js'
|
|
9
16
|
import { statusOfAdd } from '../addToConversation.js'
|
|
10
17
|
import type { TreeMenuItem } from './contextMenu.js'
|
|
18
|
+
import type { SidebarCtx } from './types.js'
|
|
19
|
+
import type { TreeMenuTarget } from './contextMenu.js'
|
|
20
|
+
|
|
21
|
+
/** 目标路径对应的变更条目(不在清单里返回 undefined)。 */
|
|
22
|
+
function svnEntryOf(target: TreeMenuTarget, ctx: SidebarCtx): SvnChangeEntry | undefined {
|
|
23
|
+
if (!target.path) return undefined
|
|
24
|
+
const map = ctx.svnChangeMap
|
|
25
|
+
return map ? map[target.path] : undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 把树菜单目标投影为动作求值上下文(能力 + 目标类型 + 状态门禁)。
|
|
30
|
+
* 显隐判定统一交给 shared 的 svnActionOn,避免与页签菜单/命令栏漂移。
|
|
31
|
+
* @author ddj 2026年09月16号
|
|
32
|
+
* @param target 右键目标
|
|
33
|
+
* @param ctx 面板上下文
|
|
34
|
+
* @returns 求值上下文;无 SVN 状态返回 null
|
|
35
|
+
*/
|
|
36
|
+
function svnContextOf(target: TreeMenuTarget, ctx: SidebarCtx): SvnActionContext | null {
|
|
37
|
+
const svn = ctx.svn
|
|
38
|
+
if (!svn) return null
|
|
39
|
+
const entry = svnEntryOf(target, ctx)
|
|
40
|
+
const isRoot = target.path === ''
|
|
41
|
+
const targetType: SvnTargetType = isRoot ? 'root' : (target.type === 'directory' ? 'directory' : 'file')
|
|
42
|
+
return {
|
|
43
|
+
managed: svn.managed,
|
|
44
|
+
svnCli: svn.svnCli,
|
|
45
|
+
tortoise: svn.tortoise,
|
|
46
|
+
// 自研能力集合:驱动「自研替换时间线」(隐藏已被自研覆盖的 Tortoise 项)
|
|
47
|
+
svnFeatures: svn.svnFeatures,
|
|
48
|
+
target: targetType,
|
|
49
|
+
// 不在变更清单里视为受版本控制(干净文件);未版本控制条目显式为 false
|
|
50
|
+
versioned: entry ? entry.versioned : true,
|
|
51
|
+
status: entry?.status,
|
|
52
|
+
diffable: isSvnDiffable(target.path, entry?.status),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 树菜单动作执行上下文(RPC 与反馈经面板 ctx 注入)。
|
|
58
|
+
* @author ddj 2026年09月16号
|
|
59
|
+
* @param target 右键目标
|
|
60
|
+
* @param ctx 面板上下文
|
|
61
|
+
* @returns 执行上下文
|
|
62
|
+
*/
|
|
63
|
+
function treeRunCtx(target: TreeMenuTarget, ctx: SidebarCtx): SvnActionRunCtx {
|
|
64
|
+
return {
|
|
65
|
+
sessionId: ctx.sessionId,
|
|
66
|
+
scope: ctx.scope,
|
|
67
|
+
path: target.path,
|
|
68
|
+
notify: (message) => ctx.notify?.(message),
|
|
69
|
+
openSvnDiff: (p) => ctx.openSvnDiff?.(p),
|
|
70
|
+
openSvnLog: ctx.openSvnLog,
|
|
71
|
+
refreshChanges: ctx.refreshSvnChanges,
|
|
72
|
+
// 确认对话框可注入(测试/宿主可替换;缺省走 window.confirm)
|
|
73
|
+
confirm: ctx.confirm,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 构造 SVN 组菜单项(动作清单来自 shared/svnActions.ts)。
|
|
79
|
+
*
|
|
80
|
+
* 收敛收益:此前每个动作在这里手写一项、页签菜单再手写一遍、命令栏第三遍,
|
|
81
|
+
* 显隐规则三处漂移(P2 的「加入/还原」状态矩阵就重复过两份)。现在只做映射。
|
|
82
|
+
* @author ddj 2026年09月16号
|
|
83
|
+
* @returns SVN 组条目
|
|
84
|
+
*/
|
|
85
|
+
function svnMenuItems(): TreeMenuItem[] {
|
|
86
|
+
return svnActionsFor('tree').map((action) => ({
|
|
87
|
+
id: 'svn-' + action.id,
|
|
88
|
+
label: action.label,
|
|
89
|
+
order: action.order,
|
|
90
|
+
danger: action.danger === true,
|
|
91
|
+
separator: action.separator === true,
|
|
92
|
+
visible: (target: TreeMenuTarget, ctx: SidebarCtx) => {
|
|
93
|
+
const context = svnContextOf(target, ctx)
|
|
94
|
+
return context !== null && svnActionOn(action, context)
|
|
95
|
+
},
|
|
96
|
+
run: (target: TreeMenuTarget, ctx: SidebarCtx) => {
|
|
97
|
+
runSvnAction(action, treeRunCtx(target, ctx))
|
|
98
|
+
},
|
|
99
|
+
}))
|
|
100
|
+
}
|
|
11
101
|
|
|
12
102
|
/**
|
|
13
103
|
* 构造内置右键菜单项列表(后续内置项直接追加)。
|
|
@@ -45,5 +135,6 @@ export function createDefaultFileMenuItems(): TreeMenuItem[] {
|
|
|
45
135
|
})
|
|
46
136
|
},
|
|
47
137
|
},
|
|
138
|
+
...svnMenuItems(),
|
|
48
139
|
]
|
|
49
140
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* 保留旧条目强制后台重列(force);每次用户发起加载后预取 ≤4 个子目录(排除重型
|
|
7
7
|
* 目录、缓存新鲜跳过、不级联);10s 轻量跟随已展开目录(命中索引,RPC 近零成本)。
|
|
8
8
|
* 差异角标/右键菜单/展开状态持久化行为不变;行图标用官方原语(文件夹图标 + 文件类型图标)。
|
|
9
|
-
*
|
|
9
|
+
* SVN 状态徽标(P2):行尾方形字母,数据来自 ctx.svnChangeMap(客户端合成,不改 listDir 契约)。
|
|
10
|
+
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31 / 2026-09-10 / 2026-09-16
|
|
10
11
|
*/
|
|
11
12
|
import React from 'react'
|
|
12
13
|
import { FileTypeIcon, IconFolderClose16, IconFolderOpen16, IconRefreshOutline16, classifyFileType } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
@@ -17,6 +18,8 @@ import { explorerLoad, explorerSave } from '../../state/explorerCache.js'
|
|
|
17
18
|
import { entriesCacheGet, entriesCacheIsFresh, entriesCachePut } from '../../state/explorerEntriesCache.js'
|
|
18
19
|
import { workspaceScopeOf } from '../../state/scopeStore.js'
|
|
19
20
|
import { ancestorDirsOf } from '../../tabActions.js'
|
|
21
|
+
import { refreshSvnChanges, refreshSvnStatus } from '../../svnStatus.js'
|
|
22
|
+
import { SVN_STATUS_TONE, svnBadgeOf, svnBadgeTitle } from '../../../shared/svn.js'
|
|
20
23
|
import type { SidebarCtx } from '../types.js'
|
|
21
24
|
|
|
22
25
|
const DIR_CAP = 4000
|
|
@@ -105,6 +108,7 @@ export function FileExplorer(props) {
|
|
|
105
108
|
const openFile = ctx?.openFile
|
|
106
109
|
const activePath = ctx?.activePath ?? null
|
|
107
110
|
const pendingByPath = ctx?.pendingByPath ?? {}
|
|
111
|
+
const svnChangeMap = ctx?.svnChangeMap ?? {}
|
|
108
112
|
const [root, setRoot] = React.useState(null)
|
|
109
113
|
const [dirs, setDirs] = React.useState({}) // rel → 最新条目(本会话内存态)
|
|
110
114
|
const [expanded, setExpanded] = React.useState({})
|
|
@@ -264,6 +268,10 @@ export function FileExplorer(props) {
|
|
|
264
268
|
setExpanded({})
|
|
265
269
|
}
|
|
266
270
|
void loadDir('', { force: true, prefetch: true })
|
|
271
|
+
// SVN 状态一并强制重测(检出 .svn / 卸载 svn 后可恢复菜单显隐)
|
|
272
|
+
refreshSvnStatus(sessionId, scope)
|
|
273
|
+
// 变更清单同样重查(目录重列后徽标要跟上;host 侧 TTL 内会被 force 绕过)
|
|
274
|
+
refreshSvnChanges(sessionId, scope)
|
|
267
275
|
}
|
|
268
276
|
refreshRef.current = refresh
|
|
269
277
|
reloadDirRef.current = loadDir
|
|
@@ -387,6 +395,9 @@ export function FileExplorer(props) {
|
|
|
387
395
|
const active = !isDir && e.path === activePath
|
|
388
396
|
const contextTarget = Boolean(menu && menuEntries.length && menu.target.path === e.path)
|
|
389
397
|
const dim = e.type === 'other'
|
|
398
|
+
// SVN 状态徽标:客户端由变更清单合成的查表(未加载/干净/未版本控制被过滤时不出徽标)
|
|
399
|
+
const svnEntry = svnChangeMap[e.path]
|
|
400
|
+
const svnLetter = svnEntry ? svnBadgeOf(svnEntry) : ''
|
|
390
401
|
return React.createElement('div', {
|
|
391
402
|
key: e.path,
|
|
392
403
|
className: 'edrv-tree-row'
|
|
@@ -409,6 +420,12 @@ export function FileExplorer(props) {
|
|
|
409
420
|
rowIconEl(e, isDir, isOpen),
|
|
410
421
|
React.createElement('span', { className: 'edrv-tree-name' + (dim ? ' edrv-tree-dim' : '') },
|
|
411
422
|
e.name),
|
|
423
|
+
(svnLetter
|
|
424
|
+
? React.createElement('span', {
|
|
425
|
+
className: 'edrv-tree-svn edrv-svn-ch-' + (SVN_STATUS_TONE[svnEntry.status] ?? 'plain'),
|
|
426
|
+
title: 'SVN ' + svnBadgeTitle(svnEntry),
|
|
427
|
+
}, svnLetter)
|
|
428
|
+
: null),
|
|
412
429
|
(pending > 0
|
|
413
430
|
? React.createElement('span', { className: 'edrv-tree-badge' }, String(pending))
|
|
414
431
|
: null))
|