berry-agent 0.1.0-alpha.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 (401) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +96 -0
  3. package/dist/.api-emit.stamp +1 -0
  4. package/dist/.build-meta.json +6 -0
  5. package/dist/agent/events.js +1 -0
  6. package/dist/agent/index.js +2 -0
  7. package/dist/agent/loop.js +139 -0
  8. package/dist/agent/queue.js +119 -0
  9. package/dist/agent/stream.js +62 -0
  10. package/dist/agent/tools-batch.js +221 -0
  11. package/dist/agent/types.js +8 -0
  12. package/dist/api/berry-agent-llm.d.ts +18 -0
  13. package/dist/api/berry-agent.d.ts +9 -0
  14. package/dist/api/surface.json +1632 -0
  15. package/dist/api/tsconfig.paths.json +12 -0
  16. package/dist/api/typebox-compile.d.ts +2 -0
  17. package/dist/api/typebox-value.d.ts +2 -0
  18. package/dist/api/typebox.d.ts +2 -0
  19. package/dist/browser/cdp.js +256 -0
  20. package/dist/browser/codes.js +31 -0
  21. package/dist/browser/discover.js +107 -0
  22. package/dist/browser/engine.js +169 -0
  23. package/dist/browser/index.js +17 -0
  24. package/dist/browser/install.js +342 -0
  25. package/dist/browser/page.js +321 -0
  26. package/dist/browser/service.js +158 -0
  27. package/dist/browser/tools.js +198 -0
  28. package/dist/browser/types.js +81 -0
  29. package/dist/channels/ask-queue.js +82 -0
  30. package/dist/channels/commands.js +129 -0
  31. package/dist/channels/engine/cell.js +143 -0
  32. package/dist/channels/engine/diff.js +143 -0
  33. package/dist/channels/engine/engine.js +416 -0
  34. package/dist/channels/engine/index.js +8 -0
  35. package/dist/channels/engine/input-keys.js +86 -0
  36. package/dist/channels/engine/input.js +596 -0
  37. package/dist/channels/engine/memory-io.js +71 -0
  38. package/dist/channels/engine/process-io.js +38 -0
  39. package/dist/channels/engine/types.js +17 -0
  40. package/dist/channels/engine/width.js +122 -0
  41. package/dist/channels/index.js +15 -0
  42. package/dist/channels/registry.js +56 -0
  43. package/dist/channels/sdk/admit.js +29 -0
  44. package/dist/channels/sdk/backend.js +102 -0
  45. package/dist/channels/sdk/cursor.js +56 -0
  46. package/dist/channels/sdk/index.js +20 -0
  47. package/dist/channels/sdk/jsonl.js +208 -0
  48. package/dist/channels/sdk/protocol.js +24 -0
  49. package/dist/channels/sdk/schema.js +91 -0
  50. package/dist/channels/sdk/wire-core.js +446 -0
  51. package/dist/channels/service.js +169 -0
  52. package/dist/channels/tui/autocomplete/autocomplete.js +42 -0
  53. package/dist/channels/tui/autocomplete/file-mentions.js +106 -0
  54. package/dist/channels/tui/autocomplete/popup.js +148 -0
  55. package/dist/channels/tui/autocomplete/provider.js +7 -0
  56. package/dist/channels/tui/autocomplete/token.js +68 -0
  57. package/dist/channels/tui/backend/ansi-rows.js +211 -0
  58. package/dist/channels/tui/backend/main-screen.js +207 -0
  59. package/dist/channels/tui/backend/osc.js +91 -0
  60. package/dist/channels/tui/backend/transcript.js +230 -0
  61. package/dist/channels/tui/backend/tui-backend.js +915 -0
  62. package/dist/channels/tui/editor/editor-model.js +543 -0
  63. package/dist/channels/tui/editor/editor-view.js +140 -0
  64. package/dist/channels/tui/editor/editor.js +275 -0
  65. package/dist/channels/tui/editor/undo-stack.js +35 -0
  66. package/dist/channels/tui/editor/visual-lines.js +126 -0
  67. package/dist/channels/tui/editor/word-nav.js +106 -0
  68. package/dist/channels/tui/history/history-viewer.js +403 -0
  69. package/dist/channels/tui/index.js +3 -0
  70. package/dist/channels/tui/layout.js +167 -0
  71. package/dist/channels/tui/markdown/blocks.js +111 -0
  72. package/dist/channels/tui/markdown/inline.js +75 -0
  73. package/dist/channels/tui/markdown/markdown.js +182 -0
  74. package/dist/channels/tui/memory/memory-viewer.js +540 -0
  75. package/dist/channels/tui/overlay/alt-screen.js +105 -0
  76. package/dist/channels/tui/overlay/overlay.js +57 -0
  77. package/dist/channels/tui/overlay/select-confirm.js +156 -0
  78. package/dist/channels/tui/panels/todo-panel.js +54 -0
  79. package/dist/channels/tui/panels/tool-progress-panel.js +93 -0
  80. package/dist/channels/tui/scroll/scroll-view.js +254 -0
  81. package/dist/channels/tui/status/status-line.js +76 -0
  82. package/dist/channels/tui/text.js +38 -0
  83. package/dist/channels/tui/theme.js +41 -0
  84. package/dist/channels/types.js +11 -0
  85. package/dist/channels/ui-core.js +209 -0
  86. package/dist/checkpoint/capture.js +45 -0
  87. package/dist/checkpoint/codes.js +33 -0
  88. package/dist/checkpoint/command.js +82 -0
  89. package/dist/checkpoint/gate.js +48 -0
  90. package/dist/checkpoint/index.js +20 -0
  91. package/dist/checkpoint/restore.js +172 -0
  92. package/dist/checkpoint/store.js +247 -0
  93. package/dist/checkpoint/types.js +4 -0
  94. package/dist/checkpoint/walk.js +146 -0
  95. package/dist/compaction/ccr-tools.js +112 -0
  96. package/dist/compaction/ccr.js +84 -0
  97. package/dist/compaction/codes.js +21 -0
  98. package/dist/compaction/index.js +18 -0
  99. package/dist/compaction/policy.js +200 -0
  100. package/dist/compaction/service.js +422 -0
  101. package/dist/compaction/slots.js +121 -0
  102. package/dist/compaction/types.js +10 -0
  103. package/dist/context/codes.js +40 -0
  104. package/dist/context/events.js +160 -0
  105. package/dist/context/index.js +13 -0
  106. package/dist/context/logger.js +136 -0
  107. package/dist/context/scope.js +182 -0
  108. package/dist/context/workspace.js +104 -0
  109. package/dist/contracts/agent-events.d.ts +73 -0
  110. package/dist/contracts/agent-events.js +16 -0
  111. package/dist/contracts/api.d.ts +249 -0
  112. package/dist/contracts/api.js +299 -0
  113. package/dist/contracts/approval.d.ts +37 -0
  114. package/dist/contracts/approval.js +9 -0
  115. package/dist/contracts/env-ref.d.ts +41 -0
  116. package/dist/contracts/env-ref.js +32 -0
  117. package/dist/contracts/errors.d.ts +56 -0
  118. package/dist/contracts/errors.js +266 -0
  119. package/dist/contracts/events.d.ts +70 -0
  120. package/dist/contracts/events.js +329 -0
  121. package/dist/contracts/index.d.ts +32 -0
  122. package/dist/contracts/index.js +26 -0
  123. package/dist/contracts/llm.d.ts +275 -0
  124. package/dist/contracts/llm.js +12 -0
  125. package/dist/contracts/messages.d.ts +63 -0
  126. package/dist/contracts/messages.js +65 -0
  127. package/dist/contracts/redact.d.ts +46 -0
  128. package/dist/contracts/redact.js +223 -0
  129. package/dist/contracts/tools.d.ts +197 -0
  130. package/dist/contracts/tools.js +33 -0
  131. package/dist/contracts/types.d.ts +302 -0
  132. package/dist/contracts/types.js +50 -0
  133. package/dist/contracts/ui.d.ts +122 -0
  134. package/dist/contracts/ui.js +15 -0
  135. package/dist/conversation/agent-service.js +61 -0
  136. package/dist/conversation/approval-wiring.js +82 -0
  137. package/dist/conversation/backoff.js +38 -0
  138. package/dist/conversation/codes.js +47 -0
  139. package/dist/conversation/control-tools.js +102 -0
  140. package/dist/conversation/control.js +210 -0
  141. package/dist/conversation/driver.js +922 -0
  142. package/dist/conversation/index.js +12 -0
  143. package/dist/conversation/model-visible.js +169 -0
  144. package/dist/conversation/open-tools.js +126 -0
  145. package/dist/conversation/reseed.js +132 -0
  146. package/dist/conversation/sessions.js +215 -0
  147. package/dist/conversation/todo.js +203 -0
  148. package/dist/conversation/types.js +35 -0
  149. package/dist/conversation/wiring.js +204 -0
  150. package/dist/credentials/codes.js +59 -0
  151. package/dist/credentials/commands.js +186 -0
  152. package/dist/credentials/env-ref.js +34 -0
  153. package/dist/credentials/index.js +28 -0
  154. package/dist/credentials/migration.js +18 -0
  155. package/dist/credentials/oauth.js +266 -0
  156. package/dist/credentials/refresh.js +152 -0
  157. package/dist/credentials/secrets.js +118 -0
  158. package/dist/credentials/types.js +30 -0
  159. package/dist/exec/bash.js +279 -0
  160. package/dist/exec/codes.js +37 -0
  161. package/dist/exec/env.js +99 -0
  162. package/dist/exec/environment.js +39 -0
  163. package/dist/exec/git-guard.js +623 -0
  164. package/dist/exec/index.js +24 -0
  165. package/dist/exec/registry.js +120 -0
  166. package/dist/exec/spawn.js +281 -0
  167. package/dist/exec/tail.js +77 -0
  168. package/dist/exec/types.js +5 -0
  169. package/dist/goal/codes.js +33 -0
  170. package/dist/goal/command.js +175 -0
  171. package/dist/goal/fold.js +121 -0
  172. package/dist/goal/gates.js +122 -0
  173. package/dist/goal/index.js +22 -0
  174. package/dist/goal/migration.js +48 -0
  175. package/dist/goal/service.js +501 -0
  176. package/dist/goal/todo-tool.js +174 -0
  177. package/dist/goal/types.js +1 -0
  178. package/dist/goal/update-tool.js +46 -0
  179. package/dist/host/approval-cmd.js +276 -0
  180. package/dist/host/assembly.js +1146 -0
  181. package/dist/host/boot-failures.js +104 -0
  182. package/dist/host/budget-advisory.js +74 -0
  183. package/dist/host/budget-broadcast.js +50 -0
  184. package/dist/host/builtins.js +54 -0
  185. package/dist/host/cli.js +544 -0
  186. package/dist/host/codes.js +120 -0
  187. package/dist/host/config-schema.js +195 -0
  188. package/dist/host/conversation-stack.js +654 -0
  189. package/dist/host/core-plugins.js +1685 -0
  190. package/dist/host/credentials-cmd.js +42 -0
  191. package/dist/host/disclosure.js +30 -0
  192. package/dist/host/dispatch.js +105 -0
  193. package/dist/host/doors-cmd.js +154 -0
  194. package/dist/host/dump-config.js +44 -0
  195. package/dist/host/hook-dispatch-guard.js +27 -0
  196. package/dist/host/import-gate.js +153 -0
  197. package/dist/host/index.js +54 -0
  198. package/dist/host/issue-session.js +241 -0
  199. package/dist/host/loader.js +372 -0
  200. package/dist/host/main.js +161 -0
  201. package/dist/host/manifest.js +381 -0
  202. package/dist/host/mcp-entry.js +53 -0
  203. package/dist/host/plugin-boot.js +952 -0
  204. package/dist/host/plugin-context.js +672 -0
  205. package/dist/host/plugin-install.js +480 -0
  206. package/dist/host/plugin-reload.js +124 -0
  207. package/dist/host/plugin-store.js +462 -0
  208. package/dist/host/plugin-tools.js +359 -0
  209. package/dist/host/plugin-uninstall.js +257 -0
  210. package/dist/host/plugins-cmd.js +302 -0
  211. package/dist/host/plugins-command.js +130 -0
  212. package/dist/host/plugins-config.js +195 -0
  213. package/dist/host/prompt-sections.js +133 -0
  214. package/dist/host/run-entry.js +544 -0
  215. package/dist/host/runtime.js +209 -0
  216. package/dist/host/scheduler-tick.js +294 -0
  217. package/dist/host/serve-daemon.js +501 -0
  218. package/dist/host/serve-entry.js +279 -0
  219. package/dist/host/session-anchor.js +41 -0
  220. package/dist/host/sessions-cmd.js +219 -0
  221. package/dist/host/sessions-face.js +47 -0
  222. package/dist/host/settings-store.js +112 -0
  223. package/dist/host/signals.js +69 -0
  224. package/dist/host/single-instance.js +105 -0
  225. package/dist/host/subagent-factory.js +291 -0
  226. package/dist/host/testkit/harness.js +283 -0
  227. package/dist/host/testkit/index.js +14 -0
  228. package/dist/host/testkit/matrix.js +302 -0
  229. package/dist/host/tool-policy-store.js +177 -0
  230. package/dist/host/triggers.js +212 -0
  231. package/dist/host/tui-entry.js +216 -0
  232. package/dist/host/webui-bridge.js +165 -0
  233. package/dist/issue/codes.js +38 -0
  234. package/dist/issue/filter.js +146 -0
  235. package/dist/issue/github.js +116 -0
  236. package/dist/issue/index.js +17 -0
  237. package/dist/issue/mount.js +70 -0
  238. package/dist/issue/poll.js +58 -0
  239. package/dist/issue/service.js +341 -0
  240. package/dist/issue/tools.js +70 -0
  241. package/dist/issue/types.js +34 -0
  242. package/dist/issue/webhook.js +126 -0
  243. package/dist/llm/codes.js +77 -0
  244. package/dist/llm/complete.js +196 -0
  245. package/dist/llm/events.js +15 -0
  246. package/dist/llm/index.js +24 -0
  247. package/dist/llm/inflight.js +65 -0
  248. package/dist/llm/model-id.js +58 -0
  249. package/dist/llm/provider-face.d.ts +32 -0
  250. package/dist/llm/provider-face.js +34 -0
  251. package/dist/llm/recovery.js +146 -0
  252. package/dist/llm/runtime.js +46 -0
  253. package/dist/llm/stream-fn.js +147 -0
  254. package/dist/lsp/codes.js +31 -0
  255. package/dist/lsp/connection.js +147 -0
  256. package/dist/lsp/frame.js +110 -0
  257. package/dist/lsp/index.js +16 -0
  258. package/dist/lsp/inject.js +58 -0
  259. package/dist/lsp/instance.js +264 -0
  260. package/dist/lsp/service.js +522 -0
  261. package/dist/lsp/tools.js +53 -0
  262. package/dist/lsp/types.js +201 -0
  263. package/dist/mcp/bridge.js +222 -0
  264. package/dist/mcp/codes.js +21 -0
  265. package/dist/mcp/index.js +19 -0
  266. package/dist/mcp/jsonrpc.js +230 -0
  267. package/dist/mcp/service.js +125 -0
  268. package/dist/mcp/tools.js +145 -0
  269. package/dist/mcp/types.js +108 -0
  270. package/dist/memory/cite.js +131 -0
  271. package/dist/memory/codes.js +50 -0
  272. package/dist/memory/command.js +71 -0
  273. package/dist/memory/consolidate.js +219 -0
  274. package/dist/memory/cycle.js +109 -0
  275. package/dist/memory/dao.js +878 -0
  276. package/dist/memory/diff.js +238 -0
  277. package/dist/memory/extract.js +181 -0
  278. package/dist/memory/fts.js +58 -0
  279. package/dist/memory/index.js +42 -0
  280. package/dist/memory/inject.js +354 -0
  281. package/dist/memory/merge.js +178 -0
  282. package/dist/memory/migration.js +132 -0
  283. package/dist/memory/pollution.js +59 -0
  284. package/dist/memory/port.js +385 -0
  285. package/dist/memory/review.js +201 -0
  286. package/dist/memory/scan.js +89 -0
  287. package/dist/memory/tools.js +508 -0
  288. package/dist/memory/types.js +140 -0
  289. package/dist/obs/codes.js +38 -0
  290. package/dist/obs/db.js +115 -0
  291. package/dist/obs/index.js +20 -0
  292. package/dist/obs/rollup.js +61 -0
  293. package/dist/obs/service.js +363 -0
  294. package/dist/obs/session-tools.js +282 -0
  295. package/dist/obs/session-view.js +225 -0
  296. package/dist/obs/tool.js +97 -0
  297. package/dist/obs/types.js +1 -0
  298. package/dist/persist/audit.js +63 -0
  299. package/dist/persist/aux.js +39 -0
  300. package/dist/persist/codes.js +35 -0
  301. package/dist/persist/index.js +30 -0
  302. package/dist/persist/load-history.js +50 -0
  303. package/dist/persist/migrations.js +37 -0
  304. package/dist/persist/paths.js +108 -0
  305. package/dist/persist/persistence.js +229 -0
  306. package/dist/persist/schema.js +99 -0
  307. package/dist/persist/secret-box.js +95 -0
  308. package/dist/persist/store.js +706 -0
  309. package/dist/persist/write-behind.js +238 -0
  310. package/dist/safety/approval.js +171 -0
  311. package/dist/safety/bwrap.js +123 -0
  312. package/dist/safety/codes.js +57 -0
  313. package/dist/safety/danger.js +505 -0
  314. package/dist/safety/gate.js +226 -0
  315. package/dist/safety/index.js +24 -0
  316. package/dist/safety/presets.js +73 -0
  317. package/dist/safety/roots.js +139 -0
  318. package/dist/safety/sandbox.js +214 -0
  319. package/dist/safety/seatbelt.js +88 -0
  320. package/dist/safety/sensitive.js +34 -0
  321. package/dist/safety/tool-policy.js +203 -0
  322. package/dist/safety/types.js +13 -0
  323. package/dist/scheduler/codes.js +54 -0
  324. package/dist/scheduler/cron-backend.js +135 -0
  325. package/dist/scheduler/engine.js +280 -0
  326. package/dist/scheduler/gates.js +60 -0
  327. package/dist/scheduler/index.js +25 -0
  328. package/dist/scheduler/migration.js +22 -0
  329. package/dist/scheduler/runner.js +171 -0
  330. package/dist/scheduler/schedule.js +193 -0
  331. package/dist/scheduler/service.js +283 -0
  332. package/dist/scheduler/tick.js +139 -0
  333. package/dist/scheduler/types.js +1 -0
  334. package/dist/sdk/http.js +865 -0
  335. package/dist/sdk/index.js +22 -0
  336. package/dist/sdk/mcp.js +289 -0
  337. package/dist/sdk/plugin-route-registry.js +126 -0
  338. package/dist/sdk/plugin-routes.js +149 -0
  339. package/dist/sdk/security.js +116 -0
  340. package/dist/sdk/types.js +32 -0
  341. package/dist/session/budget.js +159 -0
  342. package/dist/session/codes.js +40 -0
  343. package/dist/session/derive.js +180 -0
  344. package/dist/session/event-data.js +1 -0
  345. package/dist/session/fork.js +72 -0
  346. package/dist/session/import-gates.js +148 -0
  347. package/dist/session/index.js +18 -0
  348. package/dist/session/recover.js +103 -0
  349. package/dist/session/session.js +279 -0
  350. package/dist/session/snapshot.js +107 -0
  351. package/dist/skills/agents.js +242 -0
  352. package/dist/skills/codes.js +51 -0
  353. package/dist/skills/discovery.js +266 -0
  354. package/dist/skills/frontmatter.js +215 -0
  355. package/dist/skills/index.js +28 -0
  356. package/dist/skills/load.js +118 -0
  357. package/dist/skills/manage.js +201 -0
  358. package/dist/skills/registry.js +138 -0
  359. package/dist/skills/render.js +124 -0
  360. package/dist/skills/sections.js +171 -0
  361. package/dist/skills/types.js +21 -0
  362. package/dist/subagent/codes.js +50 -0
  363. package/dist/subagent/declarative.js +55 -0
  364. package/dist/subagent/index.js +19 -0
  365. package/dist/subagent/notify.js +51 -0
  366. package/dist/subagent/provide.js +7 -0
  367. package/dist/subagent/registry.js +149 -0
  368. package/dist/subagent/service.js +338 -0
  369. package/dist/subagent/surface.js +44 -0
  370. package/dist/subagent/tool.js +153 -0
  371. package/dist/subagent/types.js +25 -0
  372. package/dist/tools/apply-patch.js +164 -0
  373. package/dist/tools/codes.js +109 -0
  374. package/dist/tools/fs.js +501 -0
  375. package/dist/tools/index.js +17 -0
  376. package/dist/tools/observed.js +99 -0
  377. package/dist/tools/pipeline.js +176 -0
  378. package/dist/tools/protected-read.js +54 -0
  379. package/dist/tools/registry.js +250 -0
  380. package/dist/tools/search.js +451 -0
  381. package/dist/tools/worktree.js +327 -0
  382. package/dist/web/codes.js +37 -0
  383. package/dist/web/gate.js +43 -0
  384. package/dist/web/hygiene.js +195 -0
  385. package/dist/web/index.js +16 -0
  386. package/dist/web/service.js +207 -0
  387. package/dist/web/ssrf-guard.js +44 -0
  388. package/dist/web/tool.js +76 -0
  389. package/dist/web/types.js +10 -0
  390. package/dist/webui/assets/index-B-t2O8gh.js +22 -0
  391. package/dist/webui/assets/index-DOlgSZ9Q.css +2 -0
  392. package/dist/webui/index.html +14 -0
  393. package/dist/webui/index.js +14 -0
  394. package/dist/webui/server.js +549 -0
  395. package/dist/webui/types.js +38 -0
  396. package/examples/README.md +26 -0
  397. package/examples/minimal-code-plugin/entry.js +43 -0
  398. package/examples/minimal-code-plugin/package.json +12 -0
  399. package/examples/pure-skill-pack/package.json +14 -0
  400. package/examples/pure-skill-pack/skills/markdown-table/SKILL.md +32 -0
  401. package/package.json +86 -0
