dsh-vscode-mode 0.1.1

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.
Files changed (163) hide show
  1. package/README.md +95 -0
  2. package/assets/compare_idle.png +0 -0
  3. package/assets/compare_select.png +0 -0
  4. package/assets/vendor/monaco/vs/abap-D-t0cyap.js +1 -0
  5. package/assets/vendor/monaco/vs/apex-CcIm7xu6.js +1 -0
  6. package/assets/vendor/monaco/vs/assets/css.worker-URu8fCFR.js +97 -0
  7. package/assets/vendor/monaco/vs/assets/editor.worker-lj3bdIIn.js +30 -0
  8. package/assets/vendor/monaco/vs/assets/editorWebWorkerMain-CA_vMoUU.js +30 -0
  9. package/assets/vendor/monaco/vs/assets/html.worker-D1SL3iM8.js +474 -0
  10. package/assets/vendor/monaco/vs/assets/json.worker-CoJx_OPf.js +62 -0
  11. package/assets/vendor/monaco/vs/assets/ts.worker-BWKtMYOk.js +67722 -0
  12. package/assets/vendor/monaco/vs/azcli-BA0tQDCg.js +1 -0
  13. package/assets/vendor/monaco/vs/basic-languages/monaco.contribution.js +1 -0
  14. package/assets/vendor/monaco/vs/bat-C397hTD6.js +1 -0
  15. package/assets/vendor/monaco/vs/bicep-DF5aW17k.js +2 -0
  16. package/assets/vendor/monaco/vs/cameligo-plsz8qhj.js +1 -0
  17. package/assets/vendor/monaco/vs/clojure-Y2auQMzK.js +1 -0
  18. package/assets/vendor/monaco/vs/coffee-Bu45yuWE.js +1 -0
  19. package/assets/vendor/monaco/vs/cpp-CkKPQIni.js +1 -0
  20. package/assets/vendor/monaco/vs/csharp-CX28MZyh.js +1 -0
  21. package/assets/vendor/monaco/vs/csp-D8uWnyxW.js +1 -0
  22. package/assets/vendor/monaco/vs/css-CaeNmE3S.js +3 -0
  23. package/assets/vendor/monaco/vs/css.worker-CyhWkhHo.js +1 -0
  24. package/assets/vendor/monaco/vs/cssMode-CV6Ay48H.js +1 -0
  25. package/assets/vendor/monaco/vs/cypher-DVThT8BS.js +1 -0
  26. package/assets/vendor/monaco/vs/dart-CmGfCvrO.js +1 -0
  27. package/assets/vendor/monaco/vs/dockerfile-CZqqYdch.js +1 -0
  28. package/assets/vendor/monaco/vs/ecl-30fUercY.js +1 -0
  29. package/assets/vendor/monaco/vs/editor/editor.main.css +1 -0
  30. package/assets/vendor/monaco/vs/editor/editor.main.js +1 -0
  31. package/assets/vendor/monaco/vs/editor/editor.worker.js +1 -0
  32. package/assets/vendor/monaco/vs/editor-KLE6jdfb.js +831 -0
  33. package/assets/vendor/monaco/vs/editor.js +1 -0
  34. package/assets/vendor/monaco/vs/editorWorkerHost-fVE1cjcC.js +35 -0
  35. package/assets/vendor/monaco/vs/elixir-xjPaIfzF.js +1 -0
  36. package/assets/vendor/monaco/vs/flow9-DqtmStfK.js +1 -0
  37. package/assets/vendor/monaco/vs/freemarker2-FWreY7v7.js +3 -0
  38. package/assets/vendor/monaco/vs/fsharp-BOMdg4U1.js +1 -0
  39. package/assets/vendor/monaco/vs/go-D_hbi-Jt.js +1 -0
  40. package/assets/vendor/monaco/vs/graphql-CKUU4kLG.js +1 -0
  41. package/assets/vendor/monaco/vs/handlebars-CZi4pHCH.js +1 -0
  42. package/assets/vendor/monaco/vs/hcl-DTaboeZW.js +1 -0
  43. package/assets/vendor/monaco/vs/html-j_7ZTNRU.js +1 -0
  44. package/assets/vendor/monaco/vs/html.worker-CA3iAimZ.js +1 -0
  45. package/assets/vendor/monaco/vs/htmlMode-27by_KM4.js +1 -0
  46. package/assets/vendor/monaco/vs/index-049xrXrp.js +1 -0
  47. package/assets/vendor/monaco/vs/index-CBVt3dzv.js +5 -0
  48. package/assets/vendor/monaco/vs/index.js +1 -0
  49. package/assets/vendor/monaco/vs/ini-CsNwO04R.js +1 -0
  50. package/assets/vendor/monaco/vs/initialize-DL0l1TGY.js +1 -0
  51. package/assets/vendor/monaco/vs/java-CI4ZMsH9.js +1 -0
  52. package/assets/vendor/monaco/vs/javascript-DTJl4Jn1.js +1 -0
  53. package/assets/vendor/monaco/vs/json.worker-BizpAl9O.js +1 -0
  54. package/assets/vendor/monaco/vs/jsonMode-DLUiMUrI.js +1 -0
  55. package/assets/vendor/monaco/vs/julia-BwzEvaQw.js +1 -0
  56. package/assets/vendor/monaco/vs/kotlin-IUYPiTV8.js +1 -0
  57. package/assets/vendor/monaco/vs/language/css/css.worker.js +66 -0
  58. package/assets/vendor/monaco/vs/language/css/monaco.contribution.js +1 -0
  59. package/assets/vendor/monaco/vs/language/html/html.worker.js +443 -0
  60. package/assets/vendor/monaco/vs/language/html/monaco.contribution.js +1 -0
  61. package/assets/vendor/monaco/vs/language/json/json.worker.js +17 -0
  62. package/assets/vendor/monaco/vs/language/json/monaco.contribution.js +1 -0
  63. package/assets/vendor/monaco/vs/language/typescript/monaco.contribution.js +1 -0
  64. package/assets/vendor/monaco/vs/language/typescript/ts.worker.js +67693 -0
  65. package/assets/vendor/monaco/vs/less-C0eDYdqa.js +2 -0
  66. package/assets/vendor/monaco/vs/lexon-iON-Kj97.js +1 -0
  67. package/assets/vendor/monaco/vs/liquid-CLJYelW6.js +1 -0
  68. package/assets/vendor/monaco/vs/loader.js +1368 -0
  69. package/assets/vendor/monaco/vs/lspLanguageFeatures-BIkJOWLw.js +2 -0
  70. package/assets/vendor/monaco/vs/lua-DtygF91M.js +1 -0
  71. package/assets/vendor/monaco/vs/m3-CsR4AuFi.js +1 -0
  72. package/assets/vendor/monaco/vs/main-BEx-Fmlo.js +15 -0
  73. package/assets/vendor/monaco/vs/main-DsK8pnKg.js +3 -0
  74. package/assets/vendor/monaco/vs/markdown-C_rD0bIw.js +1 -0
  75. package/assets/vendor/monaco/vs/mdx-CpzZIPHF.js +1 -0
  76. package/assets/vendor/monaco/vs/mips-CiYP61RB.js +1 -0
  77. package/assets/vendor/monaco/vs/monaco.contribution-9cKT3C7t.js +1 -0
  78. package/assets/vendor/monaco/vs/monaco.contribution-BE88ZNGY.js +1 -0
  79. package/assets/vendor/monaco/vs/monaco.contribution-BPhsneLd.js +1 -0
  80. package/assets/vendor/monaco/vs/monaco.contribution-BgRy6xDf.js +1 -0
  81. package/assets/vendor/monaco/vs/msdax-C38-sJlp.js +1 -0
  82. package/assets/vendor/monaco/vs/mysql-CdtbpvbG.js +1 -0
  83. package/assets/vendor/monaco/vs/nls/lang/zh-cn.d.ts +1 -0
  84. package/assets/vendor/monaco/vs/nls/lang/zh-cn.js +9 -0
  85. package/assets/vendor/monaco/vs/nls.messages-loader.js +1 -0
  86. package/assets/vendor/monaco/vs/objective-c-CntZFaHX.js +1 -0
  87. package/assets/vendor/monaco/vs/pascal-r6kuqfl_.js +1 -0
  88. package/assets/vendor/monaco/vs/pascaligo-BiXoTmXh.js +1 -0
  89. package/assets/vendor/monaco/vs/perl-DABw_TcH.js +1 -0
  90. package/assets/vendor/monaco/vs/pgsql-me_jFXeX.js +1 -0
  91. package/assets/vendor/monaco/vs/php-D_kh-9LK.js +1 -0
  92. package/assets/vendor/monaco/vs/pla-VfZjczW0.js +1 -0
  93. package/assets/vendor/monaco/vs/postiats-BBSzz8Pk.js +1 -0
  94. package/assets/vendor/monaco/vs/powerquery-Dt-g_2cc.js +1 -0
  95. package/assets/vendor/monaco/vs/powershell-B-7ap1zc.js +1 -0
  96. package/assets/vendor/monaco/vs/protobuf-BmtuEB1A.js +2 -0
  97. package/assets/vendor/monaco/vs/pug-BRpRNeEb.js +1 -0
  98. package/assets/vendor/monaco/vs/python-CqWUUgfu.js +1 -0
  99. package/assets/vendor/monaco/vs/qsharp-BzsFaUU9.js +1 -0
  100. package/assets/vendor/monaco/vs/r-f8dDdrp4.js +1 -0
  101. package/assets/vendor/monaco/vs/razor-hok5y9Km.js +1 -0
  102. package/assets/vendor/monaco/vs/redis-fvZQY4PI.js +1 -0
  103. package/assets/vendor/monaco/vs/redshift-45Et0LQi.js +1 -0
  104. package/assets/vendor/monaco/vs/restructuredtext-C7UUFKFD.js +1 -0
  105. package/assets/vendor/monaco/vs/ruby-CZO8zYTz.js +1 -0
  106. package/assets/vendor/monaco/vs/rust-Bfetafyc.js +1 -0
  107. package/assets/vendor/monaco/vs/sb-3GYllVck.js +1 -0
  108. package/assets/vendor/monaco/vs/scala-foMgrKo1.js +1 -0
  109. package/assets/vendor/monaco/vs/scheme-CHdMtr7p.js +1 -0
  110. package/assets/vendor/monaco/vs/scss-C1cmLt9V.js +3 -0
  111. package/assets/vendor/monaco/vs/shell-ClXCKCEW.js +1 -0
  112. package/assets/vendor/monaco/vs/solidity-MZ6ExpPy.js +1 -0
  113. package/assets/vendor/monaco/vs/sophia-DWkuSsPQ.js +1 -0
  114. package/assets/vendor/monaco/vs/sparql-AUGFYSyk.js +1 -0
  115. package/assets/vendor/monaco/vs/sql-32GpJSV2.js +1 -0
  116. package/assets/vendor/monaco/vs/st-CuDFIVZ_.js +1 -0
  117. package/assets/vendor/monaco/vs/swift-t-PfMj7W.js +1 -0
  118. package/assets/vendor/monaco/vs/systemverilog-Ch4vA8Yt.js +1 -0
  119. package/assets/vendor/monaco/vs/tcl-D74tq1nH.js +1 -0
  120. package/assets/vendor/monaco/vs/toggleHighContrast-qGX7E9o7.js +61 -0
  121. package/assets/vendor/monaco/vs/ts.worker-2QLmBukE.js +1 -0
  122. package/assets/vendor/monaco/vs/tsMode-B0S_kp2q.js +11 -0
  123. package/assets/vendor/monaco/vs/twig-C6taOxMV.js +1 -0
  124. package/assets/vendor/monaco/vs/typescript-_2t_511t.js +1 -0
  125. package/assets/vendor/monaco/vs/typespec-8OfoLt6R.js +1 -0
  126. package/assets/vendor/monaco/vs/vb-Dyb2648j.js +1 -0
  127. package/assets/vendor/monaco/vs/wgsl-BhLXMOR0.js +298 -0
  128. package/assets/vendor/monaco/vs/workers-BBttULjf.js +1 -0
  129. package/assets/vendor/monaco/vs/xml-cZjNDqDT.js +1 -0
  130. package/assets/vendor/monaco/vs/yaml-A1fOIdH6.js +1 -0
  131. package/cordis.patch.yml +5 -0
  132. package/lib/client.js +1914 -0
  133. package/lib/client.js.map +1 -0
  134. package/lib/index.js +1298 -0
  135. package/lib/index.js.map +1 -0
  136. package/package.json +60 -0
  137. package/src/capture.ts +73 -0
  138. package/src/client/events.ts +36 -0
  139. package/src/client/index.ts +37 -0
  140. package/src/client/monaco/diffRender.ts +162 -0
  141. package/src/client/monaco/loader.ts +71 -0
  142. package/src/client/rpc.ts +28 -0
  143. package/src/client/state/records.ts +90 -0
  144. package/src/client/state/regions.ts +81 -0
  145. package/src/client/styles/css.d.ts +2 -0
  146. package/src/client/styles/editor.css +87 -0
  147. package/src/client/ui/DiffBadge.ts +51 -0
  148. package/src/client/ui/DiffBarEmpty.ts +37 -0
  149. package/src/client/ui/DiffBox.ts +79 -0
  150. package/src/client/ui/DiffLauncher.ts +124 -0
  151. package/src/client/ui/EditorView.ts +577 -0
  152. package/src/client/ui/QuickOpen.ts +91 -0
  153. package/src/client/ui/shared.ts +28 -0
  154. package/src/index.ts +43 -0
  155. package/src/model.ts +197 -0
  156. package/src/registry.ts +35 -0
  157. package/src/revert.ts +107 -0
  158. package/src/routes.ts +138 -0
  159. package/src/rpc.ts +295 -0
  160. package/src/shared/rpc.ts +67 -0
  161. package/src/shared/types.ts +121 -0
  162. package/src/store.ts +244 -0
  163. package/src/workspace.ts +74 -0
