dsh-vscode-mode 0.1.43 → 0.1.45
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 +3 -1
- package/lib/client.js +418 -92
- package/lib/client.js.map +1 -1
- package/lib/index.js +6 -3
- package/lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/client/keybindings.ts +41 -15
- package/src/client/monaco/lsp/providers.ts +129 -62
- package/src/client/navHistory.ts +104 -0
- package/src/client/styles/editor.css +2 -6
- package/src/client/ui/EditorView.ts +200 -10
- package/src/client/ui/KeybindingsPanel.ts +3 -3
- package/src/client/ui/QuickOpen.ts +2 -2
- package/src/shared/keybindings.ts +5 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vscode-mode",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.45",
|
|
4
4
|
"description": "DSH 上的类 VSCode 编码体验:Monaco 编辑器(文件页签/QuickOpen/状态栏,可驻 dsh-better-sidebar 侧边栏与对话同屏)+ Agent 编辑差异审查(整文件差异/采纳/拒绝/归档/回滚)+ 语言服务器(LSP)智能(跳转定义/引用/hover/大纲 + VSIX 扩展市场安装),状态持久化到工作区旁车",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* dsh-vscode-mode client — 快捷键配置模块(解析/匹配/状态同步)。
|
|
3
|
-
* 纯逻辑(parseChord/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
3
|
+
* 纯逻辑(parseChord/parseChords/formatChord/matchEvent/chordFromEvent/normalizeKey)不依赖 DOM,可单测;
|
|
4
4
|
* 模块状态由 settings 订阅驱动(client/index.ts 调 keybindingsApply)。
|
|
5
5
|
* 键位语义:Ctrl 与 Cmd 互认(延续现有 Ctrl+P/Ctrl+B 捕获行为)。
|
|
6
6
|
* 作者 ddj 2026年08月26号
|
|
@@ -23,6 +23,8 @@ export const COMMANDS: Array<{ id: string; label: string }> = [
|
|
|
23
23
|
{ id: 'edrv.quickOpen', label: '快速打开文件' },
|
|
24
24
|
{ id: 'edrv.toggleSidebar', label: '切换侧边栏' },
|
|
25
25
|
{ id: 'edrv.searchInFiles', label: '在工作区中搜索' },
|
|
26
|
+
{ id: 'edrv.navigateBack', label: '后退(导航历史)' },
|
|
27
|
+
{ id: 'edrv.navigateForward', label: '前进(导航历史)' },
|
|
26
28
|
]
|
|
27
29
|
|
|
28
30
|
const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
@@ -30,18 +32,19 @@ const MODIFIERS: Record<string, 'ctrl' | 'shift' | 'alt' | 'meta'> = {
|
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
let current: Record<string, string> = { ...KEYBINDING_DEFAULTS }
|
|
33
|
-
let parsed: Record<string, Binding
|
|
35
|
+
let parsed: Record<string, Binding[]> = {}
|
|
34
36
|
const listeners = new Set<() => void>()
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
39
|
* 应用设置快照(与默认值合并;未知 id 丢弃;空对象 = 全部默认)。
|
|
40
|
+
* 每个命令可含多候选键位(`|` 分隔),任一命中即触发。
|
|
38
41
|
* @author ddj 2026年08月26号
|
|
39
42
|
* @param raw 设置 scope 的 keybindings 字段
|
|
40
43
|
*/
|
|
41
44
|
export function keybindingsApply(raw: unknown): void {
|
|
42
45
|
current = { ...KEYBINDING_DEFAULTS, ...normalizeKeybindings(raw) }
|
|
43
46
|
parsed = {}
|
|
44
|
-
for (const id of Object.keys(current)) parsed[id] =
|
|
47
|
+
for (const id of Object.keys(current)) parsed[id] = parseChords(current[id])
|
|
45
48
|
for (const listener of listeners) {
|
|
46
49
|
try { listener() } catch { /* 监听器异常不影响其他订阅 */ }
|
|
47
50
|
}
|
|
@@ -70,13 +73,13 @@ export function chordOf(id: string): string | null {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
/**
|
|
73
|
-
*
|
|
76
|
+
* 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
|
|
74
77
|
* @author ddj 2026年08月26号
|
|
75
78
|
* @param id 命令 id
|
|
76
|
-
* @returns
|
|
79
|
+
* @returns 解析键位数组(可能为空)
|
|
77
80
|
*/
|
|
78
|
-
export function
|
|
79
|
-
return parsed[id] ??
|
|
81
|
+
export function bindingsOf(id: string): Binding[] {
|
|
82
|
+
return parsed[id] ?? []
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/**
|
|
@@ -114,6 +117,26 @@ export function parseChord(chord: string): Binding | null {
|
|
|
114
117
|
return binding.key ? binding : null
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/**
|
|
121
|
+
* 解析多候选键位弦(`|` 分隔):逐段解析,丢弃非法段,去重。
|
|
122
|
+
* @author ddj 2026年08月26号
|
|
123
|
+
* @param chord 键位弦(如 `Alt+ArrowLeft|Ctrl+Alt+-`)
|
|
124
|
+
* @returns 解析键位数组(纯 `|`/空/非法 → 空数组)
|
|
125
|
+
*/
|
|
126
|
+
export function parseChords(chord: string): Binding[] {
|
|
127
|
+
const out: Binding[] = []
|
|
128
|
+
const seen = new Set<string>()
|
|
129
|
+
for (const part of String(chord ?? '').split('|')) {
|
|
130
|
+
const binding = parseChord(part)
|
|
131
|
+
if (!binding) continue
|
|
132
|
+
const key = formatChord(binding)
|
|
133
|
+
if (seen.has(key)) continue
|
|
134
|
+
seen.add(key)
|
|
135
|
+
out.push(binding)
|
|
136
|
+
}
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
117
140
|
/**
|
|
118
141
|
* 格式化键位(Ctrl/Cmd/Shift/Alt + 主键;meta 显示 Cmd)。
|
|
119
142
|
* @author ddj 2026年08月26号
|
|
@@ -131,18 +154,21 @@ export function formatChord(binding: Binding): string {
|
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
/**
|
|
134
|
-
* 事件是否命中键位(Ctrl 与 Cmd
|
|
157
|
+
* 事件是否命中键位(Ctrl 与 Cmd 互认;多候选任一命中即 true)。
|
|
135
158
|
* @author ddj 2026年08月26号
|
|
136
159
|
* @param e 键盘事件(鸭子类型,便于单测)
|
|
137
|
-
* @param
|
|
160
|
+
* @param bindings 解析键位或键位数组(null/空 = 未绑定,永不命中)
|
|
138
161
|
* @returns 是否命中
|
|
139
162
|
*/
|
|
140
|
-
export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string },
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
163
|
+
export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string }, bindings: Binding | Binding[] | null): boolean {
|
|
164
|
+
const list = Array.isArray(bindings) ? bindings : bindings ? [bindings] : []
|
|
165
|
+
for (const binding of list) {
|
|
166
|
+
if (Boolean(e.ctrlKey || e.metaKey) !== (binding.ctrl || binding.meta)) continue
|
|
167
|
+
if (Boolean(e.shiftKey) !== binding.shift) continue
|
|
168
|
+
if (Boolean(e.altKey) !== binding.alt) continue
|
|
169
|
+
if (normalizeKey(e.key ?? '') === binding.key) return true
|
|
170
|
+
}
|
|
171
|
+
return false
|
|
146
172
|
}
|
|
147
173
|
|
|
148
174
|
/**
|
|
@@ -2,16 +2,18 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* dsh-vscode-mode client — Monaco LSP provider 注册 + 跳转/引用命令。
|
|
4
4
|
* 数据层:Definition/Reference/DocumentSymbol/Hover provider(同一文件内跳转 Monaco 原生可用)。
|
|
5
|
-
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 /
|
|
6
|
-
*
|
|
5
|
+
* 交互层:edrv.goToDefinition(F12)/ edrv.findReferences(Shift+F12 / 右键)/ Ctrl+点击引用导航,
|
|
6
|
+
* 跨文件跳转自研(target 经 openFileAt 打开并定位),多条引用走 Monaco 原生 References Peek
|
|
7
|
+
* (左侧代码预览 + 右侧引用列表;registerEditorOpener 接管 edrv:// 点击跳转)。
|
|
7
8
|
* 作者 ddj 2026-08-27
|
|
8
9
|
*/
|
|
9
10
|
import {
|
|
10
11
|
pathOfModel, syncDoc, closeDoc, findDefinition, findReferences,
|
|
11
12
|
fetchDocumentSymbols, fetchHover, fetchSemanticTokens,
|
|
12
13
|
targetOpenPath, targetMonoPosition, lspStatusFor, refreshStatus,
|
|
14
|
+
lspUriToAbs,
|
|
13
15
|
} from './lspClient.js'
|
|
14
|
-
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES } from '../../../shared/lsp.js'
|
|
16
|
+
import { LSP_SEMANTIC_TOKEN_MODIFIERS, LSP_SEMANTIC_TOKEN_TYPES, monoToLsp } from '../../../shared/lsp.js'
|
|
15
17
|
|
|
16
18
|
const LSP_LANGS = ['lua', 'csharp']
|
|
17
19
|
const SEMANTIC_LEGEND = {
|
|
@@ -20,7 +22,6 @@ const SEMANTIC_LEGEND = {
|
|
|
20
22
|
}
|
|
21
23
|
let registered = false
|
|
22
24
|
const disposables = []
|
|
23
|
-
let overlayEl = null
|
|
24
25
|
|
|
25
26
|
/** 目标文件打开并定位(复用现有 openFileAt 的 edrv:open-editor 事件通道)。 */
|
|
26
27
|
function openAt(path, line, column) {
|
|
@@ -72,6 +73,20 @@ export function registerLspProviders(monaco) {
|
|
|
72
73
|
|
|
73
74
|
// —— 数据 provider ——
|
|
74
75
|
disposables.push(
|
|
76
|
+
// 原生跳转(peek 参考文献列表点击 / 原生 go to definition 等)的 edrv:// 打开兜底:
|
|
77
|
+
// 目标 uri 由本插件自己定义(edrv:// 工作区相对路径),Monaco 无法自行加载,
|
|
78
|
+
// 必须经事件通道交给 EditorView openFileAt 打开并定位。
|
|
79
|
+
monaco.editor.registerEditorOpener({
|
|
80
|
+
openCodeEditor: (source, resource, selectionOrPosition) => {
|
|
81
|
+
const path = lspUriToAbs(resource)
|
|
82
|
+
if (!path) return false
|
|
83
|
+
const start = selectionOrPosition?.getStartPosition?.() ?? selectionOrPosition
|
|
84
|
+
const line = Math.max(1, start?.lineNumber ?? 1)
|
|
85
|
+
const column = Math.max(1, start?.column ?? 1)
|
|
86
|
+
openAt(path, line, column)
|
|
87
|
+
return true
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
75
90
|
monaco.languages.registerDefinitionProvider(LSP_LANGS, {
|
|
76
91
|
provideDefinition: (model, position, token) => {
|
|
77
92
|
const path = pathOfModel(model)
|
|
@@ -168,12 +183,108 @@ export async function runGoToDefinition(ed) {
|
|
|
168
183
|
await goToDefinition(ed, ed.getPosition(), true)
|
|
169
184
|
}
|
|
170
185
|
|
|
171
|
-
/**
|
|
172
|
-
|
|
173
|
-
|
|
186
|
+
/**
|
|
187
|
+
* Ctrl+鼠标点击:查询其它内容对该项的引用并导航。
|
|
188
|
+
* 0 条 → 无特殊效果;1 条 → 直接跳转;多条 → 打开 Monaco 原生 References Peek
|
|
189
|
+
* (左侧代码预览 + 右侧引用列表,点击经 registerEditorOpener 跳转)。
|
|
190
|
+
* @author ddj 2026年09月02号
|
|
191
|
+
* @param ed Monaco 编辑器
|
|
192
|
+
* @param position 点击位置(Monaco 1-based)
|
|
193
|
+
*/
|
|
194
|
+
export async function gotoRefsAt(ed, position) {
|
|
195
|
+
const model = ed.getModel()
|
|
196
|
+
const path = pathOfModel(model)
|
|
197
|
+
if (!path || !position) return
|
|
198
|
+
const all = await findReferences(path, model.getValue(), position, false)
|
|
199
|
+
const others = filterOtherRefs(all, path, position)
|
|
200
|
+
if (!others.length) return
|
|
201
|
+
if (others.length === 1) {
|
|
202
|
+
jumpToLocation(others[0])
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
// 计数器就绪:光标移到点击处(原生 Peek 按光标词查询),走原生两栏引用视图
|
|
206
|
+
ed.setPosition(position)
|
|
207
|
+
await triggerReferencePeek(ed)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 触发 Monaco 原生 References Peek(左侧代码 + 右侧引用列表,原生点击跳转)。
|
|
212
|
+
* @author ddj 2026年09月02号
|
|
213
|
+
* @param ed Monaco 编辑器
|
|
214
|
+
* @returns 是否成功触发(动作缺失时返回 false)
|
|
215
|
+
*/
|
|
216
|
+
export async function triggerReferencePeek(ed) {
|
|
217
|
+
const action = ed.getAction('editor.action.referenceSearch.trigger')
|
|
218
|
+
if (!action || typeof action.run !== 'function') return false
|
|
219
|
+
await action.run()
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* 过滤「其它内容」引用:剔除当前点击处自身(同文件且点击位置落入其 range),并按路径+起点去重。
|
|
225
|
+
* @author ddj 2026年09月02号
|
|
226
|
+
* @param locations 引用结果
|
|
227
|
+
* @param path 当前文档路径
|
|
228
|
+
* @param position 点击位置(Monaco 1-based)
|
|
229
|
+
* @returns 过滤后的其它引用
|
|
230
|
+
*/
|
|
231
|
+
function filterOtherRefs(locations, path, position) {
|
|
232
|
+
const lspPos = monoToLsp(position.lineNumber, position.column)
|
|
233
|
+
const seen = new Set()
|
|
234
|
+
const out = []
|
|
235
|
+
for (const loc of locations) {
|
|
236
|
+
const rel = targetOpenPath(loc)
|
|
237
|
+
if (sameFile(rel, path) && posInRange(lspPos, loc.lspRange)) continue
|
|
238
|
+
const start = loc.lspRange?.start ?? {}
|
|
239
|
+
const key = String(rel).toLowerCase() + ':' + (start.line ?? 0) + ':' + (start.character ?? 0)
|
|
240
|
+
if (seen.has(key)) continue
|
|
241
|
+
seen.add(key)
|
|
242
|
+
out.push(loc)
|
|
243
|
+
}
|
|
244
|
+
return out
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 两个路径是否指向同一文件(容忍相对/绝对混合与大小写差异)。
|
|
249
|
+
* @author ddj 2026年09月02号
|
|
250
|
+
* @param a 路径 A
|
|
251
|
+
* @param b 路径 B
|
|
252
|
+
* @returns 是否同一文件
|
|
253
|
+
*/
|
|
254
|
+
function sameFile(a, b) {
|
|
255
|
+
if (!a || !b) return false
|
|
256
|
+
const na = String(a).replace(/\\/g, '/').toLowerCase()
|
|
257
|
+
const nb = String(b).replace(/\\/g, '/').toLowerCase()
|
|
258
|
+
if (na === nb) return true
|
|
259
|
+
const absA = /^[A-Za-z]:\//.test(na) || na.startsWith('/')
|
|
260
|
+
const absB = /^[A-Za-z]:\//.test(nb) || nb.startsWith('/')
|
|
261
|
+
// 同为相对/同为绝对但不等 → 不同文件(避免根目录文件误匹配同名子目录文件)
|
|
262
|
+
if (absA === absB) return false
|
|
263
|
+
// 一方绝对一方相对:绝对路径以相对路径结尾视为同文件
|
|
264
|
+
return absA ? na.endsWith('/' + nb) : nb.endsWith('/' + na)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* 0-based LSP 位置是否落在 range 内(end 排除;零宽 range 按点匹配)。
|
|
269
|
+
* @author ddj 2026年09月02号
|
|
270
|
+
* @param pos LSP 位置(0-based)
|
|
271
|
+
* @param range LSP range
|
|
272
|
+
* @returns 是否在范围内
|
|
273
|
+
*/
|
|
274
|
+
function posInRange(pos, range) {
|
|
275
|
+
const start = range?.start
|
|
276
|
+
if (!start) return false
|
|
277
|
+
const end = range?.end ?? start
|
|
278
|
+
if (start.line === end.line && start.character === end.character) {
|
|
279
|
+
return pos.line === start.line && pos.character === start.character
|
|
280
|
+
}
|
|
281
|
+
if (pos.line < start.line || pos.line > end.line) return false
|
|
282
|
+
if (pos.line === start.line && pos.character < start.character) return false
|
|
283
|
+
if (pos.line === end.line && pos.character >= end.character) return false
|
|
284
|
+
return true
|
|
174
285
|
}
|
|
175
286
|
|
|
176
|
-
/** 给一个 Monaco 编辑器绑定 Ctrl/Cmd
|
|
287
|
+
/** 给一个 Monaco 编辑器绑定 Ctrl/Cmd+鼠标引用导航(幂等)。 */
|
|
177
288
|
export function bindLspEditor(ed) {
|
|
178
289
|
if (!ed || ed.__edrvLspClick) return
|
|
179
290
|
ed.__edrvLspClick = true
|
|
@@ -187,68 +298,24 @@ export function bindLspEditor(ed) {
|
|
|
187
298
|
if (!native || !position || !isLeftButton || (!ctrl && !meta)) return
|
|
188
299
|
native.preventDefault?.()
|
|
189
300
|
native.stopPropagation?.()
|
|
190
|
-
void
|
|
301
|
+
void gotoRefsAt(ed, position)
|
|
191
302
|
})
|
|
192
303
|
disposables.push(disposable)
|
|
193
304
|
}
|
|
194
305
|
|
|
195
|
-
/** Shift+F12 /
|
|
306
|
+
/** Shift+F12 / 右键「查找所有引用」:走 Monaco 原生 References Peek(两栏引用视图)。 */
|
|
196
307
|
export async function runFindReferences(ed) {
|
|
197
|
-
const model = ed.getModel()
|
|
198
|
-
const path = pathOfModel(model)
|
|
199
308
|
const position = ed.getPosition()
|
|
200
|
-
if (!
|
|
201
|
-
|
|
202
|
-
|
|
309
|
+
if (!position) return
|
|
310
|
+
ed.setPosition(position)
|
|
311
|
+
await triggerReferencePeek(ed)
|
|
203
312
|
}
|
|
204
313
|
|
|
205
|
-
/**
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
return
|
|
211
|
-
}
|
|
212
|
-
const root = ed.getDomNode()
|
|
213
|
-
if (!root) return
|
|
214
|
-
const el = document.createElement('div')
|
|
215
|
-
el.className = 'edrv-refs'
|
|
216
|
-
const header = document.createElement('div')
|
|
217
|
-
header.className = 'edrv-refs-head'
|
|
218
|
-
header.textContent = '引用 ' + locations.length + ' 处'
|
|
219
|
-
el.appendChild(header)
|
|
220
|
-
const list = document.createElement('div')
|
|
221
|
-
list.className = 'edrv-refs-list'
|
|
222
|
-
for (const loc of locations) {
|
|
223
|
-
const row = document.createElement('div')
|
|
224
|
-
row.className = 'edrv-refs-row'
|
|
225
|
-
const pos = targetMonoPosition(loc)
|
|
226
|
-
const rel = targetOpenPath(loc)
|
|
227
|
-
const name = String(rel).split('/').pop() || rel
|
|
228
|
-
row.textContent = name + ':' + pos.line + ':' + pos.column
|
|
229
|
-
row.title = rel + ':' + pos.line
|
|
230
|
-
row.addEventListener('click', () => {
|
|
231
|
-
hideReferencesOverlay()
|
|
232
|
-
jumpToLocation(loc)
|
|
233
|
-
})
|
|
234
|
-
list.appendChild(row)
|
|
235
|
-
}
|
|
236
|
-
el.appendChild(list)
|
|
237
|
-
root.appendChild(el)
|
|
238
|
-
overlayEl = el
|
|
239
|
-
const onKey = (e) => { if (e.key === 'Escape') hideReferencesOverlay() }
|
|
240
|
-
window.addEventListener('keydown', onKey)
|
|
241
|
-
el.addEventListener('click', (e) => { if (e.target === el) hideReferencesOverlay() })
|
|
242
|
-
el._onKey = onKey
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export function hideReferencesOverlay() {
|
|
246
|
-
if (overlayEl) {
|
|
247
|
-
window.removeEventListener('keydown', overlayEl._onKey)
|
|
248
|
-
overlayEl.remove()
|
|
249
|
-
overlayEl = null
|
|
250
|
-
}
|
|
251
|
-
}
|
|
314
|
+
/**
|
|
315
|
+
* 兼容占位(历史导出;自绘引用浮窗已移除,改走 Monaco 原生 Peek)。
|
|
316
|
+
* @author ddj 2026年09月02号
|
|
317
|
+
*/
|
|
318
|
+
export function hideReferencesOverlay() {}
|
|
252
319
|
|
|
253
320
|
/** 编辑器状态栏提示(复用 EditorView 的 status 通道)。 */
|
|
254
321
|
function setStatus(text) {
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-vscode-mode client — 编辑器导航历史(后退/前进)纯逻辑。
|
|
3
|
+
* 对齐 VSCode Go Back / Go Forward(navigation history):记录「文件 + 焦点位置」访问轨迹,
|
|
4
|
+
* 后退回到上一处焦点,前进回到下一处,跨文件生效。
|
|
5
|
+
* 数据模型:past(末位 = 当前)+ future 两个栈;record 压栈并清空 future;
|
|
6
|
+
* 与栈顶同 path+line+column 视为同一位置:仅刷新 viewState 快照,不压栈、不清 future。
|
|
7
|
+
* 纯逻辑无 DOM 依赖,可单测。
|
|
8
|
+
* 作者 ddj 2026年09月04号
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** 导航历史单条记录:文件 + 焦点位置(+ 可选的 viewState 快照用于恢复滚动/折叠)。 */
|
|
12
|
+
export interface NavEntry {
|
|
13
|
+
/** 工作区相对路径 */
|
|
14
|
+
path: string
|
|
15
|
+
/** 焦点行(1-based) */
|
|
16
|
+
line?: number
|
|
17
|
+
/** 焦点列(1-based) */
|
|
18
|
+
column?: number
|
|
19
|
+
/** 该位置保存的 Monaco viewState(恢复滚动/折叠) */
|
|
20
|
+
viewState?: unknown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** 导航历史接口。 */
|
|
24
|
+
export interface NavHistory {
|
|
25
|
+
/** 记录当前位置(压栈 + 清空 future;与栈顶同位置时仅更新 viewState)。 */
|
|
26
|
+
record(entry: NavEntry): void
|
|
27
|
+
/** 后退:返回将要恢复的前一条目;历史不足 2 条 → null。 */
|
|
28
|
+
back(): NavEntry | null
|
|
29
|
+
/** 前进:返回将要恢复的条目;future 空 → null。 */
|
|
30
|
+
forward(): NavEntry | null
|
|
31
|
+
/** 是否可后退。 */
|
|
32
|
+
canBack(): boolean
|
|
33
|
+
/** 是否可前进。 */
|
|
34
|
+
canForward(): boolean
|
|
35
|
+
/** 后退目标条目(不改变状态;不可退 → null)。 */
|
|
36
|
+
peekBack(): NavEntry | null
|
|
37
|
+
/** 前进目标条目(不改变状态;不可进 → null)。 */
|
|
38
|
+
peekForward(): NavEntry | null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 后退栈默认容量上限(超出逐出最旧)。 */
|
|
42
|
+
export const NAV_HISTORY_CAP = 200
|
|
43
|
+
|
|
44
|
+
/** 路径一致(容忍相对路径大小写差异,对齐既有 sameFile 语义常用写法)。 */
|
|
45
|
+
function samePath(a: string, b: string): boolean {
|
|
46
|
+
if (a === b) return true
|
|
47
|
+
return String(a).replace(/\\/g, '/').toLowerCase() === String(b).replace(/\\/g, '/').toLowerCase()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 位置一致(无焦点时按 null 归一,`undefined`/`null` 视为相等)。 */
|
|
51
|
+
function samePos(a: NavEntry, b: NavEntry): boolean {
|
|
52
|
+
if (!samePath(a.path, b.path)) return false
|
|
53
|
+
return (a.line ?? null) === (b.line ?? null) && (a.column ?? null) === (b.column ?? null)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 创建导航历史实例(容量裁剪到 cap)。
|
|
58
|
+
* @author ddj 2026年09月04号
|
|
59
|
+
* @param cap 后退栈容量上限(默认 NAV_HISTORY_CAP)
|
|
60
|
+
* @returns 导航历史实例
|
|
61
|
+
*/
|
|
62
|
+
export function createNavHistory(cap = NAV_HISTORY_CAP): NavHistory {
|
|
63
|
+
const past: NavEntry[] = []
|
|
64
|
+
const future: NavEntry[] = []
|
|
65
|
+
return {
|
|
66
|
+
record(entry: NavEntry): void {
|
|
67
|
+
if (!entry || !entry.path) return
|
|
68
|
+
const top = past[past.length - 1]
|
|
69
|
+
if (top && samePos(top, entry)) {
|
|
70
|
+
// 同一位置:只更新 viewState 快照(滚动/折叠可能变化),栈与 future 不动
|
|
71
|
+
if (top.viewState !== undefined || entry.viewState !== undefined) top.viewState = entry.viewState
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
past.push({ path: entry.path, line: entry.line, column: entry.column, viewState: entry.viewState })
|
|
75
|
+
if (past.length > cap) past.splice(0, past.length - cap)
|
|
76
|
+
future.length = 0
|
|
77
|
+
},
|
|
78
|
+
back(): NavEntry | null {
|
|
79
|
+
if (past.length < 2) return null
|
|
80
|
+
const current = past.pop()
|
|
81
|
+
if (!current) return null
|
|
82
|
+
future.push(current)
|
|
83
|
+
return past[past.length - 1]
|
|
84
|
+
},
|
|
85
|
+
forward(): NavEntry | null {
|
|
86
|
+
const entry = future.pop()
|
|
87
|
+
if (!entry) return null
|
|
88
|
+
past.push(entry)
|
|
89
|
+
return entry
|
|
90
|
+
},
|
|
91
|
+
canBack(): boolean {
|
|
92
|
+
return past.length >= 2
|
|
93
|
+
},
|
|
94
|
+
canForward(): boolean {
|
|
95
|
+
return future.length > 0
|
|
96
|
+
},
|
|
97
|
+
peekBack(): NavEntry | null {
|
|
98
|
+
return past.length >= 2 ? past[past.length - 2] : null
|
|
99
|
+
},
|
|
100
|
+
peekForward(): NavEntry | null {
|
|
101
|
+
return future.length ? future[future.length - 1] : null
|
|
102
|
+
},
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -43,6 +43,8 @@
|
|
|
43
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; }
|
|
44
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; }
|
|
45
45
|
[data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
|
|
46
|
+
[data-edrv-view] .edrv-chip-btn:disabled { opacity: .35; cursor: default; }
|
|
47
|
+
[data-edrv-view] .edrv-chip-btn:disabled:hover { background: transparent; }
|
|
46
48
|
[data-edrv-view] .edrv-mn-add-line { background: rgba(15,157,88,.10); }
|
|
47
49
|
[data-edrv-view] .edrv-mn-gutter-add { color: #0f9d58 !important; font-weight: 700; display: flex !important; align-items: center; justify-content: center; }
|
|
48
50
|
[data-edrv-view] .edrv-mn-gutter-add::before { content: "+"; }
|
|
@@ -291,11 +293,5 @@
|
|
|
291
293
|
[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); }
|
|
292
294
|
[data-edrv-view] .edrv-search-toggle:disabled { opacity: .4; cursor: default; }
|
|
293
295
|
|
|
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
296
|
|
|
301
297
|
|