@@ -0,0 +1,543 @@
1
+ /**
2
+ * 多行输入编辑模型(Editor 件族核心——纯状态机,零渲染依赖)。
3
+ *
4
+ * 07 §4.1 引擎节件 6(组件与呈现装配件) 多行输入件条款的语义载体:字素光标算术(永不落半字)、
5
+ * 词级删除、undo(上限 100)、输入历史(100 帽 + draft 保底)、IME 预编辑
6
+ * 挂起态、垂直移动 sticky 列(显示列制——CJK 双宽对齐)。机制语义承 pi
7
+ * Editor(裁 ⓪ 从零重写:粘贴标记机制不采纳——本仓规范「多行粘贴整段入框
8
+ * 呈现」;视觉列按显示宽非码元差)。
9
+ *
10
+ * 坐标系:cursorCol 为 UTF-16 下标(字符串切片高效),但一切移动 / 删除
11
+ * 原语经 visual-lines 件的字素边界函数——结构上保证光标恒在字素边界。
12
+ */
13
+ import { buildVisualLineMap, colAtDisplayColumn, findVisualLineAt, nextGraphemeBoundary, prefixDisplayWidth, prevGraphemeBoundary, } from './visual-lines.js';
14
+ import { findWordBackward, findWordForward } from './word-nav.js';
15
+ import { UndoStack } from './undo-stack.js';
16
+ /** 输入历史帽(07 呈现面件 2 语义:输入历史 100 条) */
17
+ export const HISTORY_LIMIT = 100;
18
+ /** undo 上限(码面定值——与历史帽同值,规范只钉「有上限」) */
19
+ export const UNDO_LIMIT = 100;
20
+ /** 文本规范化:CRLF / CR → LF(粘贴与程序设值共用路) */
21
+ function normalizeText(text) {
22
+ return text.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
23
+ }
24
+ /**
25
+ * 编辑模型:状态自持、变更方法驱动;onChange 通知(渲染侧组装请求重绘)。
26
+ */
27
+ export class EditorModel {
28
+ state = { lines: [''], cursorLine: 0, cursorCol: 0 };
29
+ undoStack = new UndoStack(UNDO_LIMIT);
30
+ /** undo 合并判据(fish 式):连续词字符插入合并一单元;空白段、词接空白后各独立成步 */
31
+ lastAction = null;
32
+ history = [];
33
+ historyIndex = -1; // -1 = 不在浏览态
34
+ historyDraft = null;
35
+ /** 垂直移动 sticky 列(显示列——非垂直移动清空) */
36
+ preferredVisualCol = null;
37
+ /** IME 预编辑挂起段(null = 无组字;渲染呈现、不并入正文) */
38
+ preedit = null;
39
+ /** 布局宽(视图渲染时回写——垂直导航的折行依据,与渲染同宽) */
40
+ layoutWidth = 80;
41
+ /** 变更通知(订阅面——由持有方装配) */
42
+ onChange = null;
43
+ /* ---------------- 查询面 ---------------- */
44
+ getText() {
45
+ return this.state.lines.join('\n');
46
+ }
47
+ getLines() {
48
+ return [...this.state.lines];
49
+ }
50
+ getCursor() {
51
+ return { line: this.state.cursorLine, col: this.state.cursorCol };
52
+ }
53
+ /** IME 预编辑挂起段(渲染面消费) */
54
+ get pendingPreedit() {
55
+ return this.preedit;
56
+ }
57
+ /** 视觉行映射(视图渲染与视口滚动共用——与模型内部导航同宽同源) */
58
+ visualLines() {
59
+ return buildVisualLineMap(this.state.lines, this.layoutWidth);
60
+ }
61
+ /** 光标所在视觉行下标 */
62
+ currentVisualLine(map) {
63
+ return findVisualLineAt(map, this.state.cursorLine, this.state.cursorCol);
64
+ }
65
+ /** 光标的视觉列(显示列——含预编辑宽:组字期光标在预编辑段尾) */
66
+ cursorDisplayColumn() {
67
+ const line = this.state.lines[this.state.cursorLine] ?? '';
68
+ let cols = prefixDisplayWidth(line, this.state.cursorCol);
69
+ if (this.preedit !== null)
70
+ cols += prefixDisplayWidth(this.preedit, this.preedit.length);
71
+ return cols;
72
+ }
73
+ /** 布局宽回写(视图渲染时——垂直导航用,须与渲染宽一致) */
74
+ setLayoutWidth(width) {
75
+ this.layoutWidth = Math.max(1, width);
76
+ }
77
+ /* ---------------- 设值与提交 ---------------- */
78
+ /** 整体设值(程序面:清历史浏览 + undo 快照 + 光标归尾) */
79
+ setText(text) {
80
+ this.exitHistoryBrowsing();
81
+ const normalized = normalizeText(text);
82
+ if (this.getText() !== normalized)
83
+ this.pushUndo();
84
+ this.state = { lines: normalized.split('\n'), cursorLine: 0, cursorCol: 0 };
85
+ const last = this.state.lines[this.state.lines.length - 1] ?? '';
86
+ this.state.cursorLine = this.state.lines.length - 1;
87
+ this.state.cursorCol = last.length;
88
+ this.lastAction = null;
89
+ this.notify();
90
+ }
91
+ /**
92
+ * 提交:取 trim 后全文、全清(状态 / undo / 历史浏览 / 预编辑)返回。
93
+ * 历史入册归调用方(组件层 onSubmit 消费方决定)。
94
+ */
95
+ submit() {
96
+ const result = this.getText().trim();
97
+ this.state = { lines: [''], cursorLine: 0, cursorCol: 0 };
98
+ this.undoStack.clear();
99
+ this.lastAction = null;
100
+ this.preedit = null;
101
+ this.preferredVisualCol = null;
102
+ this.exitHistoryBrowsing();
103
+ this.notify();
104
+ return result;
105
+ }
106
+ /* ---------------- 插入路(text 游程 / IME 提交 / 粘贴共用) ---------------- */
107
+ /**
108
+ * 光标处插入文本(可多行——拆行落位、光标落插入段尾)。
109
+ * undo 合并:连续非空白段合并一单元(打字游程)、空白段独立成步。
110
+ */
111
+ insertText(text) {
112
+ if (text === '')
113
+ return;
114
+ this.exitHistoryBrowsing();
115
+ const isWhitespace = text.trim().length === 0;
116
+ if (isWhitespace || this.lastAction !== 'type-word')
117
+ this.pushUndo();
118
+ this.lastAction = isWhitespace ? 'type-whitespace' : 'type-word';
119
+ this.insertTextRaw(normalizeText(text));
120
+ this.notify();
121
+ }
122
+ /** 原子插入(无 undo 编排——内部路) */
123
+ insertTextRaw(text) {
124
+ const inserted = text.split('\n');
125
+ const current = this.state.lines[this.state.cursorLine] ?? '';
126
+ const before = current.slice(0, this.state.cursorCol);
127
+ const after = current.slice(this.state.cursorCol);
128
+ if (inserted.length === 1) {
129
+ this.state.lines[this.state.cursorLine] = before + inserted[0] + after;
130
+ this.setCursorCol(this.state.cursorCol + inserted[0].length);
131
+ }
132
+ else {
133
+ this.state.lines = [
134
+ ...this.state.lines.slice(0, this.state.cursorLine),
135
+ before + inserted[0],
136
+ ...inserted.slice(1, -1),
137
+ inserted[inserted.length - 1] + after,
138
+ ...this.state.lines.slice(this.state.cursorLine + 1),
139
+ ];
140
+ this.state.cursorLine += inserted.length - 1;
141
+ this.setCursorCol(inserted[inserted.length - 1].length);
142
+ }
143
+ }
144
+ /**
145
+ * 整 token 代换(补全弹层唯一写路径):指定行 [start,end) 段整段替换、
146
+ * 光标落代换尾。独立 undo 步(不与前后输入合并)、退出历史浏览、清预编辑
147
+ * 与水平粘滞链。
148
+ */
149
+ replaceToken(line, start, end, replacement) {
150
+ this.exitHistoryBrowsing();
151
+ this.pushUndo();
152
+ this.preedit = null;
153
+ this.preferredVisualCol = null;
154
+ this.lastAction = null;
155
+ const text = this.state.lines[line] ?? '';
156
+ this.state.lines[line] = text.slice(0, start) + replacement + text.slice(end);
157
+ this.state.cursorLine = line;
158
+ this.state.cursorCol = start + replacement.length;
159
+ this.notify();
160
+ }
161
+ /* ---------------- 删除族(字素算术 + 行合并) ---------------- */
162
+ /** 退格:删光标前一字素;行首与前行合并(换行删除语义) */
163
+ backspace() {
164
+ this.lastAction = null;
165
+ const line = this.state.lines[this.state.cursorLine] ?? '';
166
+ if (this.state.cursorCol > 0) {
167
+ this.pushUndo();
168
+ const boundary = prevGraphemeBoundary(line, this.state.cursorCol);
169
+ this.state.lines[this.state.cursorLine] = line.slice(0, boundary) + line.slice(this.state.cursorCol);
170
+ this.setCursorCol(boundary);
171
+ }
172
+ else if (this.state.cursorLine > 0) {
173
+ this.pushUndo();
174
+ const prev = this.state.lines[this.state.cursorLine - 1] ?? '';
175
+ this.state.lines[this.state.cursorLine - 1] = prev + line;
176
+ this.state.lines.splice(this.state.cursorLine, 1);
177
+ this.state.cursorLine--;
178
+ this.setCursorCol(prev.length);
179
+ }
180
+ else {
181
+ return; // 空框行首——无操作(不空通知)
182
+ }
183
+ this.exitHistoryBrowsing();
184
+ this.notify();
185
+ }
186
+ /** 前删:删光标处一字素;行尾与下一行合并 */
187
+ deleteForward() {
188
+ this.lastAction = null;
189
+ this.exitHistoryBrowsing();
190
+ const line = this.state.lines[this.state.cursorLine] ?? '';
191
+ if (this.state.cursorCol < line.length) {
192
+ this.pushUndo();
193
+ const boundary = nextGraphemeBoundary(line, this.state.cursorCol);
194
+ this.state.lines[this.state.cursorLine] = line.slice(0, this.state.cursorCol) + line.slice(boundary);
195
+ }
196
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
197
+ this.pushUndo();
198
+ const next = this.state.lines[this.state.cursorLine + 1] ?? '';
199
+ this.state.lines[this.state.cursorLine] = line + next;
200
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
201
+ }
202
+ else {
203
+ return; // 末行行尾——无操作
204
+ }
205
+ this.notify();
206
+ }
207
+ /** 删至行首:光标前段整删;行首与前行合并 */
208
+ deleteToLineStart() {
209
+ this.lastAction = null;
210
+ this.exitHistoryBrowsing();
211
+ const line = this.state.lines[this.state.cursorLine] ?? '';
212
+ if (this.state.cursorCol > 0) {
213
+ this.pushUndo();
214
+ this.state.lines[this.state.cursorLine] = line.slice(this.state.cursorCol);
215
+ this.setCursorCol(0);
216
+ }
217
+ else if (this.state.cursorLine > 0) {
218
+ this.pushUndo();
219
+ const prev = this.state.lines[this.state.cursorLine - 1] ?? '';
220
+ this.state.lines[this.state.cursorLine - 1] = prev + line;
221
+ this.state.lines.splice(this.state.cursorLine, 1);
222
+ this.state.cursorLine--;
223
+ this.setCursorCol(prev.length);
224
+ }
225
+ else {
226
+ return;
227
+ }
228
+ this.notify();
229
+ }
230
+ /** 删至行尾:光标后段整删;行尾与下一行合并 */
231
+ deleteToLineEnd() {
232
+ this.lastAction = null;
233
+ this.exitHistoryBrowsing();
234
+ const line = this.state.lines[this.state.cursorLine] ?? '';
235
+ if (this.state.cursorCol < line.length) {
236
+ this.pushUndo();
237
+ this.state.lines[this.state.cursorLine] = line.slice(0, this.state.cursorCol);
238
+ }
239
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
240
+ this.pushUndo();
241
+ const next = this.state.lines[this.state.cursorLine + 1] ?? '';
242
+ this.state.lines[this.state.cursorLine] = line + next;
243
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
244
+ }
245
+ else {
246
+ return;
247
+ }
248
+ this.notify();
249
+ }
250
+ /** 词级退删:删光标前一词(词边界 = word-nav 单源);行首与前行合并 */
251
+ deleteWordBackward() {
252
+ this.lastAction = null;
253
+ this.exitHistoryBrowsing();
254
+ const line = this.state.lines[this.state.cursorLine] ?? '';
255
+ if (this.state.cursorCol === 0) {
256
+ if (this.state.cursorLine > 0) {
257
+ this.pushUndo();
258
+ const prev = this.state.lines[this.state.cursorLine - 1] ?? '';
259
+ this.state.lines[this.state.cursorLine - 1] = prev + line;
260
+ this.state.lines.splice(this.state.cursorLine, 1);
261
+ this.state.cursorLine--;
262
+ this.setCursorCol(prev.length);
263
+ this.notify();
264
+ }
265
+ return;
266
+ }
267
+ this.pushUndo();
268
+ const boundary = findWordBackward(line, this.state.cursorCol);
269
+ this.state.lines[this.state.cursorLine] = line.slice(0, boundary) + line.slice(this.state.cursorCol);
270
+ this.setCursorCol(boundary);
271
+ this.notify();
272
+ }
273
+ /** 词级前删:删光标后一词;行尾与下一行合并 */
274
+ deleteWordForward() {
275
+ this.lastAction = null;
276
+ this.exitHistoryBrowsing();
277
+ const line = this.state.lines[this.state.cursorLine] ?? '';
278
+ if (this.state.cursorCol >= line.length) {
279
+ if (this.state.cursorLine < this.state.lines.length - 1) {
280
+ this.pushUndo();
281
+ const next = this.state.lines[this.state.cursorLine + 1] ?? '';
282
+ this.state.lines[this.state.cursorLine] = line + next;
283
+ this.state.lines.splice(this.state.cursorLine + 1, 1);
284
+ this.notify();
285
+ }
286
+ return;
287
+ }
288
+ this.pushUndo();
289
+ const boundary = findWordForward(line, this.state.cursorCol);
290
+ this.state.lines[this.state.cursorLine] = line.slice(0, this.state.cursorCol) + line.slice(boundary);
291
+ this.notify();
292
+ }
293
+ /* ---------------- 换行 ---------------- */
294
+ /** 插入换行:当前行光标处劈开、光标落新行首 */
295
+ addNewLine() {
296
+ this.lastAction = null;
297
+ this.exitHistoryBrowsing();
298
+ this.pushUndo();
299
+ const line = this.state.lines[this.state.cursorLine] ?? '';
300
+ this.state.lines[this.state.cursorLine] = line.slice(0, this.state.cursorCol);
301
+ this.state.lines.splice(this.state.cursorLine + 1, 0, line.slice(this.state.cursorCol));
302
+ this.state.cursorLine++;
303
+ this.setCursorCol(0);
304
+ this.notify();
305
+ }
306
+ /* ---------------- 移动族(字素边界 + 视觉行 sticky 列) ---------------- */
307
+ /** 左移一字素(行首 wrap 到前行行尾) */
308
+ moveLeft() {
309
+ this.lastAction = null;
310
+ const line = this.state.lines[this.state.cursorLine] ?? '';
311
+ if (this.state.cursorCol > 0) {
312
+ this.setCursorCol(prevGraphemeBoundary(line, this.state.cursorCol));
313
+ }
314
+ else if (this.state.cursorLine > 0) {
315
+ this.state.cursorLine--;
316
+ this.setCursorCol((this.state.lines[this.state.cursorLine] ?? '').length);
317
+ }
318
+ }
319
+ /** 右移一字素(行尾 wrap 到下一行行首) */
320
+ moveRight() {
321
+ this.lastAction = null;
322
+ const line = this.state.lines[this.state.cursorLine] ?? '';
323
+ if (this.state.cursorCol < line.length) {
324
+ this.setCursorCol(nextGraphemeBoundary(line, this.state.cursorCol));
325
+ }
326
+ else if (this.state.cursorLine < this.state.lines.length - 1) {
327
+ this.state.cursorLine++;
328
+ this.setCursorCol(0);
329
+ }
330
+ }
331
+ /** 上移一视觉行(sticky 列——显示列制双宽对齐) */
332
+ moveUp() {
333
+ this.lastAction = null;
334
+ const map = this.visualLines();
335
+ this.moveToVisualLine(map, this.currentVisualLine(map) - 1);
336
+ }
337
+ /** 下移一视觉行 */
338
+ moveDown() {
339
+ this.lastAction = null;
340
+ const map = this.visualLines();
341
+ this.moveToVisualLine(map, this.currentVisualLine(map) + 1);
342
+ }
343
+ /** 上 / 下翻页(页大小 = 视觉行数;光标随页夹取) */
344
+ pageUp(pageSize) {
345
+ this.lastAction = null;
346
+ const map = this.visualLines();
347
+ this.moveToVisualLine(map, this.currentVisualLine(map) - Math.max(1, pageSize));
348
+ }
349
+ pageDown(pageSize) {
350
+ this.lastAction = null;
351
+ const map = this.visualLines();
352
+ this.moveToVisualLine(map, this.currentVisualLine(map) + Math.max(1, pageSize));
353
+ }
354
+ /** 词向后移(行首 wrap 到前行行尾) */
355
+ moveWordBackward() {
356
+ this.lastAction = null;
357
+ const line = this.state.lines[this.state.cursorLine] ?? '';
358
+ if (this.state.cursorCol === 0) {
359
+ if (this.state.cursorLine > 0) {
360
+ this.state.cursorLine--;
361
+ this.setCursorCol((this.state.lines[this.state.cursorLine] ?? '').length);
362
+ }
363
+ return;
364
+ }
365
+ this.setCursorCol(findWordBackward(line, this.state.cursorCol));
366
+ }
367
+ /** 词向前移(行尾 wrap 到下一行行首) */
368
+ moveWordForward() {
369
+ this.lastAction = null;
370
+ const line = this.state.lines[this.state.cursorLine] ?? '';
371
+ if (this.state.cursorCol >= line.length) {
372
+ if (this.state.cursorLine < this.state.lines.length - 1) {
373
+ this.state.cursorLine++;
374
+ this.setCursorCol(0);
375
+ }
376
+ return;
377
+ }
378
+ this.setCursorCol(findWordForward(line, this.state.cursorCol));
379
+ }
380
+ /** 行首 */
381
+ moveHome() {
382
+ this.lastAction = null;
383
+ this.setCursorCol(0);
384
+ }
385
+ /** 行尾 */
386
+ moveEnd() {
387
+ this.lastAction = null;
388
+ this.setCursorCol((this.state.lines[this.state.cursorLine] ?? '').length);
389
+ }
390
+ /**
391
+ * jump 词向:跳到指定字符的下 / 上一处出现(多行搜索、跳过光标当前位)。
392
+ * 无匹配光标不动。
393
+ */
394
+ jumpToChar(ch, direction) {
395
+ this.lastAction = null;
396
+ const isForward = direction === 'forward';
397
+ const step = isForward ? 1 : -1;
398
+ const end = isForward ? this.state.lines.length : -1;
399
+ for (let li = this.state.cursorLine; li !== end; li += step) {
400
+ const line = this.state.lines[li] ?? '';
401
+ const isCurrent = li === this.state.cursorLine;
402
+ const from = isCurrent ? (isForward ? this.state.cursorCol + 1 : this.state.cursorCol - 1) : undefined;
403
+ const idx = isForward ? line.indexOf(ch, from) : line.lastIndexOf(ch, from);
404
+ if (idx !== -1) {
405
+ this.state.cursorLine = li;
406
+ this.setCursorCol(idx);
407
+ return;
408
+ }
409
+ }
410
+ }
411
+ /* ---------------- undo ---------------- */
412
+ undo() {
413
+ this.exitHistoryBrowsing();
414
+ const snapshot = this.undoStack.pop();
415
+ if (!snapshot)
416
+ return;
417
+ this.state = snapshot;
418
+ this.lastAction = null;
419
+ this.preedit = null;
420
+ this.preferredVisualCol = null;
421
+ this.notify();
422
+ }
423
+ canUndo() {
424
+ return this.undoStack.length > 0;
425
+ }
426
+ /* ---------------- IME 预编辑 ---------------- */
427
+ /** 挂起 / 清除预编辑段(committed = false 增量路——渲染呈现不并入正文) */
428
+ setPreedit(text) {
429
+ this.preedit = text !== null && text.length > 0 ? text : null;
430
+ }
431
+ /* ---------------- 输入历史 ---------------- */
432
+ /** 入册(提交后调用):trim 空不加、连续重复不加、100 帽 */
433
+ addToHistory(text) {
434
+ const trimmed = text.trim();
435
+ if (!trimmed)
436
+ return;
437
+ if (this.history[0] === trimmed)
438
+ return;
439
+ this.history.unshift(trimmed);
440
+ if (this.history.length > HISTORY_LIMIT)
441
+ this.history.pop();
442
+ }
443
+ /** 翻阅(-1 向旧 / +1 向新):draft 保底、尽头 no-op */
444
+ navigateHistory(direction) {
445
+ this.lastAction = null;
446
+ if (this.history.length === 0)
447
+ return;
448
+ const newIndex = this.historyIndex - direction;
449
+ if (newIndex < -1 || newIndex >= this.history.length)
450
+ return;
451
+ // 首入浏览态:快照草稿(回新态时恢复未提交编辑)
452
+ if (this.historyIndex === -1 && newIndex >= 0) {
453
+ this.pushUndo();
454
+ this.historyDraft = structuredClone(this.state);
455
+ }
456
+ this.historyIndex = newIndex;
457
+ if (this.historyIndex === -1) {
458
+ const draft = this.historyDraft;
459
+ this.historyDraft = null;
460
+ if (draft) {
461
+ this.state = draft;
462
+ this.preferredVisualCol = null;
463
+ this.notify();
464
+ }
465
+ }
466
+ else {
467
+ // 向旧翻光标归首、向新翻归尾(浏览方向语义)
468
+ const text = this.history[this.historyIndex] ?? '';
469
+ const lines = text.split('\n');
470
+ this.state.lines = lines.length === 0 ? [''] : lines;
471
+ const goingOld = direction === -1;
472
+ this.state.cursorLine = goingOld ? 0 : this.state.lines.length - 1;
473
+ const target = this.state.lines[this.state.cursorLine] ?? '';
474
+ this.setCursorCol(goingOld ? 0 : target.length);
475
+ this.notify();
476
+ }
477
+ }
478
+ /** 退出浏览态(任何编辑动作先行——draft 弃) */
479
+ exitHistoryBrowsing() {
480
+ this.historyIndex = -1;
481
+ this.historyDraft = null;
482
+ }
483
+ /** 是否在历史浏览态(↑ 触发判据之一) */
484
+ isBrowsingHistory() {
485
+ return this.historyIndex > -1;
486
+ }
487
+ /** 空框判(↑ 触发判据之一) */
488
+ isEmpty() {
489
+ return this.state.lines.length === 1 && this.state.lines[0] === '';
490
+ }
491
+ /** 光标在首视觉行判(↑ 于首行顶端才触发历史——呈现面件 2 语义) */
492
+ isOnFirstVisualLine() {
493
+ return this.currentVisualLine(this.visualLines()) === 0;
494
+ }
495
+ /** 光标在末视觉行判(↓ 触发历史回新的判据) */
496
+ isOnLastVisualLine() {
497
+ const map = this.visualLines();
498
+ return this.currentVisualLine(map) === map.length - 1;
499
+ }
500
+ /* ---------------- 内部 ---------------- */
501
+ /** 非垂直移动统一清 sticky 列(所有光标落位走此路) */
502
+ setCursorCol(col) {
503
+ this.state.cursorCol = col;
504
+ this.preferredVisualCol = null;
505
+ }
506
+ /** 落位目标视觉行(sticky 列决策——显示列制;越界 no-op) */
507
+ moveToVisualLine(map, target) {
508
+ if (target < 0 || target >= map.length)
509
+ return;
510
+ const curIdx = this.currentVisualLine(map);
511
+ const curVL = map[curIdx];
512
+ const tgtVL = map[target];
513
+ const curLine = this.state.lines[curVL.line] ?? '';
514
+ // 源显示列(光标在本段内的显示列)
515
+ const curCols = prefixDisplayWidth(curLine, this.state.cursorCol) - prefixDisplayWidth(curLine, curVL.startCol);
516
+ // sticky 决策(pi 表简化形):
517
+ // - 链中(源行已夹到尾):目标放得下回 sticky 位(清链)、放不下保持夹尾(留链);
518
+ // - 无链或编辑中途起链:源显示列放得下平移(不启链)、放不下落目标尾并起链
519
+ const preferred = this.preferredVisualCol;
520
+ let wantCols;
521
+ if (preferred !== null && curCols >= curVL.width) {
522
+ wantCols = Math.min(preferred, tgtVL.width);
523
+ this.preferredVisualCol = preferred > tgtVL.width ? preferred : null;
524
+ }
525
+ else {
526
+ wantCols = Math.min(curCols, tgtVL.width);
527
+ this.preferredVisualCol = curCols > tgtVL.width ? curCols : null;
528
+ }
529
+ const tgtLine = this.state.lines[tgtVL.line] ?? '';
530
+ // 显示列反查(相对段起点计——与 curCols 同坐标系;夹段宽,半字防线由反查内建)
531
+ const col = colAtDisplayColumn(tgtLine, tgtVL.startCol, wantCols);
532
+ this.state.cursorLine = tgtVL.line;
533
+ // 垂直落位直写(不经 setCursorCol——sticky 生命周期归本路,落位不得自清)
534
+ this.state.cursorCol = col;
535
+ }
536
+ pushUndo() {
537
+ this.undoStack.push(this.state);
538
+ }
539
+ notify() {
540
+ if (this.onChange !== null)
541
+ this.onChange(this.getText());
542
+ }
543
+ }
@@ -0,0 +1,140 @@
1
+ import { prefixDisplayWidth } from './visual-lines.js';
2
+ import { ACCENT_INDEX } from '../theme.js';
3
+ import { ansiColor } from '../../engine/index.js';
4
+ /** 边框字符(制表符单宽——写格按字素推进) */
5
+ const BORDER_TOP_LEFT = '┌';
6
+ const BORDER_TOP_RIGHT = '┐';
7
+ const BORDER_BOTTOM_LEFT = '└';
8
+ const BORDER_BOTTOM_RIGHT = '┘';
9
+ const BORDER_H = '─';
10
+ const BORDER_V = '│';
11
+ /** 聚焦态边框样式(accent 定值——theme 单源) */
12
+ const FOCUSED_BORDER = Object.freeze({ fg: ansiColor(ACCENT_INDEX) });
13
+ /** 最大可视行数缺省(装配层按终端高 30% 注入覆盖——pi 同形 max(5, rows*0.3)) */
14
+ const DEFAULT_MAX_VISIBLE_LINES = 8;
15
+ /**
16
+ * 编辑器视图:模型只读消费 + 自持视口偏移;measure / render 双段协商。
17
+ */
18
+ export class EditorView {
19
+ model;
20
+ focused = false;
21
+ /** 视口首行(视觉行下标——render 时对光标夹取自愈) */
22
+ scrollOffset = 0;
23
+ maxVisibleLines;
24
+ constructor(model, options = {}) {
25
+ this.model = model;
26
+ this.maxVisibleLines = Math.max(1, options.maxVisibleLines ?? DEFAULT_MAX_VISIBLE_LINES);
27
+ }
28
+ /** 聚焦态切换(事件路由裁决后由组件调用) */
29
+ setFocused(focused) {
30
+ this.focused = focused;
31
+ }
32
+ /** 量高:边框 2 + 视觉行数夹 maxVisibleLines(量高即分配承诺——不超卖) */
33
+ measure(width) {
34
+ this.model.setLayoutWidth(innerWidth(width));
35
+ const count = this.model.visualLines().length;
36
+ return 2 + Math.min(count, this.maxVisibleLines);
37
+ }
38
+ /** 落位渲染:边框 → 视口行 → 预编辑段 → 光标声明 */
39
+ render(buffer, region) {
40
+ const innerW = innerWidth(region.width);
41
+ const innerH = region.height - 2;
42
+ this.model.setLayoutWidth(innerW);
43
+ if (innerH <= 0 || innerW <= 0)
44
+ return; // 边框都容不下——防御位
45
+ const map = this.model.visualLines();
46
+ const cursorVL = this.model.currentVisualLine(map);
47
+ // 视口夹取自愈:光标恒可视(滚出上方提顶 / 滚出下方沉底)
48
+ this.scrollOffset = clampScroll(this.scrollOffset, cursorVL, innerH, map.length);
49
+ this.drawBorder(buffer, region, map.length);
50
+ this.drawContent(buffer, region, map, innerH);
51
+ this.drawCursor(buffer, region, map, cursorVL);
52
+ }
53
+ /* ---------------- 边框(含滚动指示) ---------------- */
54
+ drawBorder(buffer, region, totalLines) {
55
+ const style = this.focused ? FOCUSED_BORDER : undefined;
56
+ const lastRow = region.row + region.height - 1;
57
+ const lastCol = region.col + region.width - 1;
58
+ // 四角
59
+ buffer.setCell(region.row, region.col, BORDER_TOP_LEFT, style);
60
+ buffer.setCell(region.row, lastCol, BORDER_TOP_RIGHT, style);
61
+ buffer.setCell(lastRow, region.col, BORDER_BOTTOM_LEFT, style);
62
+ buffer.setCell(lastRow, lastCol, BORDER_BOTTOM_RIGHT, style);
63
+ // 横边(顶 / 底)
64
+ for (let c = region.col + 1; c < lastCol; c++) {
65
+ buffer.setCell(region.row, c, BORDER_H, style);
66
+ buffer.setCell(lastRow, c, BORDER_H, style);
67
+ }
68
+ // 竖边(左 / 右)
69
+ for (let r = region.row + 1; r < lastRow; r++) {
70
+ buffer.setCell(r, region.col, BORDER_V, style);
71
+ buffer.setCell(r, lastCol, BORDER_V, style);
72
+ }
73
+ // 滚动指示(有溢出才显——写入边框行右端、覆盖既有横边格)
74
+ const above = this.scrollOffset;
75
+ const below = totalLines - this.scrollOffset - (region.height - 2);
76
+ if (above > 0)
77
+ this.writeIndicator(buffer, region.row, lastCol, ` ↑${above}`, style);
78
+ if (below > 0)
79
+ this.writeIndicator(buffer, lastRow, lastCol, ` ↓${below}`, style);
80
+ }
81
+ /** 右端对齐写入指示文本(覆盖边框横格;越界由缓冲吸收) */
82
+ writeIndicator(buffer, row, lastCol, text, style) {
83
+ buffer.writeText(row, lastCol - text.length, text, style);
84
+ }
85
+ /* ---------------- 正文(视口内视觉行) ---------------- */
86
+ drawContent(buffer, region, map, innerH) {
87
+ const lines = this.model.getLines();
88
+ const preedit = this.model.pendingPreedit;
89
+ const cursor = this.model.getCursor();
90
+ const end = Math.min(map.length, this.scrollOffset + innerH);
91
+ for (let vi = this.scrollOffset; vi < end; vi++) {
92
+ const seg = map[vi];
93
+ const row = region.row + 1 + (vi - this.scrollOffset);
94
+ const line = lines[seg.line] ?? '';
95
+ const plain = line.slice(seg.startCol, seg.startCol + seg.length);
96
+ // 光标在本段且组字中:前缀 + 预编辑(下划线)+ 后缀三段呈现
97
+ if (preedit !== null &&
98
+ seg.line === cursor.line &&
99
+ cursor.col >= seg.startCol &&
100
+ cursor.col <= seg.startCol + seg.length) {
101
+ const prefix = line.slice(seg.startCol, cursor.col);
102
+ const suffix = line.slice(cursor.col, seg.startCol + seg.length);
103
+ const next = buffer.writeText(row, region.col + 1, prefix);
104
+ const afterPreedit = buffer.writeText(row, next, preedit, PREEDIT_STYLE);
105
+ buffer.writeText(row, afterPreedit, suffix);
106
+ continue;
107
+ }
108
+ buffer.writeText(row, region.col + 1, plain);
109
+ }
110
+ }
111
+ /* ---------------- 光标声明(聚焦态独占) ---------------- */
112
+ drawCursor(buffer, region, map, cursorVL) {
113
+ if (!this.focused)
114
+ return; // 非聚焦不声明——不抢其他交互件的本帧声明
115
+ const seg = map[cursorVL];
116
+ const cursor = this.model.getCursor();
117
+ const line = this.model.getLines()[seg.line] ?? '';
118
+ // 光标段内显示列(前缀宽差——宽字素边界由模型算术保证,落在首列)
119
+ const displayCol = prefixDisplayWidth(line, cursor.col) - prefixDisplayWidth(line, seg.startCol);
120
+ const preedit = this.model.pendingPreedit;
121
+ const preeditCols = preedit !== null ? prefixDisplayWidth(preedit, preedit.length) : 0;
122
+ buffer.setCursor(region.row + 1 + (cursorVL - this.scrollOffset), region.col + 1 + displayCol + preeditCols);
123
+ }
124
+ }
125
+ /** 预编辑段样式(下划线——组字中的挂起提示) */
126
+ const PREEDIT_STYLE = Object.freeze({ underline: true });
127
+ /** 内容区宽(左右边框各 1) */
128
+ function innerWidth(width) {
129
+ return Math.max(0, width - 2);
130
+ }
131
+ /** 视口夹取:光标滚出上方提顶、滚出下方沉底;内容短于视口归零 */
132
+ function clampScroll(offset, cursorVL, innerH, total) {
133
+ let next = offset;
134
+ if (cursorVL < next)
135
+ next = cursorVL;
136
+ if (cursorVL >= next + innerH)
137
+ next = cursorVL - innerH + 1;
138
+ const maxOffset = Math.max(0, total - innerH);
139
+ return Math.min(Math.max(0, next), maxOffset);
140
+ }