@@ -0,0 +1,577 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — EditorView:中央 VSCode 式文件编辑器(编排层)。
4
+ * 迁移自原 src/client/index.ts 的 EditorView,语义不改;差异自绘已抽到 monaco/diffRender。
5
+ * 职责:页签/QuickOpen/Monaco 编辑器/差异审查(DiffBox/Launcher/Badge 事件装配)/状态栏/自动保存。
6
+ * 作者 ddj 2026-08-20
7
+ */
8
+ import React from 'react'
9
+ import { dbg, rpc } from '../rpc.js'
10
+ import { emitRefresh } from '../events.js'
11
+ import { langOf, loadMonaco } from '../monaco/loader.js'
12
+ import { createDiffRenderer } from '../monaco/diffRender.js'
13
+ import { ST, callIdAttr, noopHunk, summarize } from '../state/records.js'
14
+ import { diffRegions } from '../state/regions.js'
15
+ import { QuickOpen } from './QuickOpen.js'
16
+ import { DiffBox } from './DiffBox.js'
17
+ import { DiffBarEmpty } from './DiffBarEmpty.js'
18
+ import { DiffLauncher } from './DiffLauncher.js'
19
+
20
+ /**
21
+ * 中央编辑区:文件页签(脏点/关闭/打开路径)+ Ctrl+P 搜索 + Monaco 编辑器 +
22
+ * 底部差异条(DiffBox/空态)+ 全局差异下拉(DiffLauncher)+ 状态栏。
23
+ * @param props.sessionId 会话 id
24
+ * @param props.schedule 延时调度(ctx.timeout)
25
+ */
26
+ export function EditorView(props) {
27
+ const sessionId = props?.sessionId
28
+ const schedule = props.schedule
29
+ const [monaco, setMonaco] = React.useState(null)
30
+ const [monacoErr, setMonacoErr] = React.useState(null)
31
+ const [records, setRecords] = React.useState({})
32
+ const [tabs, setTabs] = React.useState([])
33
+ const [active, setActive] = React.useState(null)
34
+ const [dirtyMap, setDirtyMap] = React.useState({})
35
+ const [content, setContent] = React.useState(null)
36
+ const [status, setStatus] = React.useState('')
37
+ const [error, setError] = React.useState(null)
38
+ const [openInput, setOpenInput] = React.useState(false)
39
+ const [pathDraft, setPathDraft] = React.useState('')
40
+ const [cursor, setCursor] = React.useState('')
41
+ const [launcherOpen, setLauncherOpen] = React.useState(false)
42
+ const [launcherTab, setLauncherTab] = React.useState('pending')
43
+ const [hoverAct, setHoverAct] = React.useState(null) // { region, top } 编辑区 hover 差异块的 Keep/Undo 浮层
44
+ const [diffIdx, setDiffIdx] = React.useState(0) // 当前文件内差异位置(x/x 显示)
45
+ const [fileIdx, setFileIdx] = React.useState(0) // 全局差异文件位置(x/x 文件 显示)
46
+ const editorRef = React.useRef(null)
47
+ const monacoRef = React.useRef(null)
48
+ const modelsRef = React.useRef(new Map())
49
+ const saveTimerRef = React.useRef(null)
50
+ const loadSeqRef = React.useRef(0)
51
+ const programmaticRef = React.useRef(false)
52
+ const bootRef = React.useRef(false)
53
+ const pendingFocusRef = React.useRef(null) // { path, region } 内容加载后跳转
54
+ const hoverRegionsRef = React.useRef([]) // 当前 pending 区域镜像(稳定回调读取)
55
+ const lineRegionMapRef = React.useRef(new Map()) // 行 → 区域 映射(hover 命中)
56
+ const hoverKeyRef = React.useRef(null) // 当前 hover 区域 key(区域不变不重渲染)
57
+ const hoverTopRef = React.useRef(null) // 当前 hover 浮窗 top(滚动后位置变化才重定位)
58
+ const hideTimerRef = React.useRef(null) // 延迟隐藏计时器(防闪烁)
59
+ const batchBusyRef = React.useRef(false) // 批量 Keep All/Undo All 防重入
60
+ const diffRendererRef = React.useRef(null)
61
+ if (!diffRendererRef.current) diffRendererRef.current = createDiffRenderer((sid, t) => dbg(sid, t))
62
+
63
+ const currentRecords = React.useMemo(() => {
64
+ const list = []
65
+ for (const rec of Object.values(records)) if (rec.path === active) list.push(rec)
66
+ return list
67
+ }, [records, active])
68
+
69
+ const regions = React.useMemo(() => diffRegions(currentRecords, content).filter((r) => !r.superseded), [currentRecords, content])
70
+ // useMemo 稳定引用:否则 hover 等重渲染会让 view zone effect 反复重建(- 号闪烁)
71
+ const pendingRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && !r.stale), [regions])
72
+ const staleRegions = React.useMemo(() => regions.filter((r) => r.status === ST.PENDING && r.stale), [regions])
73
+ hoverRegionsRef.current = pendingRegions
74
+ // 行 → 差异区域 映射(hover O(1) 命中;每行归属其区域)
75
+ const lineRegionMap = React.useMemo(() => {
76
+ const map = new Map()
77
+ for (const r of pendingRegions) {
78
+ if (r.start === undefined || r.end === undefined) continue
79
+ const last = Math.max(r.start, r.end - 1)
80
+ for (let ln = r.start; ln <= last; ln++) if (!map.has(ln)) map.set(ln, r)
81
+ }
82
+ return map
83
+ }, [pendingRegions])
84
+ lineRegionMapRef.current = lineRegionMap
85
+ const sum = React.useMemo(() => summarize(Object.values(records)), [records])
86
+
87
+ const addTab = (path, select) => {
88
+ setTabs((prev) => {
89
+ if (prev.some((t) => t.path === path)) return prev
90
+ return prev.concat([{ path }])
91
+ })
92
+ if (select) setActive(path)
93
+ }
94
+
95
+ const closeTab = (path) => {
96
+ flushSave()
97
+ setTabs((prev) => {
98
+ const idx = prev.findIndex((t) => t.path === path)
99
+ if (idx < 0) return prev
100
+ const next = prev.filter((t) => t.path !== path)
101
+ if (active === path) setActive(next[idx] ? next[idx].path : (next[idx - 1] ? next[idx - 1].path : null))
102
+ return next
103
+ })
104
+ }
105
+
106
+ const refreshRecords = () => {
107
+ if (!sessionId) return
108
+ rpc('edrv.list', { sessionId }).then((res) => {
109
+ if (!res || !res.ok || !Array.isArray(res.records)) return
110
+ const map = {}
111
+ for (const r of res.records) map[r.callId] = r
112
+ setRecords(map)
113
+ }).catch((e) => setError('list异常:' + String(e)))
114
+ }
115
+
116
+ const loadContent = (path, sid) => {
117
+ const seq = ++loadSeqRef.current
118
+ rpc('edrv.read', { sessionId: sid, path }).then((res) => {
119
+ if (seq !== loadSeqRef.current || path !== active) return
120
+ if (res && res.ok) { setContent(res.content); setStatus('已加载') }
121
+ else { setError(res?.error ? String(res.error) : '读取失败'); setStatus('读取失败') }
122
+ }).catch((e) => { if (seq === loadSeqRef.current) { setError('read异常:' + String(e)); setStatus('读取失败') } })
123
+ }
124
+
125
+ // 挂载轮询 + 事件订阅
126
+ React.useEffect(() => {
127
+ if (!sessionId) return
128
+ refreshRecords()
129
+ const t = setInterval(refreshRecords, 5000)
130
+ const onRefresh = () => refreshRecords()
131
+ window.addEventListener('edrv:refresh', onRefresh)
132
+ return () => { clearInterval(t); window.removeEventListener('edrv:refresh', onRefresh) }
133
+ }, [sessionId])
134
+
135
+ React.useEffect(() => {
136
+ const onOpen = (e) => {
137
+ const p = e?.detail?.path
138
+ if (p) addTab(p, true)
139
+ }
140
+ const onShowLauncher = () => setLauncherOpen(true)
141
+ window.addEventListener('edrv:open-editor', onOpen)
142
+ window.addEventListener('edrv:show-launcher', onShowLauncher)
143
+ return () => {
144
+ window.removeEventListener('edrv:open-editor', onOpen)
145
+ window.removeEventListener('edrv:show-launcher', onShowLauncher)
146
+ }
147
+ }, [sessionId])
148
+
149
+ // localStorage v2 恢复页签
150
+ React.useEffect(() => {
151
+ if (bootRef.current || !sessionId) return
152
+ bootRef.current = true
153
+ try {
154
+ const raw = localStorage.getItem('edrv.editor.v2.' + String(sessionId))
155
+ if (raw) {
156
+ const saved = JSON.parse(raw)
157
+ if (Array.isArray(saved.tabs) && saved.tabs.length) {
158
+ setTabs(saved.tabs.map((p) => ({ path: p })))
159
+ if (typeof saved.active === 'string') setActive(saved.active)
160
+ }
161
+ }
162
+ } catch (e) { /* 损坏忽略 */ }
163
+ }, [sessionId])
164
+
165
+ React.useEffect(() => {
166
+ if (!sessionId) return
167
+ try { localStorage.setItem('edrv.editor.v2.' + String(sessionId), JSON.stringify({ tabs: tabs.map((t) => t.path), active })) }
168
+ catch (e) { /* 忽略 */ }
169
+ }, [tabs, active, sessionId])
170
+
171
+ React.useEffect(() => {
172
+ if (!active) return
173
+ setContent(null); setStatus('加载中…')
174
+ loadContent(active, sessionId)
175
+ }, [active, sessionId])
176
+
177
+ React.useEffect(() => {
178
+ if (monaco || monacoErr) return
179
+ let alive = true
180
+ loadMonaco().then((m) => { if (alive) setMonaco(m) }).catch((e) => { if (alive) { setMonacoErr(String(e?.message ?? e)); setStatus('Monaco 不可用') } })
181
+ return () => { alive = false }
182
+ }, [monaco, monacoErr])
183
+
184
+ monacoRef.current = monaco
185
+
186
+ const getModel = (path, text) => {
187
+ const cache = modelsRef.current
188
+ let model = cache.get(path)
189
+ if (!model) {
190
+ model = window.monaco.editor.createModel(text ?? '', langOf(path), window.monaco.Uri.parse('edrv:///' + encodeURI(path)))
191
+ cache.set(path, model)
192
+ } else if (text !== undefined && model.getValue() !== text) {
193
+ programmaticRef.current = true
194
+ model.setValue(text)
195
+ programmaticRef.current = false
196
+ }
197
+ return model
198
+ }
199
+
200
+ const flushSave = () => {
201
+ if (saveTimerRef.current) { saveTimerRef.current(); saveTimerRef.current = null }
202
+ }
203
+
204
+ const doSave = (silent) => {
205
+ const ed = editorRef.current
206
+ if (!active || !ed) return
207
+ const text = ed.getValue()
208
+ if (!silent) setStatus('保存中…')
209
+ rpc('edrv.save', { sessionId, path: active, content: text }).then((res) => {
210
+ if (res && res.ok) {
211
+ setStatus('已保存 ' + new Date().toTimeString().slice(0, 8))
212
+ setContent(text)
213
+ setDirtyMap((d) => Object.assign({}, d, { [active]: false }))
214
+ refreshRecords()
215
+ emitRefresh()
216
+ } else { setStatus('保存失败'); setError(res?.error ? String(res.error) : '保存失败') }
217
+ }).catch((e) => { setStatus('保存失败'); setError('保存异常:' + String(e)) })
218
+ }
219
+
220
+ const onEdit = () => {
221
+ const ed = editorRef.current
222
+ if (!ed || !active) return
223
+ setDirtyMap((d) => Object.assign({}, d, { [active]: true }))
224
+ setStatus('编辑中…')
225
+ if (saveTimerRef.current) saveTimerRef.current()
226
+ saveTimerRef.current = schedule(() => doSave(true), 700)
227
+ }
228
+
229
+ // model 同步(当前内容)
230
+ React.useEffect(() => {
231
+ if (!monaco || !editorRef.current || !active || content === null) return
232
+ const ed = editorRef.current
233
+ const model = getModel(active, content)
234
+ if (ed.getModel() !== model) ed.setModel(model)
235
+ }, [monaco, active, content])
236
+
237
+ // 行内差异自绘(decorations / view zones / minus overlay)→ diffRenderer
238
+ React.useEffect(() => {
239
+ if (!monaco || !editorRef.current || !active || content === null) return
240
+ diffRendererRef.current.render(monaco, editorRef.current, pendingRegions, sessionId)
241
+ }, [monaco, active, content, pendingRegions])
242
+
243
+ React.useEffect(() => () => {
244
+ flushSave()
245
+ diffRendererRef.current?.dispose?.()
246
+ if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
247
+ for (const m of modelsRef.current.values()) m.dispose()
248
+ modelsRef.current.clear()
249
+ const root = editorRef.current && editorRef.current.getDomNode ? editorRef.current.getDomNode() : null
250
+ if (root) {
251
+ const ov = root.querySelector('.edrv-minus-overlay')
252
+ if (ov) ov.remove()
253
+ }
254
+ }, [])
255
+
256
+ // 普通 Monaco 编辑器(可编辑、单列行号)+ 自绘行内差异(decoration 绿底+ / view zone 删除块)
257
+ const ensureEditor = React.useCallback((node) => {
258
+ if (!node) {
259
+ if (editorRef.current) { editorRef.current.dispose(); editorRef.current = null }
260
+ return
261
+ }
262
+ if (editorRef.current || !monacoRef.current) return
263
+ const m = monacoRef.current
264
+ const ed = m.editor.create(node, {
265
+ value: '',
266
+ language: 'plaintext',
267
+ theme: 'vs',
268
+ fontFamily: 'var(--ds-font-family-code, ui-monospace, monospace)',
269
+ fontSize: 13,
270
+ lineHeight: 20,
271
+ minimap: { enabled: true, scale: 1 },
272
+ glyphMargin: true,
273
+ lineDecorationsWidth: 16,
274
+ automaticLayout: true,
275
+ scrollBeyondLastLine: false,
276
+ wordWrap: 'off',
277
+ tabSize: 2,
278
+ renderWhitespace: 'selection',
279
+ smoothScrolling: true,
280
+ cursorBlinking: 'smooth',
281
+ padding: { top: 8 },
282
+ })
283
+ ed.addCommand(m.KeyMod.CtrlCmd | m.KeyCode.KeyS, () => { flushSave(); doSave(false) })
284
+ ed.onDidChangeModelContent(() => {
285
+ if (!ed.getModel() || programmaticRef.current) return
286
+ onEdit()
287
+ })
288
+ ed.onDidChangeCursorPosition((e) => {
289
+ setCursor('Ln ' + e.position.lineNumber + ', Col ' + e.position.column)
290
+ })
291
+ // hover 差异块 → 浮出 Keep/Undo(req:鼠标移到编辑区差异块时显示)
292
+ // 防闪烁:① 区域不变不 setState(浮窗锚定差异块起始行,不跟随鼠标);② 延迟隐藏;
293
+ // ③ 浮窗自身 onMouseEnter 取消隐藏计时(鼠标在浮窗与编辑器间移动不闪)。
294
+ const hideSoon = () => {
295
+ if (hideTimerRef.current) return
296
+ hideTimerRef.current = setTimeout(() => {
297
+ hideTimerRef.current = null
298
+ hoverKeyRef.current = null
299
+ hoverTopRef.current = null
300
+ setHoverAct(null)
301
+ }, 180)
302
+ }
303
+ ed.onMouseMove((e) => {
304
+ const line = e?.target?.position?.lineNumber
305
+ const map = lineRegionMapRef.current
306
+ if (!line || !map.size) { hideSoon(); return }
307
+ const hit = map.get(line)
308
+ if (!hit) { hideSoon(); return }
309
+ const key = callIdAttr(hit.callId, hit.idx)
310
+ const top = Math.max(0, ed.getTopForLineNumber(Math.max(1, hit.start)) - ed.getScrollTop())
311
+ if (hoverKeyRef.current === key && hoverTopRef.current === top) return // 同区域同位置不重复更新
312
+ hoverKeyRef.current = key
313
+ hoverTopRef.current = top
314
+ if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null }
315
+ setHoverAct({ region: hit, top })
316
+ })
317
+ ed.onMouseLeave(hideSoon)
318
+ editorRef.current = ed
319
+ }, [])
320
+
321
+ // 打开文件后的差异聚焦跳转(内容/差异就绪后执行一次)
322
+ React.useEffect(() => {
323
+ const pf = pendingFocusRef.current
324
+ if (!pf || pf.path !== active || content === null) return
325
+ const target = pf.region || pendingRegions[0]
326
+ const ed = editorRef.current
327
+ if (!target || !ed) return
328
+ pendingFocusRef.current = null
329
+ ed.revealLineInCenter(Math.max(1, target.start ?? 1))
330
+ ed.setPosition({ lineNumber: Math.max(1, target.start ?? 1), column: 1 })
331
+ ed.focus()
332
+ }, [active, content, pendingRegions])
333
+
334
+ const jumpTo = (region) => {
335
+ if (editorRef.current) {
336
+ editorRef.current.revealLineInCenter(Math.max(1, region?.start ?? 1))
337
+ editorRef.current.setPosition({ lineNumber: Math.max(1, region?.start ?? 1), column: 1 })
338
+ editorRef.current.focus()
339
+ } else {
340
+ pendingFocusRef.current = { path: active, region }
341
+ }
342
+ }
343
+
344
+ // 索引校正:差异/文件被处理后 pending 列表变化,clamp 到有效范围
345
+ React.useEffect(() => {
346
+ if (diffIdx >= pendingRegions.length && pendingRegions.length > 0) setDiffIdx(pendingRegions.length - 1)
347
+ else if (pendingRegions.length === 0) setDiffIdx(0)
348
+ }, [pendingRegions.length])
349
+ React.useEffect(() => {
350
+ const i = sum.pendingFiles.findIndex((f) => f.path === active)
351
+ if (i >= 0) { if (i !== fileIdx) setFileIdx(i) }
352
+ }, [sum.pendingFiles, active])
353
+
354
+ // 上下箭头:当前文件内差异切换(x/x)
355
+ const gotoDiff = (delta) => {
356
+ if (!pendingRegions.length) return
357
+ const next = (diffIdx + delta + pendingRegions.length) % pendingRegions.length
358
+ setDiffIdx(next)
359
+ jumpTo(pendingRegions[next])
360
+ }
361
+ // 左右箭头:全局差异文件切换(x/x 文件),打开并跳转
362
+ const gotoFile = (delta) => {
363
+ if (!sum.pendingFiles.length) return
364
+ const next = (fileIdx + delta + sum.pendingFiles.length) % sum.pendingFiles.length
365
+ setFileIdx(next)
366
+ openFile(sum.pendingFiles[next].path, true)
367
+ }
368
+
369
+ const reloadFile = () => {
370
+ if (!active) return
371
+ loadContent(active, sessionId)
372
+ refreshRecords()
373
+ }
374
+
375
+ /**
376
+ * 对单个差异区域执行采纳/不采纳。
377
+ * @author ddj 2026年08月20号
378
+ * @param region 差异区域(callId/idx/create)
379
+ * @param reject true=不采纳(回滚),false=采纳
380
+ * @param silent 批量时抑制中间 reload/报错,由批量方统一收尾
381
+ * @returns Promise<boolean> 是否成功
382
+ */
383
+ const actHunk = (region, reject, silent) => {
384
+ const method = reject ? 'edrv.reject' : 'edrv.accept'
385
+ return rpc(method, { sessionId, callId: region.callId, scope: region.create ? 'call' : 'hunk', hunkIndex: region.idx }).then((res) => {
386
+ if (res && res.ok) {
387
+ setRecords((prev) => Object.assign({}, prev, { [region.callId]: res.record }))
388
+ if (!silent) { reloadFile(); emitRefresh() }
389
+ return true
390
+ }
391
+ if (!silent) setError(res?.error ? String(res.error) : '操作失败')
392
+ return false
393
+ }).catch((e) => {
394
+ if (!silent) setError('操作异常:' + String(e))
395
+ return false
396
+ })
397
+ }
398
+
399
+ const acceptFile = () => { for (const r of pendingRegions) actHunk(r, false) }
400
+ const undoFile = () => { for (const r of [...pendingRegions].reverse()) actHunk(r, true) }
401
+
402
+ // 所有差异文件的待处理 hunk 列表(二级菜单 Keep All / Undo All 用)
403
+ const allPending = React.useMemo(() => {
404
+ const out = []
405
+ for (const rec of Object.values(records)) {
406
+ if (rec.superseded === true) continue
407
+ const perHunk = Array.isArray(rec.decisions?.perHunk) ? rec.decisions.perHunk : []
408
+ const hunks = Array.isArray(rec.hunks) ? rec.hunks : []
409
+ for (let i = 0; i < hunks.length; i++) {
410
+ const st = perHunk.length ? perHunk[i] : rec.decisions.call
411
+ if (st === 'pending' && !noopHunk(rec, hunks[i])) out.push({ callId: rec.callId, idx: i, create: rec.create === true, at: rec.at })
412
+ }
413
+ }
414
+ return out
415
+ }, [records])
416
+
417
+ /**
418
+ * 批量处理所有差异文件:采纳全部 / 不采纳全部。不采纳按 at 降序(新改先回滚)串行执行。
419
+ * @author ddj 2026年08月20号
420
+ * @param reject true=全部不采纳(回滚),false=全部采纳
421
+ */
422
+ const actAllPending = (reject) => {
423
+ if (batchBusyRef.current || !allPending.length) return
424
+ batchBusyRef.current = true
425
+ const list = reject ? [...allPending].sort((a, b) => (a.at < b.at ? 1 : a.at > b.at ? -1 : b.idx - a.idx)) : allPending
426
+ let ok = 0, fail = 0
427
+ const run = async () => {
428
+ for (const r of list) {
429
+ if (await actHunk(r, reject, true)) ok++
430
+ else fail++
431
+ }
432
+ batchBusyRef.current = false
433
+ reloadFile()
434
+ emitRefresh()
435
+ setStatus((reject ? '已不采纳 ' : '已采纳 ') + ok + ' 处差异' + (fail ? ',' + fail + ' 处失败' : ''))
436
+ if (fail) setError(fail + ' 处差异处理失败(可能已被后续修改影响),可刷新后重试')
437
+ }
438
+ run()
439
+ }
440
+ const acceptAllFiles = () => actAllPending(false)
441
+ const undoAllFiles = () => actAllPending(true)
442
+ const rollbackFile = () => {
443
+ if (!active) return
444
+ if (!window.confirm('回滚当前文件到修改前状态?(相关差异将归档)')) return
445
+ rpc('edrv.rollback', { sessionId, path: active }).then((res) => {
446
+ if (res && res.ok) {
447
+ setStatus('已回滚'); reloadFile()
448
+ emitRefresh()
449
+ } else setError(res?.error ? String(res.error) : '回滚失败')
450
+ }).catch((e) => setError('回滚异常:' + String(e)))
451
+ }
452
+
453
+ const openFile = (path, focusDiff) => {
454
+ if (!path) return
455
+ addTab(path, true)
456
+ if (focusDiff) pendingFocusRef.current = { path, region: null }
457
+ }
458
+
459
+ const openPath = () => {
460
+ const p = (pathDraft || '').trim()
461
+ if (!p) return
462
+ setStatus('打开中…')
463
+ rpc('edrv.read', { sessionId, path: p }).then((res) => {
464
+ if (res && res.ok) {
465
+ openFile(p, false)
466
+ setOpenInput(false); setPathDraft(''); setStatus('已打开'); setError(null)
467
+ } else { setStatus('打开失败'); setError(res?.error ? String(res.error) : '打开失败') }
468
+ }).catch((e) => { setStatus('打开失败'); setError('打开异常:' + String(e)) })
469
+ }
470
+
471
+ const tabsEl = React.createElement('div', { className: 'edrv-tabs', style: { flex: '1 1 auto', minWidth: 0 } },
472
+ tabs.map((t) => React.createElement('div', {
473
+ key: t.path,
474
+ className: 'edrv-tab' + (t.path === active ? ' edrv-tab-active' : ''),
475
+ title: t.path,
476
+ onClick: () => { if (t.path !== active) { flushSave(); setActive(t.path) } },
477
+ },
478
+ React.createElement('span', { style: { overflow: 'hidden', textOverflow: 'ellipsis', maxWidth: 180 } }, t.path.split(/[\\/]/).pop() || t.path),
479
+ (dirtyMap[t.path] ? React.createElement('span', { className: 'edrv-tab-dot' }) : null),
480
+ React.createElement('span', { className: 'edrv-tab-x', onClick: (e) => { e.stopPropagation(); closeTab(t.path) } }, '×'))),
481
+ (openInput
482
+ ? React.createElement('input', { className: 'edrv-path-input', autoFocus: true, placeholder: '输入工作区相对/绝对路径,回车打开', value: pathDraft, onChange: (e) => setPathDraft(e.target.value), onKeyDown: (e) => { if (e.key === 'Enter') openPath(); if (e.key === 'Escape') setOpenInput(false) } })
483
+ : React.createElement('button', { className: 'edrv-tab-add', title: '打开文件(输入路径)', onClick: () => setOpenInput(true) }, '+')))
484
+
485
+ const pathBar = React.createElement('div', { className: 'edrv-pathbar', title: active || '' },
486
+ React.createElement('span', { className: 'edrv-pb-name' }, active ? String(active).split(/[\\/]/).pop() : '未打开文件'),
487
+ React.createElement('span', { className: 'edrv-pb-full' }, active || '使用右上搜索框 (Ctrl+P) 打开文件'))
488
+
489
+ const tabRow = React.createElement('div', { style: { display: 'flex', alignItems: 'center', background: 'var(--dsw-alias-bg-layer-1,transparent)', flexShrink: 0 } },
490
+ tabsEl,
491
+ React.createElement(QuickOpen, { sessionId, onOpen: (p) => openFile(p, false) }))
492
+
493
+ const statusBar = React.createElement('div', { className: 'edrv-statusbar' },
494
+ React.createElement('span', null, '编辑'),
495
+ React.createElement('span', null, active ? langOf(active) : ''),
496
+ React.createElement('span', null, cursor),
497
+ (status ? React.createElement('span', null, status) : null),
498
+ React.createElement('span', { style: { flex: 1 } }),
499
+ (sum.totalFiles > 0
500
+ ? React.createElement('button', { className: 'edrv-diffchip', title: '打开差异总览/归档', onClick: () => setLauncherOpen((o) => !o) }, '⚠ 差异 ' + sum.totalFiles + ' 文件')
501
+ : null),
502
+ React.createElement('button', { className: 'edrv-chip-btn', title: '刷新', onClick: reloadFile }, '⟳'))
503
+
504
+ const otherFiles = sum.pendingFiles.filter((f) => f.path !== active)
505
+
506
+ let body
507
+ if (!monaco && !monacoErr) {
508
+ body = React.createElement('div', { className: 'edrv-empty' }, '正在加载 Monaco 编辑器…')
509
+ } else if (monacoErr) {
510
+ body = React.createElement('div', { className: 'edrv-empty' },
511
+ React.createElement('div', null, 'Monaco 编辑器加载失败:' + String(monacoErr)),
512
+ React.createElement('div', { style: { fontSize: 11 } }, '请确认插件包 assets/vendor/monaco 完整(/edrv/vendor 路由可达)'))
513
+ } else if (!active) {
514
+ body = React.createElement('div', { className: 'edrv-empty' },
515
+ React.createElement('div', null, '暂无打开的文件'),
516
+ React.createElement('div', { style: { fontSize: 12 } }, '使用右上搜索框 (Ctrl+P) 打开工作区文件;agent 修改文件后顶部会出现差异角标'))
517
+ } else if (content === null) {
518
+ body = React.createElement('div', { className: 'edrv-empty' }, '加载中…')
519
+ } else {
520
+ body = React.createElement('div', { className: 'edrv-monaco-host' },
521
+ React.createElement('div', { ref: ensureEditor }))
522
+ }
523
+
524
+ const hoverEl = (hoverAct && !launcherOpen)
525
+ ? React.createElement('div', {
526
+ className: 'edrv-hoveract',
527
+ style: { top: hoverAct.top, right: 12 },
528
+ onMouseEnter: () => { if (hideTimerRef.current) { clearTimeout(hideTimerRef.current); hideTimerRef.current = null } },
529
+ onMouseLeave: () => { const t = setTimeout(() => { hideTimerRef.current = null; hoverKeyRef.current = null; hoverTopRef.current = null; setHoverAct(null) }, 180); hideTimerRef.current = t },
530
+ },
531
+ React.createElement('button', { className: 'edrv-pill edrv-pill-keep', onClick: () => actHunk(hoverAct.region, false) }, '✓ Keep'),
532
+ React.createElement('button', { className: 'edrv-pill edrv-pill-undo', onClick: () => actHunk(hoverAct.region, true) }, '↩ Undo'))
533
+ : null
534
+
535
+ const overlay = launcherOpen
536
+ ? React.createElement(React.Fragment, null,
537
+ React.createElement('div', { style: { position: 'fixed', inset: 0, zIndex: 30 }, onClick: () => setLauncherOpen(false) }),
538
+ React.createElement(DiffLauncher, { sessionId, sum, tab: launcherTab, onClose: () => setLauncherOpen(false), onOpenFile: (p) => { openFile(p, true); setLauncherOpen(false) } }))
539
+ : (pendingRegions.length > 0
540
+ ? React.createElement(DiffBox, {
541
+ pendingRegions, staleRegions,
542
+ onAct: actHunk,
543
+ onAcceptFile: acceptFile,
544
+ onUndoFile: undoFile,
545
+ onAcceptAllFiles: acceptAllFiles,
546
+ onUndoAllFiles: undoAllFiles,
547
+ allPendingCount: allPending.length,
548
+ onRollback: rollbackFile,
549
+ onJump: jumpTo,
550
+ otherFiles,
551
+ onOpenOther: (p) => openFile(p, true),
552
+ onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
553
+ onRefresh: reloadFile,
554
+ activePath: active,
555
+ diffIdx, diffTotal: pendingRegions.length,
556
+ fileIdx, fileTotal: sum.pendingFiles.length,
557
+ onPrevDiff: () => gotoDiff(-1), onNextDiff: () => gotoDiff(1),
558
+ onPrevFile: () => gotoFile(-1), onNextFile: () => gotoFile(1),
559
+ })
560
+ : (sum.pendingFiles.length > 0
561
+ ? React.createElement(DiffBarEmpty, {
562
+ sum, active, staleCount: staleRegions.length,
563
+ onNextFile: () => { if (sum.pendingFiles.length) openFile(sum.pendingFiles[0].path, true) },
564
+ onOpenLauncher: (tab) => { setLauncherTab(tab || 'pending'); setLauncherOpen(true) },
565
+ onRefresh: reloadFile,
566
+ })
567
+ : null))
568
+
569
+ return React.createElement('div', { 'data-edrv-view': '1', style: { height: '100%', minHeight: 0, display: 'flex', flexDirection: 'column', background: 'var(--dsw-alias-bg-base,transparent)', overflow: 'hidden' } },
570
+ pathBar,
571
+ tabRow,
572
+ React.createElement('div', { style: { position: 'relative', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' } },
573
+ body,
574
+ hoverEl,
575
+ overlay),
576
+ statusBar)
577
+ }
@@ -0,0 +1,91 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * dsh-vscode-mode client — QuickOpen:顶部搜索框(Ctrl+P 打开文件)。
4
+ * 迁移自原 src/client/index.ts 的 QuickOpen,语义不改。
5
+ * 作者 ddj 2026-08-20
6
+ */
7
+ import React from 'react'
8
+ import { rpc } from '../rpc.js'
9
+
10
+ /**
11
+ * 快速打开搜索框:Ctrl+P 聚焦,输入 ≥2 字符经 edrv.searchFiles 搜索,
12
+ * 结果列表点击/回车打开文件。
13
+ * @param props.sessionId 会话 id
14
+ * @param props.onOpen 打开回调(path)
15
+ */
16
+ export function QuickOpen(props) {
17
+ const sessionId = props?.sessionId
18
+ const onOpen = props?.onOpen
19
+ const [q, setQ] = React.useState('')
20
+ const [results, setResults] = React.useState(null)
21
+ const [open, setOpen] = React.useState(false)
22
+ const inputRef = React.useRef(null)
23
+ const seq = React.useRef(0)
24
+ const timer = React.useRef(null)
25
+
26
+ const search = (query) => {
27
+ const s = ++seq.current
28
+ const clean = String(query || '').trim()
29
+ if (clean.length < 2) { setResults(null); return }
30
+ rpc('edrv.searchFiles', { sessionId, query: clean }).then((res) => {
31
+ if (s !== seq.current) return
32
+ setResults((res && res.ok && Array.isArray(res.files)) ? res.files : [])
33
+ }).catch(() => { if (s === seq.current) setResults([]) })
34
+ }
35
+
36
+ const onChange = (v) => {
37
+ setQ(v); setOpen(true)
38
+ if (timer.current) clearTimeout(timer.current)
39
+ timer.current = setTimeout(() => search(v), 180)
40
+ }
41
+
42
+ const pick = (p) => {
43
+ onOpen(p)
44
+ setQ(''); setResults(null); setOpen(false)
45
+ inputRef.current?.blur?.()
46
+ }
47
+
48
+ React.useEffect(() => {
49
+ const onKey = (e) => {
50
+ if ((e.ctrlKey || e.metaKey) && String(e.key).toLowerCase() === 'p') {
51
+ e.preventDefault(); e.stopPropagation()
52
+ inputRef.current?.focus?.()
53
+ setOpen(true)
54
+ } else if (e.key === 'Escape') {
55
+ setOpen(false); inputRef.current?.blur?.()
56
+ }
57
+ }
58
+ window.addEventListener('keydown', onKey, true)
59
+ return () => window.removeEventListener('keydown', onKey, true)
60
+ }, [])
61
+
62
+ const dirText = (p) => {
63
+ const base = String(p).split(/[\\/]/).pop() || String(p)
64
+ return String(p).slice(0, Math.max(0, String(p).length - base.length)).replace(/[\\/]+$/, '') || String(p)
65
+ }
66
+
67
+ let pop = null
68
+ if (open && Array.isArray(results) && q.trim().length >= 2) {
69
+ if (results.length) {
70
+ const items = results.map((p) => React.createElement('div', { key: p, className: 'edrv-search-item', onClick: () => pick(p) },
71
+ React.createElement('span', { className: 'n' }, String(p).split(/[\\/]/).pop() || p),
72
+ React.createElement('span', { className: 'd' }, dirText(p))))
73
+ pop = React.createElement('div', { className: 'edrv-search-pop' }, ...items)
74
+ } else {
75
+ pop = React.createElement('div', { className: 'edrv-search-pop' }, React.createElement('div', { className: 'edrv-search-empty' }, '无匹配文件'))
76
+ }
77
+ }
78
+
79
+ return React.createElement('div', { className: 'edrv-search-wrap' },
80
+ React.createElement('input', {
81
+ ref: inputRef, className: 'edrv-search', placeholder: '搜索文件 (Ctrl+P)',
82
+ value: q,
83
+ onChange: (e) => onChange(e.target.value),
84
+ onFocus: () => { if (q.trim().length >= 2 && !results) setOpen(true) },
85
+ onKeyDown: (e) => {
86
+ if (e.key === 'Enter' && Array.isArray(results) && results.length) pick(results[0])
87
+ if (e.key === 'Escape') setOpen(false)
88
+ },
89
+ }),
90
+ pop)
91
+ }