dsh-vscode-mode 0.1.44 → 0.1.46

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vscode-mode",
3
- "version": "0.1.44",
3
+ "version": "0.1.46",
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 | null> = {}
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] = parseChord(current[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
- * 当前命令的解析键位(未绑定/非法 → null)。
76
+ * 当前命令的解析键位集合(未绑定/非法 → 空数组;含多候选)。
74
77
  * @author ddj 2026年08月26号
75
78
  * @param id 命令 id
76
- * @returns 解析键位或 null
79
+ * @returns 解析键位数组(可能为空)
77
80
  */
78
- export function bindingOf(id: string): Binding | null {
79
- return parsed[id] ?? null
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 binding 解析键位(null = 未绑定,永不命中)
160
+ * @param bindings 解析键位或键位数组(null/空 = 未绑定,永不命中)
138
161
  * @returns 是否命中
139
162
  */
140
- export function matchEvent(e: { ctrlKey?: boolean; metaKey?: boolean; shiftKey?: boolean; altKey?: boolean; key?: string }, binding: Binding | null): boolean {
141
- if (!binding) return false
142
- if ((e.ctrlKey || e.metaKey) !== (binding.ctrl || binding.meta)) return false
143
- if (Boolean(e.shiftKey) !== binding.shift) return false
144
- if (Boolean(e.altKey) !== binding.alt) return false
145
- return normalizeKey(e.key ?? '') === binding.key
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
  /**
@@ -197,7 +197,16 @@ export async function gotoRefsAt(ed, position) {
197
197
  if (!path || !position) return
198
198
  const all = await findReferences(path, model.getValue(), position, false)
199
199
  const others = filterOtherRefs(all, path, position)
200
- if (!others.length) return
200
+ if (!others.length) {
201
+ // 0 条「其它」引用:降级查定义(参数/局部变量只被声明时引用为空,定义可经 host 降级链推导)
202
+ const defs = await findDefinition(path, model.getValue(), position)
203
+ const first = defs[0]
204
+ if (!first) return
205
+ const lspPos = monoToLsp(position.lineNumber, position.column)
206
+ if (sameFile(targetOpenPath(first), path) && posInRange(lspPos, first.lspRange)) return // 定义即自身:无其它可跳,保持静默
207
+ jumpToLocation(first)
208
+ return
209
+ }
201
210
  if (others.length === 1) {
202
211
  jumpToLocation(others[0])
203
212
  return
@@ -0,0 +1,101 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — Ctrl+hover 可导航标识符下划线提示。
4
+ * 背景:内嵌 Monaco 构建(0.42.0-dev)无 linkDetection contrib,原生不产生 ctrl+hover 下划线,
5
+ * 插件自绘:Ctrl/Meta 按住悬停在"语义 token 标识符"上时给词加下划线装饰,移出/松开即清除。
6
+ * 数据来源于语义 token(每 model+version 缓存 60s),不做逐词 LSP 查询。
7
+ * 作者 ddj 2026-09-02
8
+ */
9
+ import { pathOfModel, fetchSemanticTokens } from './lspClient.js'
10
+ import { decodeSemanticTokens, isNavigableTokenType } from '../../../shared/lsp.js'
11
+
12
+ const CACHE_TTL_MS = 60_000
13
+
14
+ /**
15
+ * 给一个 Monaco 编辑器绑定 Ctrl+hover 下划线提示(幂等)。
16
+ * @author ddj 2026年09月02号
17
+ * @param ed Monaco 编辑器
18
+ * @param monaco Monaco 实例
19
+ */
20
+ export function bindLspUnderline(ed, monaco) {
21
+ if (!ed || !monaco || ed.__edrvUnderline) return
22
+ ed.__edrvUnderline = true
23
+ let decoIds = []
24
+ let lastKey = null
25
+ let hoverSeq = 0
26
+ let cache = { path: null, version: -1, ranges: null, at: 0 }
27
+
28
+ const clear = () => {
29
+ if (decoIds.length) {
30
+ ed.deltaDecorations(decoIds, [])
31
+ decoIds = []
32
+ }
33
+ lastKey = null
34
+ }
35
+
36
+ /** 取当前文档语义 token 范围(模型版本 + TTL 缓存)。 */
37
+ const rangesOf = async (model) => {
38
+ const path = pathOfModel(model)
39
+ const version = model.getVersionId?.() ?? 0
40
+ if (cache.path === path && cache.version === version && cache.ranges && Date.now() - cache.at < CACHE_TTL_MS) {
41
+ return cache.ranges
42
+ }
43
+ const res = await fetchSemanticTokens(path, model.getValue()).catch(() => null)
44
+ const ranges = res && Array.isArray(res.data) ? decodeSemanticTokens(res.data) : []
45
+ cache = { path, version, ranges, at: Date.now() }
46
+ return ranges
47
+ }
48
+
49
+ ed.onMouseMove((event) => {
50
+ const native = event?.event
51
+ const ctrl = native?.ctrlKey ?? false
52
+ const meta = native?.metaKey ?? false
53
+ if (!ctrl && !meta) {
54
+ clear()
55
+ hoverSeq++
56
+ return
57
+ }
58
+ const pos = event?.target?.position
59
+ const model = ed.getModel()
60
+ if (!pos || !model) {
61
+ clear()
62
+ hoverSeq++
63
+ return
64
+ }
65
+ const word = model.getWordAtPosition(pos)
66
+ if (!word) {
67
+ clear()
68
+ hoverSeq++
69
+ return
70
+ }
71
+ const seq = ++hoverSeq
72
+ const path = pathOfModel(model)
73
+ void rangesOf(model).then((ranges) => {
74
+ if (seq !== hoverSeq) return // 鼠标已移走,忽略过期结果
75
+ const lineIndex = Math.max(0, word.startLineNumber - 1)
76
+ const startCol = Math.max(0, word.startColumn - 1)
77
+ const endCol = Math.max(startCol, word.endColumn - 1)
78
+ const hit = ranges.some((r) => {
79
+ if (r.start.line !== lineIndex) return false
80
+ return isNavigableTokenType(r.type) && r.start.character <= startCol && r.end.character >= endCol
81
+ })
82
+ if (!hit) {
83
+ if (lastKey !== null) clear()
84
+ return
85
+ }
86
+ const key = String(path) + ':' + word.startColumn + ':' + word.endColumn
87
+ if (key === lastKey) return
88
+ lastKey = key
89
+ decoIds = ed.deltaDecorations(decoIds, [{
90
+ range: new monaco.Range(word.startLineNumber, word.startColumn, word.endLineNumber, word.endColumn),
91
+ options: { inlineClassName: 'edrv-nav-underline' },
92
+ }])
93
+ })
94
+ })
95
+
96
+ ed.onMouseLeave(() => clear())
97
+ ed.onDidBlurEditorText?.(() => clear())
98
+ }
99
+
100
+ /** 兼容占位(与 providers 的 hideReferencesOverlay 对称,无需真正释放)。 */
101
+ export function disposeLspUnderline() {}
@@ -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
+ }
@@ -40,9 +40,13 @@
40
40
  [data-edrv-view] .edrv-lsp-status-dot.ready { background: var(--dsw-alias-state-success-primary, #0f9d58); }
41
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
42
  @keyframes edrv-lsp-pulse { 50% { opacity: .45; } }
43
+ /* Ctrl+hover 可导航标识符下划线提示(绑定 Monaco inlineClassName 装饰) */
44
+ [data-edrv-view] .edrv-nav-underline { text-decoration: underline; text-decoration-color: var(--dsw-alias-interactive-primary, #4fc1ff); text-underline-offset: 1px; cursor: pointer; }
43
45
  [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
46
  [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
47
  [data-edrv-view] .edrv-chip-btn:hover { background: var(--dsw-alias-interactive-bg-hover, rgba(255,255,255,.06)); }
48
+ [data-edrv-view] .edrv-chip-btn:disabled { opacity: .35; cursor: default; }
49
+ [data-edrv-view] .edrv-chip-btn:disabled:hover { background: transparent; }
46
50
  [data-edrv-view] .edrv-mn-add-line { background: rgba(15,157,88,.10); }
47
51
  [data-edrv-view] .edrv-mn-gutter-add { color: #0f9d58 !important; font-weight: 700; display: flex !important; align-items: center; justify-content: center; }
48
52
  [data-edrv-view] .edrv-mn-gutter-add::before { content: "+"; }