dsh-vscode-mode 0.1.30 → 0.1.35
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/LICENSE +21 -0
- package/README.md +30 -0
- package/lib/client.js +1439 -46
- package/lib/client.js.map +1 -1
- package/lib/index.js +3519 -222
- package/lib/index.js.map +1 -1
- package/package.json +2 -2
- package/src/capture.ts +3 -0
- package/src/client/events.ts +11 -0
- package/src/client/index.ts +20 -2
- package/src/client/monaco/lsp/index.ts +30 -0
- package/src/client/monaco/lsp/lspClient.ts +192 -0
- package/src/client/monaco/lsp/providers.ts +229 -0
- package/src/client/outline/OutlinePanel.tsx +1 -1
- package/src/client/outline/lspSource.ts +49 -0
- package/src/client/outline/parse.ts +31 -1
- package/src/client/outline/types.ts +2 -0
- package/src/client/paths.ts +36 -0
- package/src/client/sidebar/panels/FileExplorer.ts +95 -30
- package/src/client/sidebar/panels/SearchPanel.ts +11 -2
- package/src/client/state/explorerCache.ts +3 -1
- package/src/client/state/explorerEntriesCache.ts +172 -0
- package/src/client/state/viewStateCache.ts +3 -1
- package/src/client/styles/editor.css +16 -0
- package/src/client/styles/mcp.css +63 -0
- package/src/client/ui/EditorView.ts +61 -6
- package/src/client/ui/LspSettings.ts +268 -0
- package/src/client/ui/McpSettings.ts +5 -1
- package/src/client/ui/PerfSettings.ts +242 -0
- package/src/client/ui/PerfSizeBadge.ts +54 -0
- package/src/compat.ts +3 -2
- package/src/fileOpenSettings.ts +1 -0
- package/src/index.ts +25 -2
- package/src/lsp/client.ts +205 -0
- package/src/lsp/config.ts +107 -0
- package/src/lsp/extmgr.ts +208 -0
- package/src/lsp/jsonrpc.ts +86 -0
- package/src/lsp/manager.ts +133 -0
- package/src/lsp/providers.ts +235 -0
- package/src/lsp/rpc.ts +334 -0
- package/src/lsp/server.ts +346 -0
- package/src/lsp/settings.ts +44 -0
- package/src/lsp/transport.ts +143 -0
- package/src/lsp/uri.ts +62 -0
- package/src/lsp/zip.ts +82 -0
- package/src/paths.ts +284 -0
- package/src/perf.ts +340 -0
- package/src/perfPatch.ts +76 -0
- package/src/routes.ts +7 -12
- package/src/rpc.ts +184 -36
- package/src/search/content.ts +14 -7
- package/src/search/ripgrep.ts +54 -1
- package/src/search/types.ts +4 -1
- package/src/shared/lsp.ts +138 -0
- package/src/shared/rpc.ts +103 -2
- package/src/store.ts +2 -2
- package/src/treeIndex.ts +397 -0
- package/src/treeRead.ts +87 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 浏览器端缓存键/路径统一出口(PathConst)。
|
|
3
|
+
* 统一 localStorage 键前缀(各缓存模块/编辑区/搜索面板共用),避免内联散落;
|
|
4
|
+
* 键带 schema 版本号,未来版本递增时经 sweepLegacyKeys 自动清旧。
|
|
5
|
+
* 浏览器不涉 ~/.dsh 文件系统,这里只管键名 + shared 路由常量。
|
|
6
|
+
* 作者 ddj 2026-09-01
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** localStorage 键前缀表(按功能隔离;均按会话拼接)。 */
|
|
10
|
+
export const CACHE_KEY = {
|
|
11
|
+
/** 展开状态(explorerCache v1,当前使用)。 */
|
|
12
|
+
expanded: 'edrv.cache.explorer.v1.',
|
|
13
|
+
/** 目录条目 SWR 缓存(explorerEntriesCache v2,当前使用)。 */
|
|
14
|
+
entries: 'edrv.cache.entries.v2.',
|
|
15
|
+
/** 编辑区视图状态(viewStateCache v1)。 */
|
|
16
|
+
viewstate: 'edrv.cache.viewstate.v1.',
|
|
17
|
+
/** 编辑器页签/活动文件(EditorView v2)。 */
|
|
18
|
+
editor: 'edrv.editor.v2.',
|
|
19
|
+
/** 侧边栏状态(EditorView;按布局 side/central 追加段)。 */
|
|
20
|
+
sidebar: 'edrv.sidebar.',
|
|
21
|
+
/** 搜索面板条件(SearchPanel v1)。 */
|
|
22
|
+
search: 'edrv.search.v1.',
|
|
23
|
+
/** 侧边栏提示已关闭标记(全局,无会话)。 */
|
|
24
|
+
sideHint: 'edrv.side-hint-dismissed',
|
|
25
|
+
} as const
|
|
26
|
+
|
|
27
|
+
/** 按前缀拼会话键(侧边栏/编辑器等需要布局段时自行拼接后传入)。 */
|
|
28
|
+
export function cacheKey(prefix: string, sessionId: string): string {
|
|
29
|
+
return prefix + sessionId
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 旧版本键清理入口:未来前缀版本递增时在此登记旧前缀并清空(当前无实际旧键)。 */
|
|
33
|
+
export function sweepLegacyKeys(): void {
|
|
34
|
+
// 例:entries v2 取代 v1 时 → localStorage.removeItem('edrv.cache.entries.v1.' + sid)
|
|
35
|
+
// 当前各功能均为现役版本,无待清旧键。
|
|
36
|
+
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// @ts-nocheck
|
|
2
2
|
/**
|
|
3
|
-
* dsh-vscode-mode client —
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* dsh-vscode-mode client — 侧边栏「文件管理」面板(懒加载目录树 + SWR 缓存)。
|
|
4
|
+
* 展开目录立即渲染(dirs 内存态 → 本地条目缓存兜底),后台刷新(edrv.listDir 走
|
|
5
|
+
* host 树索引,命中近乎零成本);挂载/切会话从缓存即时恢复展开树;⟳/edrv:refresh
|
|
6
|
+
* 保留旧条目强制后台重列(force);每次用户发起加载后预取 ≤4 个子目录(排除重型
|
|
7
|
+
* 目录、缓存新鲜跳过、不级联);10s 轻量跟随已展开目录(命中索引,RPC 近零成本)。
|
|
8
|
+
* 差异角标/右键菜单/展开状态持久化行为不变。
|
|
9
|
+
* 作者 ddj 2026-08-26 / 2026-08-27 / 2026-08-31
|
|
9
10
|
*/
|
|
10
11
|
import React from 'react'
|
|
11
12
|
import { rpc } from '../../rpc.js'
|
|
@@ -13,13 +14,17 @@ import { ContextMenu } from '../../ui/ContextMenu.js'
|
|
|
13
14
|
import { rowMenuPosition } from '../../ui/menuPosition.js'
|
|
14
15
|
import { buildTreeMenu } from '../contextMenu.js'
|
|
15
16
|
import { explorerLoad, explorerSave } from '../../state/explorerCache.js'
|
|
17
|
+
import { entriesCacheGet, entriesCacheIsFresh, entriesCachePut } from '../../state/explorerEntriesCache.js'
|
|
16
18
|
import type { SidebarCtx } from '../types.js'
|
|
17
19
|
|
|
18
20
|
const DIR_CAP = 4000
|
|
19
21
|
const SAVE_DEBOUNCE_MS = 300
|
|
22
|
+
const FOLLOW_INTERVAL_MS = 10_000
|
|
23
|
+
const PREFETCH_MAX = 4
|
|
24
|
+
const PREFETCH_EXCLUDED = new Set(['node_modules', '.git', '.hg', '.svn', '.pnpm', '.pnpm-store'])
|
|
20
25
|
|
|
21
26
|
/**
|
|
22
|
-
*
|
|
27
|
+
* 目录树面板主体(SWR:有缓存先渲染,无缓存才显示加载态;加载总在后台)。
|
|
23
28
|
* @param props.ctx 面板共享上下文(sessionId/openFile/activePath/pendingByPath/fileMenuItems/notify)
|
|
24
29
|
*/
|
|
25
30
|
export function FileExplorer(props) {
|
|
@@ -29,34 +34,76 @@ export function FileExplorer(props) {
|
|
|
29
34
|
const activePath = ctx?.activePath ?? null
|
|
30
35
|
const pendingByPath = ctx?.pendingByPath ?? {}
|
|
31
36
|
const [root, setRoot] = React.useState(null)
|
|
32
|
-
const [dirs, setDirs] = React.useState({})
|
|
37
|
+
const [dirs, setDirs] = React.useState({}) // rel → 最新条目(本会话内存态)
|
|
33
38
|
const [expanded, setExpanded] = React.useState({})
|
|
34
39
|
const [loading, setLoading] = React.useState({})
|
|
35
|
-
const [
|
|
40
|
+
const [errors, setErrors] = React.useState({}) // rel → 错误文案(仅无任何数据时展示)
|
|
36
41
|
const [menu, setMenu] = React.useState(null) // 右键菜单 { x, y, target }
|
|
42
|
+
// 各状态 ref 镜像:定时器/监听用最新闭包
|
|
37
43
|
const tokensRef = React.useRef({})
|
|
38
|
-
// expanded 的 ref 镜像:edrv:refresh 监听用首次渲染闭包,但需读到最新展开态
|
|
39
44
|
const expandedRef = React.useRef({})
|
|
40
45
|
expandedRef.current = expanded
|
|
41
|
-
|
|
46
|
+
const loadingRef = React.useRef({})
|
|
47
|
+
loadingRef.current = loading
|
|
48
|
+
const dirsRef = React.useRef({})
|
|
49
|
+
dirsRef.current = dirs
|
|
42
50
|
const saveTimerRef = React.useRef(null)
|
|
51
|
+
const loadDirRef = React.useRef(null)
|
|
52
|
+
const refreshRef = React.useRef(null)
|
|
43
53
|
|
|
44
|
-
|
|
54
|
+
/** 渲染取数:内存态 → 本地条目缓存 → null(显示加载态)。 */
|
|
55
|
+
const entriesOf = (rel) => dirsRef.current[rel] ?? entriesCacheGet(sessionId, rel) ?? null
|
|
56
|
+
|
|
57
|
+
/** 预取子目录:≤4 个、排除重型目录、缓存新鲜跳过、已加载跳过、不级联。 */
|
|
58
|
+
const prefetchDirs = (rel, entries) => {
|
|
59
|
+
const want = []
|
|
60
|
+
for (const e of entries) {
|
|
61
|
+
if (e.type !== 'directory') continue
|
|
62
|
+
if (PREFETCH_EXCLUDED.has(e.name)) continue
|
|
63
|
+
if (entriesCacheIsFresh(sessionId, e.path)) continue
|
|
64
|
+
if (dirsRef.current[e.path] !== undefined) continue
|
|
65
|
+
want.push(e.path)
|
|
66
|
+
if (want.length >= PREFETCH_MAX) break
|
|
67
|
+
}
|
|
68
|
+
for (const sub of want) void loadDir(sub, {})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 后台加载目录:token 守卫防乱序;force 跳过索引命中强制实列;
|
|
73
|
+
* 非 force 且已在途时跳过(避免轮询打断用户展开的响应)。
|
|
74
|
+
*/
|
|
75
|
+
const loadDir = (rel, opts) => {
|
|
76
|
+
const force = opts?.force === true
|
|
77
|
+
const doPrefetch = opts?.prefetch === true
|
|
78
|
+
if (!force && loadingRef.current[rel]) return
|
|
45
79
|
const token = (tokensRef.current[rel] || 0) + 1
|
|
46
80
|
tokensRef.current[rel] = token
|
|
47
81
|
setLoading((prev) => Object.assign({}, prev, { [rel]: true }))
|
|
48
|
-
|
|
49
|
-
|
|
82
|
+
setErrors((prev) => {
|
|
83
|
+
const next = Object.assign({}, prev)
|
|
84
|
+
delete next[rel]
|
|
85
|
+
return next
|
|
86
|
+
})
|
|
87
|
+
const args = { sessionId, path: rel }
|
|
88
|
+
if (force) args.force = true
|
|
89
|
+
rpc('edrv.listDir', args).then((res) => {
|
|
50
90
|
if (tokensRef.current[rel] !== token) return
|
|
51
91
|
if (res && res.ok && Array.isArray(res.entries)) {
|
|
52
92
|
setDirs((prev) => Object.assign({}, prev, { [rel]: res.entries }))
|
|
53
|
-
if (res.root
|
|
93
|
+
if (res.root) setRoot((prev) => prev || res.root)
|
|
94
|
+
entriesCachePut(sessionId, rel, res.entries)
|
|
95
|
+
if (doPrefetch) prefetchDirs(rel, res.entries)
|
|
54
96
|
} else {
|
|
55
|
-
|
|
97
|
+
// 有可渲染数据时静默(旧数据可能过期但可用);全无数据才报错
|
|
98
|
+
if (!entriesOf(rel)) {
|
|
99
|
+
setErrors((prev) => Object.assign({}, prev, { [rel]: res?.error ? String(res.error) : '读取目录失败' }))
|
|
100
|
+
}
|
|
56
101
|
}
|
|
57
102
|
}).catch((e) => {
|
|
58
103
|
if (tokensRef.current[rel] !== token) return
|
|
59
|
-
|
|
104
|
+
if (!entriesOf(rel)) {
|
|
105
|
+
setErrors((prev) => Object.assign({}, prev, { [rel]: '读取目录异常:' + String(e) }))
|
|
106
|
+
}
|
|
60
107
|
}).finally(() => {
|
|
61
108
|
if (tokensRef.current[rel] !== token) return
|
|
62
109
|
setLoading((prev) => {
|
|
@@ -66,43 +113,45 @@ export function FileExplorer(props) {
|
|
|
66
113
|
})
|
|
67
114
|
})
|
|
68
115
|
}
|
|
116
|
+
loadDirRef.current = loadDir
|
|
69
117
|
|
|
70
118
|
const toggle = (rel) => {
|
|
71
119
|
if (expanded[rel] === true) {
|
|
72
120
|
setExpanded((prev) => Object.assign({}, prev, { [rel]: false }))
|
|
73
121
|
return
|
|
74
122
|
}
|
|
75
|
-
if (!dirs[rel]) void loadDir(rel)
|
|
76
123
|
setExpanded((prev) => Object.assign({}, prev, { [rel]: true }))
|
|
124
|
+
void loadDir(rel, { prefetch: true })
|
|
77
125
|
}
|
|
78
126
|
|
|
79
127
|
const refresh = () => {
|
|
80
128
|
const keep = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true)
|
|
81
129
|
tokensRef.current = {}
|
|
82
130
|
setMenu(null)
|
|
83
|
-
|
|
131
|
+
setErrors({})
|
|
132
|
+
// SWR:保留旧条目不清空,后台强制重列(force 跳过索引)
|
|
84
133
|
setLoading({})
|
|
85
|
-
setError(null)
|
|
86
134
|
if (keep.length) {
|
|
87
135
|
const next = {}
|
|
88
136
|
for (const rel of keep) next[rel] = true
|
|
89
137
|
setExpanded(next)
|
|
90
|
-
for (const rel of keep) void loadDir(rel)
|
|
138
|
+
for (const rel of keep) void loadDir(rel, { force: true, prefetch: true })
|
|
91
139
|
} else {
|
|
92
140
|
setExpanded({})
|
|
93
141
|
}
|
|
94
|
-
void loadDir('')
|
|
142
|
+
void loadDir('', { force: true, prefetch: true })
|
|
95
143
|
}
|
|
144
|
+
refreshRef.current = refresh
|
|
96
145
|
|
|
97
146
|
React.useEffect(() => {
|
|
98
147
|
tokensRef.current = {}
|
|
99
148
|
setDirs({})
|
|
100
149
|
setExpanded({})
|
|
101
150
|
setLoading({})
|
|
102
|
-
|
|
151
|
+
setErrors({})
|
|
103
152
|
setMenu(null)
|
|
104
153
|
setRoot(null)
|
|
105
|
-
// 恢复上次展开状态(对齐 VSCode
|
|
154
|
+
// 恢复上次展开状态(对齐 VSCode:持久化展开路径,条目缓存即时渲染、后台刷新)
|
|
106
155
|
const cached = sessionId ? explorerLoad(sessionId) : null
|
|
107
156
|
const restored = cached?.expanded ?? []
|
|
108
157
|
if (cached?.root) setRoot(cached.root)
|
|
@@ -111,9 +160,9 @@ export function FileExplorer(props) {
|
|
|
111
160
|
for (const rel of restored) next[rel] = true
|
|
112
161
|
setExpanded(next)
|
|
113
162
|
}
|
|
114
|
-
void loadDir('')
|
|
163
|
+
void loadDir('', { prefetch: true })
|
|
115
164
|
for (const rel of restored) {
|
|
116
|
-
if (rel) void loadDir(rel)
|
|
165
|
+
if (rel) void loadDir(rel, { prefetch: true })
|
|
117
166
|
}
|
|
118
167
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
119
168
|
}, [sessionId])
|
|
@@ -132,13 +181,26 @@ export function FileExplorer(props) {
|
|
|
132
181
|
}
|
|
133
182
|
}, [expanded, root, sessionId])
|
|
134
183
|
|
|
184
|
+
// edrv:refresh(差异决策/回滚后):保留旧条目,强制后台重列
|
|
135
185
|
React.useEffect(() => {
|
|
136
|
-
const onRefresh = () =>
|
|
186
|
+
const onRefresh = () => refreshRef.current?.()
|
|
137
187
|
window.addEventListener('edrv:refresh', onRefresh)
|
|
138
188
|
return () => window.removeEventListener('edrv:refresh', onRefresh)
|
|
139
189
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
140
190
|
}, [])
|
|
141
191
|
|
|
192
|
+
// 10s 轻量跟随:已展开目录后台刷新(命中 host 索引,近零成本),树跟随 agent 写入
|
|
193
|
+
React.useEffect(() => {
|
|
194
|
+
if (!sessionId) return
|
|
195
|
+
const timer = setInterval(() => {
|
|
196
|
+
const open = Object.keys(expandedRef.current).filter((k) => expandedRef.current[k] === true)
|
|
197
|
+
if (!open.length) return
|
|
198
|
+
for (const rel of open) loadDirRef.current?.(rel, {})
|
|
199
|
+
}, FOLLOW_INTERVAL_MS)
|
|
200
|
+
return () => clearInterval(timer)
|
|
201
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
202
|
+
}, [sessionId])
|
|
203
|
+
|
|
142
204
|
const rootName = root ? String(root).split(/[\\/]/).pop() || root : ''
|
|
143
205
|
|
|
144
206
|
const rowEl = (e, depth, isDir, isOpen) => {
|
|
@@ -172,7 +234,7 @@ export function FileExplorer(props) {
|
|
|
172
234
|
}
|
|
173
235
|
|
|
174
236
|
const rowsOf = (rel, depth) => {
|
|
175
|
-
const entries =
|
|
237
|
+
const entries = entriesOf(rel)
|
|
176
238
|
if (!entries) {
|
|
177
239
|
return [React.createElement('div', { key: rel + ':loading', className: 'edrv-tree-loading', style: { paddingLeft: 6 + depth * 14 } },
|
|
178
240
|
loading[rel] ? '加载中…' : '无法加载')]
|
|
@@ -212,6 +274,9 @@ export function FileExplorer(props) {
|
|
|
212
274
|
}))
|
|
213
275
|
: []
|
|
214
276
|
|
|
277
|
+
const errorList = Object.entries(errors)
|
|
278
|
+
const errorText = errorList.length ? errorList[0][1] : null
|
|
279
|
+
|
|
215
280
|
return React.createElement('div', { className: 'edrv-side-panel', onContextMenu: onPanelContext },
|
|
216
281
|
React.createElement('div', { className: 'edrv-side-head' },
|
|
217
282
|
React.createElement('span', { className: 'edrv-side-title' }, '资源管理器'),
|
|
@@ -219,9 +284,9 @@ export function FileExplorer(props) {
|
|
|
219
284
|
React.createElement('span', { style: { flex: 1 } }),
|
|
220
285
|
React.createElement('button', { className: 'edrv-side-btn', title: '刷新目录树', onClick: refresh }, '⟳')),
|
|
221
286
|
React.createElement('div', { className: 'edrv-tree' },
|
|
222
|
-
(
|
|
287
|
+
(errorText
|
|
223
288
|
? React.createElement('div', { className: 'edrv-tree-error' },
|
|
224
|
-
React.createElement('span', null, String(
|
|
289
|
+
React.createElement('span', null, String(errorText)),
|
|
225
290
|
React.createElement('button', { className: 'edrv-side-btn', onClick: () => refresh() }, '重试'))
|
|
226
291
|
: null),
|
|
227
292
|
rowsOf('', 0)),
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import React from 'react'
|
|
11
11
|
import { rpc } from '../../rpc.js'
|
|
12
12
|
import type { SidebarCtx } from '../types.js'
|
|
13
|
+
import { CACHE_KEY } from '../../paths.js'
|
|
13
14
|
|
|
14
15
|
const DEBOUNCE_MS = 250
|
|
15
16
|
const INCLUDE_PLACEHOLDER = '例如 *.ts, src/**/include'
|
|
@@ -56,6 +57,7 @@ export function SearchPanel(props) {
|
|
|
56
57
|
const [sectionOpen, setSectionOpen] = React.useState(false)
|
|
57
58
|
const [status, setStatus] = React.useState('idle') // idle | searching | done | error
|
|
58
59
|
const [error, setError] = React.useState('')
|
|
60
|
+
const [warning, setWarning] = React.useState('') // rg 遍历错误时的部分结果提示(结果照常展示)
|
|
59
61
|
const [matches, setMatches] = React.useState(null)
|
|
60
62
|
const [truncated, setTruncated] = React.useState(false)
|
|
61
63
|
const [collapsed, setCollapsed] = React.useState({})
|
|
@@ -78,7 +80,7 @@ export function SearchPanel(props) {
|
|
|
78
80
|
React.useEffect(() => {
|
|
79
81
|
if (!sessionId) return
|
|
80
82
|
try {
|
|
81
|
-
const saved = JSON.parse(localStorage.getItem(
|
|
83
|
+
const saved = JSON.parse(localStorage.getItem(CACHE_KEY.search + String(sessionId)) || 'null')
|
|
82
84
|
if (saved && typeof saved === 'object') {
|
|
83
85
|
if (typeof saved.query === 'string') setQuery(saved.query)
|
|
84
86
|
if (typeof saved.matchCase === 'boolean') setMatchCase(saved.matchCase)
|
|
@@ -97,7 +99,7 @@ export function SearchPanel(props) {
|
|
|
97
99
|
React.useEffect(() => {
|
|
98
100
|
if (!sessionId) return
|
|
99
101
|
try {
|
|
100
|
-
localStorage.setItem(
|
|
102
|
+
localStorage.setItem(CACHE_KEY.search + String(sessionId), JSON.stringify({
|
|
101
103
|
query, matchCase, wholeWord, regex, includeText, excludeText, onlyActive, excludeOn, sectionOpen,
|
|
102
104
|
}))
|
|
103
105
|
} catch (e) { /* 忽略 */ }
|
|
@@ -114,6 +116,7 @@ export function SearchPanel(props) {
|
|
|
114
116
|
const req = requestRef.current
|
|
115
117
|
setStatus('searching')
|
|
116
118
|
setError('')
|
|
119
|
+
setWarning('')
|
|
117
120
|
rpc('edrv.searchContent', {
|
|
118
121
|
sessionId, query: clean,
|
|
119
122
|
matchCase: req.matchCase, wholeWord: req.wholeWord, regex: req.regex,
|
|
@@ -123,6 +126,7 @@ export function SearchPanel(props) {
|
|
|
123
126
|
if (res && res.ok) {
|
|
124
127
|
setMatches(Array.isArray(res.matches) ? res.matches : [])
|
|
125
128
|
setTruncated(res.truncated === true)
|
|
129
|
+
setWarning(res?.warning ? String(res.warning) : '')
|
|
126
130
|
setStatus('done')
|
|
127
131
|
} else {
|
|
128
132
|
setError(res?.error ? String(res.error) : '搜索失败')
|
|
@@ -249,6 +253,10 @@ export function SearchPanel(props) {
|
|
|
249
253
|
: '无匹配结果')
|
|
250
254
|
: null
|
|
251
255
|
|
|
256
|
+
const warnBanner = warning
|
|
257
|
+
? React.createElement('div', { className: 'edrv-search-warn', title: warning }, warning)
|
|
258
|
+
: null
|
|
259
|
+
|
|
252
260
|
const body = (() => {
|
|
253
261
|
if (status === 'searching') return React.createElement('div', { className: 'edrv-search-empty' }, '搜索中…')
|
|
254
262
|
if (status === 'error') return React.createElement('div', { className: 'edrv-search-error' }, error)
|
|
@@ -290,5 +298,6 @@ export function SearchPanel(props) {
|
|
|
290
298
|
optsBar,
|
|
291
299
|
filterRows,
|
|
292
300
|
summary,
|
|
301
|
+
warnBanner,
|
|
293
302
|
body)
|
|
294
303
|
}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 资源管理栏展开状态持久化(纯函数,可单测)。
|
|
3
3
|
* 对齐 VSCode:只持久化展开路径集合,不缓存目录条目数据(条目实时 listDir)。
|
|
4
|
+
* 键前缀统一走 paths.ts PathConst(CACHE_KEY.expanded)。
|
|
4
5
|
* 作者 ddj 2026-08-28
|
|
5
6
|
*/
|
|
7
|
+
import { CACHE_KEY } from '../paths.js'
|
|
6
8
|
|
|
7
9
|
/** localStorage 键前缀(按会话隔离)。 */
|
|
8
|
-
const KEY_PREFIX =
|
|
10
|
+
const KEY_PREFIX = CACHE_KEY.expanded
|
|
9
11
|
|
|
10
12
|
/** 展开路径容量上限:超出丢弃最旧(按写入顺序近似 LRU)。 */
|
|
11
13
|
export const EXPANDED_CAP = 500
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 资源管理栏目录条目缓存(SWR:旧数据即时渲染 + 后台刷新)。
|
|
3
|
+
* 与 explorerCache(展开状态持久化)互补:本文件只缓存目录条目数据,键按会话隔离。
|
|
4
|
+
* 纯函数(序列化/解析/裁剪)导出可单测;内存镜像 Map 同步读避免每次渲染 parse
|
|
5
|
+
* localStorage,落盘 300ms 防抖;配额满/隐私模式 try/catch 降级纯内存。
|
|
6
|
+
* 作者 ddj 2026-08-31
|
|
7
|
+
*/
|
|
8
|
+
import type { TreeEntry } from '../../shared/rpc.js'
|
|
9
|
+
import { CACHE_KEY } from '../paths.js'
|
|
10
|
+
|
|
11
|
+
// --region 常量
|
|
12
|
+
const KEY_PREFIX = CACHE_KEY.entries
|
|
13
|
+
/** 缓存目录上限(超出按 ts 逐出最旧,保活热门目录)。 */
|
|
14
|
+
export const CACHE_MAX_DIRS = 300
|
|
15
|
+
/** 单目录条目缓存上限(即时渲染用,刷新补齐全量)。 */
|
|
16
|
+
export const CACHE_DIR_ENTRIES = 500
|
|
17
|
+
/** 新鲜度窗口:预取去重用(展示不受限,永远可用缓存渲染)。 */
|
|
18
|
+
export const CACHE_FRESH_MS = 30_000
|
|
19
|
+
/** 落盘防抖。 */
|
|
20
|
+
export const CACHE_SAVE_DEBOUNCE_MS = 300
|
|
21
|
+
// --endregion
|
|
22
|
+
|
|
23
|
+
/** 单目录缓存条目。 */
|
|
24
|
+
export interface CachedDir {
|
|
25
|
+
ts: number
|
|
26
|
+
entries: TreeEntry[]
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** 缓存数据(v2:与 v1 展开状态缓存分离)。 */
|
|
30
|
+
export interface EntriesCacheData {
|
|
31
|
+
v: 2
|
|
32
|
+
root: string | null
|
|
33
|
+
dirs: Record<string, CachedDir>
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** 裁剪到上限(目录按 ts 逐出最旧;单目录条目截断)。 */
|
|
37
|
+
export function entriesTrim(data: EntriesCacheData, maxDirs = CACHE_MAX_DIRS, maxEntries = CACHE_DIR_ENTRIES): EntriesCacheData {
|
|
38
|
+
const dirs: Record<string, CachedDir> = {}
|
|
39
|
+
const sorted = Object.entries(data.dirs).sort((a, b) => b[1].ts - a[1].ts)
|
|
40
|
+
for (const [rel, dir] of sorted) {
|
|
41
|
+
if (Object.keys(dirs).length >= maxDirs) break
|
|
42
|
+
dirs[rel] = { ts: dir.ts, entries: dir.entries.length > maxEntries ? dir.entries.slice(0, maxEntries) : dir.entries }
|
|
43
|
+
}
|
|
44
|
+
return { v: 2, root: data.root, dirs }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 序列化缓存数据。 */
|
|
48
|
+
export function entriesSerialize(data: EntriesCacheData): string {
|
|
49
|
+
return JSON.stringify(data)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 解析缓存文本;损坏/格式不符 → null。 */
|
|
53
|
+
export function entriesParse(text: string | null): EntriesCacheData | null {
|
|
54
|
+
if (!text) return null
|
|
55
|
+
try {
|
|
56
|
+
const raw = JSON.parse(text) as { v?: unknown; root?: unknown; dirs?: unknown }
|
|
57
|
+
if (!raw || raw.v !== 2 || !raw.dirs || typeof raw.dirs !== 'object') return null
|
|
58
|
+
const dirs: Record<string, CachedDir> = {}
|
|
59
|
+
for (const [rel, d] of Object.entries(raw.dirs)) {
|
|
60
|
+
if (!d || typeof d !== 'object') continue
|
|
61
|
+
const dir = d as { ts?: unknown; entries?: unknown }
|
|
62
|
+
if (!Array.isArray(dir.entries)) continue
|
|
63
|
+
const entries: TreeEntry[] = []
|
|
64
|
+
for (const e of dir.entries) {
|
|
65
|
+
if (!e || typeof e !== 'object') continue
|
|
66
|
+
const item = e as { name?: unknown; path?: unknown; type?: unknown }
|
|
67
|
+
if (typeof item.name !== 'string' || typeof item.path !== 'string') continue
|
|
68
|
+
const type = item.type === 'directory' ? 'directory' : item.type === 'file' ? 'file' : 'other'
|
|
69
|
+
entries.push({ name: item.name, path: item.path, type })
|
|
70
|
+
}
|
|
71
|
+
if (!entries.length) continue
|
|
72
|
+
dirs[rel] = { ts: typeof dir.ts === 'number' ? dir.ts : 0, entries }
|
|
73
|
+
}
|
|
74
|
+
return { v: 2, root: typeof raw.root === 'string' ? raw.root : null, dirs }
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return null
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// --region 内存镜像 + 防抖落盘
|
|
81
|
+
/** sessionId → 缓存镜像(同步读,避免每次渲染 parse)。 */
|
|
82
|
+
const mirrors = new Map<string, EntriesCacheData>()
|
|
83
|
+
/** sessionId → 落盘防抖计时器。 */
|
|
84
|
+
const saveTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
85
|
+
/** 单调时钟:连续 put 的 ts 严格递增(同毫秒不并列,逐出顺序可预期)。 */
|
|
86
|
+
let clock = 0
|
|
87
|
+
|
|
88
|
+
/** 取下一时间戳(不小于真实时间且严格递增)。 */
|
|
89
|
+
function nextTs(): number {
|
|
90
|
+
clock = Math.max(Date.now(), clock + 1)
|
|
91
|
+
return clock
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** localStorage 读取(不可用 → null)。 */
|
|
95
|
+
function readRaw(sessionId: string): string | null {
|
|
96
|
+
try {
|
|
97
|
+
return window.localStorage.getItem(KEY_PREFIX + sessionId)
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** localStorage 写入(配额/隐私模式失败静默)。 */
|
|
104
|
+
function writeRaw(sessionId: string, text: string): void {
|
|
105
|
+
try {
|
|
106
|
+
window.localStorage.setItem(KEY_PREFIX + sessionId, text)
|
|
107
|
+
} catch (error) { /* 配额满/隐私模式忽略 */ }
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** 取(或建)会话缓存镜像。 */
|
|
111
|
+
function mirrorOf(sessionId: string): EntriesCacheData {
|
|
112
|
+
const existing = mirrors.get(sessionId)
|
|
113
|
+
if (existing) return existing
|
|
114
|
+
const data = entriesParse(readRaw(sessionId)) ?? { v: 2, root: null, dirs: {} }
|
|
115
|
+
mirrors.set(sessionId, data)
|
|
116
|
+
return data
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 调度防抖落盘(重置已有计时器)。 */
|
|
120
|
+
function scheduleSave(sessionId: string): void {
|
|
121
|
+
const prev = saveTimers.get(sessionId)
|
|
122
|
+
if (prev) clearTimeout(prev)
|
|
123
|
+
saveTimers.set(sessionId, setTimeout(() => {
|
|
124
|
+
saveTimers.delete(sessionId)
|
|
125
|
+
const data = mirrors.get(sessionId)
|
|
126
|
+
if (data) writeRaw(sessionId, entriesSerialize(entriesTrim(data)))
|
|
127
|
+
}, CACHE_SAVE_DEBOUNCE_MS))
|
|
128
|
+
}
|
|
129
|
+
// --endregion
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 读某目录缓存条目(无 → null;展示不受新鲜度限制)。
|
|
133
|
+
* @author ddj 2026年08月31号
|
|
134
|
+
* @param sessionId 会话 id
|
|
135
|
+
* @param rel 目录相对路径('' = 根)
|
|
136
|
+
* @returns 缓存条目或 null
|
|
137
|
+
*/
|
|
138
|
+
export function entriesCacheGet(sessionId: string | undefined, rel: string): TreeEntry[] | null {
|
|
139
|
+
if (!sessionId) return null
|
|
140
|
+
const dir = mirrorOf(sessionId).dirs[rel]
|
|
141
|
+
if (!dir || !dir.entries.length) return null
|
|
142
|
+
return dir.entries
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 某目录缓存是否新鲜(预取去重用)。
|
|
147
|
+
* @author ddj 2026年08月31号
|
|
148
|
+
* @param sessionId 会话 id
|
|
149
|
+
* @param rel 目录相对路径
|
|
150
|
+
* @returns 新鲜返回 true
|
|
151
|
+
*/
|
|
152
|
+
export function entriesCacheIsFresh(sessionId: string | undefined, rel: string): boolean {
|
|
153
|
+
if (!sessionId) return false
|
|
154
|
+
const dir = mirrorOf(sessionId).dirs[rel]
|
|
155
|
+
return Boolean(dir && Date.now() - dir.ts < CACHE_FRESH_MS)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* 写入某目录缓存条目(内存镜像 + 防抖落盘;单目录条目截断)。
|
|
160
|
+
* @author ddj 2026年08月31号
|
|
161
|
+
* @param sessionId 会话 id
|
|
162
|
+
* @param rel 目录相对路径
|
|
163
|
+
* @param entries 目录条目
|
|
164
|
+
*/
|
|
165
|
+
export function entriesCachePut(sessionId: string | undefined, rel: string, entries: TreeEntry[]): void {
|
|
166
|
+
if (!sessionId) return
|
|
167
|
+
const data = mirrorOf(sessionId)
|
|
168
|
+
data.dirs[rel] = { ts: nextTs(), entries: entries.slice(0, CACHE_DIR_ENTRIES) }
|
|
169
|
+
const trimmed = entriesTrim(data)
|
|
170
|
+
data.dirs = trimmed.dirs
|
|
171
|
+
scheduleSave(sessionId)
|
|
172
|
+
}
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
* dsh-vscode-mode client — 编辑器视图状态持久化(纯函数,可单测)。
|
|
3
3
|
* 对齐 VSCode workbench.editor.restoreViewState:恢复每个文件的光标/滚动/折叠位置。
|
|
4
4
|
* 视图状态 = Monaco editor.saveViewState() 返回值(JSON 可序列化),按 path 存储。
|
|
5
|
+
* 键前缀统一走 paths.ts PathConst(CACHE_KEY.viewstate)。
|
|
5
6
|
* 作者 ddj 2026-08-28
|
|
6
7
|
*/
|
|
8
|
+
import { CACHE_KEY } from '../paths.js'
|
|
7
9
|
|
|
8
10
|
/** localStorage 键前缀(按会话隔离)。 */
|
|
9
|
-
const KEY_PREFIX =
|
|
11
|
+
const KEY_PREFIX = CACHE_KEY.viewstate
|
|
10
12
|
|
|
11
13
|
/** 视图状态条目容量上限。 */
|
|
12
14
|
export const VIEWSTATE_CAP = 50
|
|
@@ -33,6 +33,13 @@
|
|
|
33
33
|
[data-edrv-view] .edrv-btn-mini:disabled { opacity: .45; cursor: default; }
|
|
34
34
|
[data-edrv-view] .edrv-statusbar { display: flex; align-items: center; gap: 10px; padding: 3px 10px; border-top: 1px solid var(--dsw-alias-border-l1, #333); background: var(--dsw-alias-bg-layer-1, transparent); font-size: 11px; color: var(--dsw-alias-label-tertiary, #888); flex-shrink: 0; white-space: nowrap; overflow: hidden; }
|
|
35
35
|
[data-edrv-view] .edrv-statusbar .edrv-sp-path { flex: 1; overflow: hidden; text-overflow: ellipsis; }
|
|
36
|
+
[data-edrv-view] .edrv-statusbar .edrv-sp-lsp { flex: 0 1 auto; overflow: hidden; text-overflow: ellipsis; }
|
|
37
|
+
[data-edrv-view] .edrv-statusbar .edrv-sp-progress-message { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #52606d); }
|
|
38
|
+
[data-edrv-view] .edrv-statusbar .edrv-sp-progress { flex: 0 0 auto; min-width: 36px; text-align: right; font-variant-numeric: tabular-nums; color: var(--dsw-alias-label-secondary, #52606d); }
|
|
39
|
+
[data-edrv-view] .edrv-lsp-status-dot { width: 7px; height: 7px; border-radius: 50%; flex: 0 0 auto; background: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
40
|
+
[data-edrv-view] .edrv-lsp-status-dot.ready { background: var(--dsw-alias-state-success-primary, #0f9d58); }
|
|
41
|
+
[data-edrv-view] .edrv-lsp-status-dot.busy { background: var(--dsw-alias-state-warn-primary, #d97706); animation: edrv-lsp-pulse 1.2s ease-in-out infinite; }
|
|
42
|
+
@keyframes edrv-lsp-pulse { 50% { opacity: .45; } }
|
|
36
43
|
[data-edrv-view] .edrv-diffchip { font-size: 11px; padding: 2px 10px; border: 1px solid var(--dsw-alias-border-l1, #333); border-radius: 999px; background: var(--dsw-alias-bg-layer-2, transparent); color: var(--dsw-alias-state-warn-primary, #b7791f); cursor: pointer; white-space: nowrap; flex-shrink: 0; }
|
|
37
44
|
[data-edrv-view] .edrv-chip-btn { font-size: 12px; padding: 2px 6px; border: none; background: transparent; color: var(--dsw-alias-label-secondary, #aaa); cursor: pointer; border-radius: 4px; flex-shrink: 0; }
|
|
38
45
|
[data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
|
|
@@ -270,6 +277,7 @@
|
|
|
270
277
|
[data-edrv-view] .edrv-search-hit { background: var(--dsw-alias-state-warn-primary, #e5b93f); color: #1f2933; border-radius: 2px; padding: 0 1px; }
|
|
271
278
|
[data-edrv-view] .edrv-search-empty { padding: 8px 4px; font-size: 12px; color: var(--dsw-alias-label-tertiary, #9aa5b1); }
|
|
272
279
|
[data-edrv-view] .edrv-search-error { padding: 6px 8px; border: 1px solid rgba(217,83,79,.4); border-radius: 6px; background: #fdeaea; color: var(--dsw-alias-state-error-primary, #d9534f); font-size: 11px; }
|
|
280
|
+
[data-edrv-view] .edrv-search-warn { padding: 6px 8px; border: 1px solid rgba(240,173,78,.45); border-radius: 6px; background: #fdf6e3; color: #b8860b; font-size: 11px; word-break: break-all; }
|
|
273
281
|
|
|
274
282
|
/* include/exclude filter rows (collapsible) */
|
|
275
283
|
[data-edrv-view] .edrv-search-rows { display: flex; flex-direction: column; gap: 4px; padding: 2px 0 4px; }
|
|
@@ -283,3 +291,11 @@
|
|
|
283
291
|
[data-edrv-view] .edrv-search-toggle.on { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.14)); color: var(--dsw-alias-label-primary, #1f2933); border-color: var(--dsw-alias-brand-primary, #0f9d58); }
|
|
284
292
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
285
293
|
|
|
294
|
+
/* LSP 引用结果浮动面板(Shift+F12 / 右键「查找所有引用」) */
|
|
295
|
+
[data-edrv-view] .edrv-refs { position: absolute; right: 12px; top: 40px; z-index: 60; width: 360px; max-height: 60%; display: flex; flex-direction: column; border: 1px solid var(--dsw-alias-border-l2, #555); border-radius: 8px; background: var(--dsw-alias-bg-panel, #161b22); box-shadow: 0 6px 24px rgba(0,0,0,.4); overflow: hidden; font-size: 12px; }
|
|
296
|
+
[data-edrv-view] .edrv-refs-head { padding: 6px 10px; font-weight: 600; color: var(--dsw-alias-label-primary, #eee); border-bottom: 1px solid var(--dsw-alias-border-l2, #333); background: var(--dsw-alias-bg-layer-1, transparent); }
|
|
297
|
+
[data-edrv-view] .edrv-refs-list { overflow-y: auto; }
|
|
298
|
+
[data-edrv-view] .edrv-refs-row { padding: 4px 10px; cursor: pointer; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; color: var(--dsw-alias-label-secondary, #c0c8d0); font-family: var(--ds-font-family-code, ui-monospace, monospace); }
|
|
299
|
+
[data-edrv-view] .edrv-refs-row:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(15,157,88,.10)); color: var(--dsw-alias-label-primary, #fff); }
|
|
300
|
+
|
|
301
|
+
|
|
@@ -128,3 +128,66 @@
|
|
|
128
128
|
.vsm-kb-actionsbar{display:flex;align-items:center;gap:10px;margin-top:12px}
|
|
129
129
|
.vsm-kb-dirty{flex:1;font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
130
130
|
.vsm-kb-message{font-size:12px;color:var(--dsw-alias-brand-primary,#4f8cff);margin-top:8px}
|
|
131
|
+
|
|
132
|
+
/* 语言服务器(LSP)设置子 Tab */
|
|
133
|
+
.vsm-lsp-form{display:flex;flex-direction:column;gap:6px;margin-top:8px}
|
|
134
|
+
.vsm-lsp-form label{display:flex;flex-direction:column;gap:4px;font-size:12px;color:var(--dsw-alias-label-secondary,#9aa5b1)}
|
|
135
|
+
.vsm-lsp-form input{height:26px;padding:0 8px;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;outline:none}
|
|
136
|
+
.vsm-lsp-form input:focus{border-color:var(--dsw-alias-brand-primary,#4f8cff)}
|
|
137
|
+
.vsm-lsp-form button{align-self:flex-start;margin-top:2px}
|
|
138
|
+
.vsm-lsp-hint{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin-top:10px;line-height:1.6}
|
|
139
|
+
|
|
140
|
+
/* 会话性能(PerfSettings 子 Tab) */
|
|
141
|
+
.vsm-perf-page{display:flex;flex-direction:column;gap:14px;max-width:920px}
|
|
142
|
+
.vsm-perf-page h2{margin:0;font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
143
|
+
.vsm-perf-page p{margin:0;color:var(--dsw-alias-label-tertiary,#9aa5b1);font-size:13px;line-height:1.5}
|
|
144
|
+
.vsm-perf-cards{display:flex;gap:12px;flex-wrap:wrap}
|
|
145
|
+
.vsm-perf-card{display:flex;flex-direction:column;gap:4px;min-width:120px;padding:12px 16px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
146
|
+
.vsm-perf-card span{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
147
|
+
.vsm-perf-card b{font-size:20px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
148
|
+
.vsm-perf-card b.big{color:var(--dsw-alias-state-error-primary,#e05252)}
|
|
149
|
+
.vsm-perf-toolbar{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
150
|
+
.vsm-perf-selcount{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);margin:0 4px}
|
|
151
|
+
.vsm-perf-ws{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;overflow:hidden}
|
|
152
|
+
.vsm-perf-ws-head{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:10px 14px;background:var(--dsw-alias-bg-layer-1,#262626);border-bottom:1px solid var(--dsw-alias-border-l2,#3a3a3a)}
|
|
153
|
+
.vsm-perf-ws-name{font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary,#e6e6e6);font-family:var(--ds-font-family-code,ui-monospace,monospace);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
154
|
+
.vsm-perf-ws-meta{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);flex-shrink:0}
|
|
155
|
+
.vsm-perf-rows{display:flex;flex-direction:column}
|
|
156
|
+
.vsm-perf-row{display:flex;align-items:center;gap:10px;padding:7px 14px;border-bottom:1px solid var(--dsw-alias-border-l1,#333);font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
157
|
+
.vsm-perf-row:last-child{border-bottom:0}
|
|
158
|
+
.vsm-perf-row:hover{background:var(--dsw-alias-interactive-bg-hover,#ffffff0d)}
|
|
159
|
+
.vsm-perf-row input[type=checkbox]{accent-color:var(--dsw-alias-brand-primary,#6d55ff);cursor:pointer}
|
|
160
|
+
.vsm-perf-cell{min-width:0}
|
|
161
|
+
.vsm-perf-id{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-family:var(--ds-font-family-code,ui-monospace,monospace);color:var(--dsw-alias-label-primary,#eee)}
|
|
162
|
+
.vsm-perf-size{width:70px;text-align:right;flex-shrink:0;color:var(--dsw-alias-label-secondary,#c8c8c8)}
|
|
163
|
+
.vsm-perf-size.big{color:var(--dsw-alias-state-error-primary,#e05252);font-weight:600}
|
|
164
|
+
.vsm-perf-mtime{width:44px;flex-shrink:0;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
165
|
+
.vsm-perf-active{width:44px;flex-shrink:0;font-size:11px;color:var(--dsw-alias-state-warn-primary,#e5b93f);text-align:right}
|
|
166
|
+
.vsm-perf-moved{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
167
|
+
.vsm-perf-moved h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
168
|
+
.vsm-perf-config{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
169
|
+
.vsm-perf-config h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
170
|
+
.vsm-perf-config-state{display:flex;align-items:center;gap:12px;margin:10px 0}
|
|
171
|
+
.vsm-perf-note{font-size:12px;color:var(--dsw-alias-state-warn-primary,#e5b93f)}
|
|
172
|
+
.vsm-perf-block{margin:8px 0 0;padding:10px 12px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:8px;background:var(--dsw-alias-bg-base,#111);color:var(--dsw-alias-label-secondary,#c8c8c8);font-size:11px;line-height:1.5;font-family:var(--ds-font-family-code,ui-monospace,monospace);white-space:pre-wrap;overflow-x:auto;max-width:100%}
|
|
173
|
+
.vsm-perf-guide{border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:10px;padding:12px 14px;background:var(--dsw-alias-bg-layer-2,transparent)}
|
|
174
|
+
.vsm-perf-guide h3{margin:0 0 8px;font-size:14px;color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
175
|
+
.vsm-perf-modal{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:var(--dsw-alias-bg-mask-1,#000000aa);z-index:1000;padding:20px}
|
|
176
|
+
.vsm-perf-modal-card{width:min(560px,92vw);max-height:72vh;display:flex;flex-direction:column;gap:12px;padding:18px 20px;border:1px solid var(--dsw-alias-border-l2,#3a3a3a);border-radius:12px;background:var(--dsw-alias-bg-layer-2,#1f1f1f);color:var(--dsw-alias-label-primary,#e6e6e6)}
|
|
177
|
+
.vsm-perf-modal-card h3{margin:0;font-size:16px}
|
|
178
|
+
.vsm-perf-modal-note{font-size:12px}
|
|
179
|
+
.vsm-perf-modal-list{list-style:none;margin:0;padding:0;overflow-y:auto;font-size:12px;color:var(--dsw-alias-label-secondary,#c8c8c8);display:flex;flex-direction:column;gap:4px}
|
|
180
|
+
.vsm-perf-modal-list li{padding:4px 6px;border-radius:6px;background:var(--dsw-alias-bg-layer-1,#262626);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
181
|
+
.vsm-perf-modal-more{font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1)}
|
|
182
|
+
.vsm-perf-modal-actions{display:flex;justify-content:flex-end;gap:10px;margin-top:4px}
|
|
183
|
+
.vsm-secondary{border:1px solid var(--dsw-alias-border-l2,#555);border-radius:6px;padding:8px 16px;background:none;color:var(--dsw-alias-label-primary,#eee);font-size:13px;cursor:pointer;white-space:nowrap}
|
|
184
|
+
.vsm-secondary:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#ffffff14)}
|
|
185
|
+
.vsm-secondary:disabled{opacity:.55;cursor:default}
|
|
186
|
+
.vsm-small{padding:4px 10px;font-size:12px}
|
|
187
|
+
|
|
188
|
+
/* 扩展管理(LSP 语言服务器) */
|
|
189
|
+
.vsm-mcp-actions button.vsm-primary,
|
|
190
|
+
.vsm-mcp-actions button.vsm-danger{width:auto;height:auto;padding:5px 12px;font-size:12px;border-radius:6px;line-height:1.4}
|
|
191
|
+
.vsm-mcp-actions button.vsm-danger{color:var(--dsw-alias-state-error-primary,#f07878);background:none}
|
|
192
|
+
.vsm-mcp-actions button.vsm-danger:hover{background:var(--dsw-alias-interactive-bg-hover-danger,#ff000014)}
|
|
193
|
+
.vsm-ext-ver{font-weight:400;font-size:12px;color:var(--dsw-alias-label-tertiary,#9aa5b1);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|