@f5-sales-demo/xcsh 19.51.2

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 (899) hide show
  1. package/CHANGELOG.md +7073 -0
  2. package/README.md +14 -0
  3. package/examples/README.md +21 -0
  4. package/examples/custom-tools/README.md +109 -0
  5. package/examples/custom-tools/hello/index.ts +20 -0
  6. package/examples/custom-tools/todo/index.ts +206 -0
  7. package/examples/extensions/README.md +143 -0
  8. package/examples/extensions/api-demo.ts +89 -0
  9. package/examples/extensions/chalk-logger.ts +25 -0
  10. package/examples/extensions/hello.ts +32 -0
  11. package/examples/extensions/pirate.ts +43 -0
  12. package/examples/extensions/plan-mode.ts +551 -0
  13. package/examples/extensions/reload-runtime.ts +37 -0
  14. package/examples/extensions/todo.ts +296 -0
  15. package/examples/extensions/tools.ts +145 -0
  16. package/examples/extensions/with-deps/index.ts +36 -0
  17. package/examples/extensions/with-deps/package-lock.json +31 -0
  18. package/examples/extensions/with-deps/package.json +16 -0
  19. package/examples/hooks/README.md +56 -0
  20. package/examples/hooks/auto-commit-on-exit.ts +48 -0
  21. package/examples/hooks/confirm-destructive.ts +58 -0
  22. package/examples/hooks/custom-compaction.ts +115 -0
  23. package/examples/hooks/dirty-repo-guard.ts +51 -0
  24. package/examples/hooks/file-trigger.ts +40 -0
  25. package/examples/hooks/git-checkpoint.ts +52 -0
  26. package/examples/hooks/handoff.ts +149 -0
  27. package/examples/hooks/permission-gate.ts +33 -0
  28. package/examples/hooks/protected-paths.ts +29 -0
  29. package/examples/hooks/qna.ts +118 -0
  30. package/examples/hooks/status-line.ts +39 -0
  31. package/examples/sdk/01-minimal.ts +21 -0
  32. package/examples/sdk/02-custom-model.ts +49 -0
  33. package/examples/sdk/03-custom-prompt.ts +43 -0
  34. package/examples/sdk/04-skills.ts +43 -0
  35. package/examples/sdk/06-extensions.ts +80 -0
  36. package/examples/sdk/06-hooks.ts +61 -0
  37. package/examples/sdk/07-context-files.ts +35 -0
  38. package/examples/sdk/08-prompt-templates.ts +41 -0
  39. package/examples/sdk/08-slash-commands.ts +46 -0
  40. package/examples/sdk/09-api-keys-and-oauth.ts +54 -0
  41. package/examples/sdk/11-sessions.ts +47 -0
  42. package/examples/sdk/README.md +172 -0
  43. package/package.json +539 -0
  44. package/scripts/build-info/resolvers.ts +51 -0
  45. package/scripts/capture-ax-fixture.ts +84 -0
  46. package/scripts/capture-login-fixture.ts +30 -0
  47. package/scripts/check-workflow-field-coverage.ts +101 -0
  48. package/scripts/extension-uat-harness.ts +340 -0
  49. package/scripts/format-prompts.ts +68 -0
  50. package/scripts/generate-api-spec-index.ts +580 -0
  51. package/scripts/generate-branding-index.ts +81 -0
  52. package/scripts/generate-build-info.ts +106 -0
  53. package/scripts/generate-console-catalog.ts +88 -0
  54. package/scripts/generate-console-field-metadata.ts +74 -0
  55. package/scripts/generate-docs-index.ts +40 -0
  56. package/scripts/generate-template.ts +32 -0
  57. package/scripts/generate-terraform-index.ts +81 -0
  58. package/scripts/mermaid-gallery.ts +132 -0
  59. package/scripts/uat-matrix-modalities.ts +451 -0
  60. package/scripts/uat-matrix-report.ts +175 -0
  61. package/scripts/uat-matrix.ts +353 -0
  62. package/src/async/index.ts +2 -0
  63. package/src/async/job-manager.ts +416 -0
  64. package/src/async/support.ts +5 -0
  65. package/src/autoresearch/apply-contract-to-state.ts +1 -0
  66. package/src/autoresearch/command-resume.md +17 -0
  67. package/src/autoresearch/contract.ts +205 -0
  68. package/src/autoresearch/dashboard.ts +374 -0
  69. package/src/autoresearch/git.ts +191 -0
  70. package/src/autoresearch/helpers.ts +328 -0
  71. package/src/autoresearch/index.ts +402 -0
  72. package/src/autoresearch/prompt.md +236 -0
  73. package/src/autoresearch/resume-message.md +16 -0
  74. package/src/autoresearch/state.ts +303 -0
  75. package/src/autoresearch/tools/init-experiment.ts +400 -0
  76. package/src/autoresearch/tools/log-experiment.ts +793 -0
  77. package/src/autoresearch/tools/run-experiment.ts +679 -0
  78. package/src/autoresearch/types.ts +114 -0
  79. package/src/browser/acquire.ts +249 -0
  80. package/src/browser/actions.ts +126 -0
  81. package/src/browser/auth.ts +176 -0
  82. package/src/browser/ax.ts +134 -0
  83. package/src/browser/cdp-core.ts +45 -0
  84. package/src/browser/cdp-page-actions.ts +51 -0
  85. package/src/browser/chrome-locate.ts +66 -0
  86. package/src/browser/dom-context.ts +89 -0
  87. package/src/browser/dt-context.ts +100 -0
  88. package/src/browser/extension-bridge.ts +182 -0
  89. package/src/browser/extension-page-actions.ts +397 -0
  90. package/src/browser/extension-provider.ts +373 -0
  91. package/src/browser/index.ts +17 -0
  92. package/src/browser/input-commit.ts +74 -0
  93. package/src/browser/native-messaging.ts +20 -0
  94. package/src/browser/page-actions.ts +18 -0
  95. package/src/browser/provider.ts +155 -0
  96. package/src/browser/resolver.ts +126 -0
  97. package/src/browser/selector.ts +40 -0
  98. package/src/bun-imports.d.ts +22 -0
  99. package/src/capability/context-file.ts +43 -0
  100. package/src/capability/extension-module.ts +34 -0
  101. package/src/capability/extension.ts +47 -0
  102. package/src/capability/fs.ts +107 -0
  103. package/src/capability/hook.ts +40 -0
  104. package/src/capability/index.ts +438 -0
  105. package/src/capability/instruction.ts +37 -0
  106. package/src/capability/mcp.ts +74 -0
  107. package/src/capability/prompt.ts +35 -0
  108. package/src/capability/rule.ts +224 -0
  109. package/src/capability/settings.ts +34 -0
  110. package/src/capability/skill.ts +50 -0
  111. package/src/capability/slash-command.ts +40 -0
  112. package/src/capability/ssh.ts +41 -0
  113. package/src/capability/system-prompt.ts +34 -0
  114. package/src/capability/tool.ts +38 -0
  115. package/src/capability/types.ts +168 -0
  116. package/src/cli/agents-cli.ts +138 -0
  117. package/src/cli/args.ts +286 -0
  118. package/src/cli/chrome-cli.ts +88 -0
  119. package/src/cli/classify-install-target.ts +50 -0
  120. package/src/cli/commands/init-xdg.ts +27 -0
  121. package/src/cli/config-cli.ts +418 -0
  122. package/src/cli/file-processor.ts +121 -0
  123. package/src/cli/grep-cli.ts +160 -0
  124. package/src/cli/grievances-cli.ts +78 -0
  125. package/src/cli/initial-message.ts +58 -0
  126. package/src/cli/jupyter-cli.ts +106 -0
  127. package/src/cli/list-models.ts +128 -0
  128. package/src/cli/plugin-cli.ts +964 -0
  129. package/src/cli/read-cli.ts +68 -0
  130. package/src/cli/session-picker.ts +52 -0
  131. package/src/cli/setup-cli.ts +479 -0
  132. package/src/cli/shell-cli.ts +174 -0
  133. package/src/cli/ssh-cli.ts +179 -0
  134. package/src/cli/stats-cli.ts +182 -0
  135. package/src/cli/update-cli.ts +446 -0
  136. package/src/cli/web-search-cli.ts +203 -0
  137. package/src/cli.ts +123 -0
  138. package/src/commands/agents.ts +57 -0
  139. package/src/commands/chrome-host.ts +71 -0
  140. package/src/commands/chrome.ts +28 -0
  141. package/src/commands/commit.ts +36 -0
  142. package/src/commands/config.ts +51 -0
  143. package/src/commands/grep.ts +48 -0
  144. package/src/commands/grievances.ts +20 -0
  145. package/src/commands/jupyter.ts +32 -0
  146. package/src/commands/launch.ts +144 -0
  147. package/src/commands/plugin.ts +78 -0
  148. package/src/commands/read.ts +33 -0
  149. package/src/commands/setup.ts +42 -0
  150. package/src/commands/shell.ts +29 -0
  151. package/src/commands/ssh.ts +60 -0
  152. package/src/commands/stats.ts +29 -0
  153. package/src/commands/update.ts +21 -0
  154. package/src/commands/web-search.ts +46 -0
  155. package/src/commit/agentic/agent.ts +314 -0
  156. package/src/commit/agentic/fallback.ts +96 -0
  157. package/src/commit/agentic/index.ts +354 -0
  158. package/src/commit/agentic/prompts/analyze-file.md +22 -0
  159. package/src/commit/agentic/prompts/session-user.md +25 -0
  160. package/src/commit/agentic/prompts/split-confirm.md +1 -0
  161. package/src/commit/agentic/prompts/system.md +38 -0
  162. package/src/commit/agentic/state.ts +60 -0
  163. package/src/commit/agentic/tools/analyze-file.ts +127 -0
  164. package/src/commit/agentic/tools/git-file-diff.ts +191 -0
  165. package/src/commit/agentic/tools/git-hunk.ts +50 -0
  166. package/src/commit/agentic/tools/git-overview.ts +81 -0
  167. package/src/commit/agentic/tools/index.ts +54 -0
  168. package/src/commit/agentic/tools/propose-changelog.ts +139 -0
  169. package/src/commit/agentic/tools/propose-commit.ts +122 -0
  170. package/src/commit/agentic/tools/recent-commits.ts +81 -0
  171. package/src/commit/agentic/tools/schemas.ts +31 -0
  172. package/src/commit/agentic/tools/split-commit.ts +251 -0
  173. package/src/commit/agentic/topo-sort.ts +44 -0
  174. package/src/commit/agentic/trivial.ts +51 -0
  175. package/src/commit/agentic/validation.ts +183 -0
  176. package/src/commit/analysis/conventional.ts +122 -0
  177. package/src/commit/analysis/index.ts +4 -0
  178. package/src/commit/analysis/scope.ts +242 -0
  179. package/src/commit/analysis/summary.ts +105 -0
  180. package/src/commit/analysis/validation.ts +66 -0
  181. package/src/commit/changelog/detect.ts +40 -0
  182. package/src/commit/changelog/generate.ts +101 -0
  183. package/src/commit/changelog/index.ts +234 -0
  184. package/src/commit/changelog/parse.ts +44 -0
  185. package/src/commit/cli.ts +85 -0
  186. package/src/commit/git/diff.ts +148 -0
  187. package/src/commit/index.ts +5 -0
  188. package/src/commit/map-reduce/index.ts +69 -0
  189. package/src/commit/map-reduce/map-phase.ts +193 -0
  190. package/src/commit/map-reduce/reduce-phase.ts +108 -0
  191. package/src/commit/map-reduce/utils.ts +9 -0
  192. package/src/commit/message.ts +11 -0
  193. package/src/commit/model-selection.ts +66 -0
  194. package/src/commit/pipeline.ts +242 -0
  195. package/src/commit/prompts/analysis-system.md +148 -0
  196. package/src/commit/prompts/analysis-user.md +38 -0
  197. package/src/commit/prompts/changelog-system.md +50 -0
  198. package/src/commit/prompts/changelog-user.md +18 -0
  199. package/src/commit/prompts/file-observer-system.md +24 -0
  200. package/src/commit/prompts/file-observer-user.md +8 -0
  201. package/src/commit/prompts/reduce-system.md +50 -0
  202. package/src/commit/prompts/reduce-user.md +17 -0
  203. package/src/commit/prompts/summary-retry.md +3 -0
  204. package/src/commit/prompts/summary-system.md +38 -0
  205. package/src/commit/prompts/summary-user.md +13 -0
  206. package/src/commit/prompts/types-description.md +2 -0
  207. package/src/commit/types.ts +118 -0
  208. package/src/commit/utils/exclusions.ts +42 -0
  209. package/src/commit/utils.ts +44 -0
  210. package/src/config/auto-config.ts +698 -0
  211. package/src/config/context-schema.json +115 -0
  212. package/src/config/file-lock.ts +121 -0
  213. package/src/config/keybindings.ts +493 -0
  214. package/src/config/mcp-schema.json +230 -0
  215. package/src/config/model-equivalence.ts +675 -0
  216. package/src/config/model-registry.ts +2265 -0
  217. package/src/config/model-resolver.ts +1289 -0
  218. package/src/config/prompt-templates.ts +271 -0
  219. package/src/config/resolve-config-value.ts +110 -0
  220. package/src/config/settings-schema.ts +2155 -0
  221. package/src/config/settings.ts +730 -0
  222. package/src/config.ts +418 -0
  223. package/src/cursor.ts +323 -0
  224. package/src/dap/client.ts +674 -0
  225. package/src/dap/config.ts +150 -0
  226. package/src/dap/defaults.json +211 -0
  227. package/src/dap/index.ts +4 -0
  228. package/src/dap/session.ts +1255 -0
  229. package/src/dap/types.ts +600 -0
  230. package/src/debug/index.ts +440 -0
  231. package/src/debug/log-formatting.ts +58 -0
  232. package/src/debug/log-viewer.ts +908 -0
  233. package/src/debug/profiler.ts +158 -0
  234. package/src/debug/report-bundle.ts +365 -0
  235. package/src/debug/system-info.ts +107 -0
  236. package/src/discovery/agents-md.ts +67 -0
  237. package/src/discovery/agents.ts +219 -0
  238. package/src/discovery/builtin.ts +851 -0
  239. package/src/discovery/claude-plugins.ts +297 -0
  240. package/src/discovery/claude.ts +543 -0
  241. package/src/discovery/cline.ts +83 -0
  242. package/src/discovery/codex.ts +535 -0
  243. package/src/discovery/cursor.ts +220 -0
  244. package/src/discovery/gemini.ts +396 -0
  245. package/src/discovery/github.ts +118 -0
  246. package/src/discovery/helpers.ts +934 -0
  247. package/src/discovery/index.ts +79 -0
  248. package/src/discovery/language.ts +46 -0
  249. package/src/discovery/mcp-json.ts +171 -0
  250. package/src/discovery/opencode.ts +393 -0
  251. package/src/discovery/plugin-dir-roots.ts +28 -0
  252. package/src/discovery/ssh.ts +153 -0
  253. package/src/discovery/substitute-plugin-root.ts +29 -0
  254. package/src/discovery/vscode.ts +105 -0
  255. package/src/discovery/windsurf.ts +147 -0
  256. package/src/edit/diff.ts +818 -0
  257. package/src/edit/index.ts +481 -0
  258. package/src/edit/line-hash.ts +67 -0
  259. package/src/edit/modes/chunk.ts +762 -0
  260. package/src/edit/modes/hashline.ts +1366 -0
  261. package/src/edit/modes/patch.ts +1791 -0
  262. package/src/edit/modes/replace.ts +1115 -0
  263. package/src/edit/normalize.ts +375 -0
  264. package/src/edit/renderer.ts +627 -0
  265. package/src/exa/factory.ts +61 -0
  266. package/src/exa/index.ts +27 -0
  267. package/src/exa/mcp-client.ts +293 -0
  268. package/src/exa/render.ts +232 -0
  269. package/src/exa/researcher.ts +47 -0
  270. package/src/exa/search.ts +86 -0
  271. package/src/exa/types.ts +166 -0
  272. package/src/exa/websets.ts +247 -0
  273. package/src/exec/bash-executor.ts +332 -0
  274. package/src/exec/exec.ts +53 -0
  275. package/src/exec/idle-timeout-watchdog.ts +126 -0
  276. package/src/exec/non-interactive-env.ts +48 -0
  277. package/src/export/custom-share.ts +65 -0
  278. package/src/export/html/index.ts +161 -0
  279. package/src/export/html/template.css +1001 -0
  280. package/src/export/html/template.generated.ts +2 -0
  281. package/src/export/html/template.html +46 -0
  282. package/src/export/html/template.js +1950 -0
  283. package/src/export/html/template.macro.ts +24 -0
  284. package/src/export/html/vendor/highlight.min.js +1213 -0
  285. package/src/export/html/vendor/marked.min.js +6 -0
  286. package/src/export/ttsr.ts +434 -0
  287. package/src/extensibility/custom-commands/bundled/ci-green/index.ts +25 -0
  288. package/src/extensibility/custom-commands/bundled/review/index.ts +456 -0
  289. package/src/extensibility/custom-commands/index.ts +2 -0
  290. package/src/extensibility/custom-commands/loader.ts +236 -0
  291. package/src/extensibility/custom-commands/types.ts +111 -0
  292. package/src/extensibility/custom-tools/index.ts +7 -0
  293. package/src/extensibility/custom-tools/loader.ts +238 -0
  294. package/src/extensibility/custom-tools/types.ts +251 -0
  295. package/src/extensibility/custom-tools/wrapper.ts +47 -0
  296. package/src/extensibility/extensions/index.ts +15 -0
  297. package/src/extensibility/extensions/loader.ts +572 -0
  298. package/src/extensibility/extensions/runner.ts +848 -0
  299. package/src/extensibility/extensions/types.ts +1394 -0
  300. package/src/extensibility/extensions/wrapper.ts +191 -0
  301. package/src/extensibility/hooks/index.ts +5 -0
  302. package/src/extensibility/hooks/loader.ts +255 -0
  303. package/src/extensibility/hooks/runner.ts +425 -0
  304. package/src/extensibility/hooks/tool-wrapper.ts +108 -0
  305. package/src/extensibility/hooks/types.ts +826 -0
  306. package/src/extensibility/plugins/doctor.ts +66 -0
  307. package/src/extensibility/plugins/git-url.ts +281 -0
  308. package/src/extensibility/plugins/index.ts +9 -0
  309. package/src/extensibility/plugins/installer.ts +192 -0
  310. package/src/extensibility/plugins/loader.ts +267 -0
  311. package/src/extensibility/plugins/manager.ts +731 -0
  312. package/src/extensibility/plugins/marketplace/cache.ts +136 -0
  313. package/src/extensibility/plugins/marketplace/fetcher.ts +318 -0
  314. package/src/extensibility/plugins/marketplace/index.ts +6 -0
  315. package/src/extensibility/plugins/marketplace/manager.ts +781 -0
  316. package/src/extensibility/plugins/marketplace/prerequisites.ts +255 -0
  317. package/src/extensibility/plugins/marketplace/registry.ts +196 -0
  318. package/src/extensibility/plugins/marketplace/source-resolver.ts +147 -0
  319. package/src/extensibility/plugins/marketplace/types.ts +201 -0
  320. package/src/extensibility/plugins/parser.ts +105 -0
  321. package/src/extensibility/plugins/types.ts +194 -0
  322. package/src/extensibility/skills.ts +269 -0
  323. package/src/extensibility/slash-commands.ts +246 -0
  324. package/src/extensibility/tool-proxy.ts +25 -0
  325. package/src/extensibility/utils.ts +38 -0
  326. package/src/index.ts +61 -0
  327. package/src/internal-urls/agent-protocol.ts +136 -0
  328. package/src/internal-urls/api-catalog-resolve.ts +495 -0
  329. package/src/internal-urls/api-catalog-types.ts +94 -0
  330. package/src/internal-urls/api-spec-resolve.ts +1036 -0
  331. package/src/internal-urls/api-spec-types.ts +239 -0
  332. package/src/internal-urls/artifact-protocol.ts +97 -0
  333. package/src/internal-urls/branding-index.generated.ts +88 -0
  334. package/src/internal-urls/build-info-runtime.ts +302 -0
  335. package/src/internal-urls/build-info.generated.ts +33 -0
  336. package/src/internal-urls/computer-profile.ts +692 -0
  337. package/src/internal-urls/console-catalog-types.ts +21 -0
  338. package/src/internal-urls/console-catalog.generated.ts +532 -0
  339. package/src/internal-urls/console-field-metadata-types.ts +59 -0
  340. package/src/internal-urls/console-field-metadata.generated.ts +4841 -0
  341. package/src/internal-urls/console-resolve.ts +178 -0
  342. package/src/internal-urls/docs-index.generated.ts +698 -0
  343. package/src/internal-urls/extension-api.md +147 -0
  344. package/src/internal-urls/index.ts +46 -0
  345. package/src/internal-urls/jobs-protocol.ts +119 -0
  346. package/src/internal-urls/json-query.ts +126 -0
  347. package/src/internal-urls/local-protocol.ts +206 -0
  348. package/src/internal-urls/mcp-protocol.ts +156 -0
  349. package/src/internal-urls/memory-protocol.ts +133 -0
  350. package/src/internal-urls/parse.ts +72 -0
  351. package/src/internal-urls/profile-collectors.ts +106 -0
  352. package/src/internal-urls/router.ts +55 -0
  353. package/src/internal-urls/rule-protocol.ts +55 -0
  354. package/src/internal-urls/skill-protocol.ts +111 -0
  355. package/src/internal-urls/terraform-index.generated.ts +1852 -0
  356. package/src/internal-urls/terraform-resolve.ts +184 -0
  357. package/src/internal-urls/terraform-types.ts +45 -0
  358. package/src/internal-urls/types.ts +52 -0
  359. package/src/internal-urls/user-profile.ts +363 -0
  360. package/src/internal-urls/xcsh-protocol.ts +644 -0
  361. package/src/ipy/cancellation.ts +28 -0
  362. package/src/ipy/executor.ts +1155 -0
  363. package/src/ipy/gateway-coordinator.ts +424 -0
  364. package/src/ipy/kernel.ts +1091 -0
  365. package/src/ipy/modules.ts +144 -0
  366. package/src/ipy/prelude.py +849 -0
  367. package/src/ipy/prelude.ts +3 -0
  368. package/src/ipy/runtime.ts +221 -0
  369. package/src/locales/ar.json +512 -0
  370. package/src/locales/de.json +512 -0
  371. package/src/locales/en.json +549 -0
  372. package/src/locales/es.json +512 -0
  373. package/src/locales/fr.json +512 -0
  374. package/src/locales/hi.json +512 -0
  375. package/src/locales/index.ts +29 -0
  376. package/src/locales/it.json +512 -0
  377. package/src/locales/ja.json +512 -0
  378. package/src/locales/ko.json +512 -0
  379. package/src/locales/pt-br.json +512 -0
  380. package/src/locales/th.json +512 -0
  381. package/src/locales/zh-cn.json +512 -0
  382. package/src/locales/zh-tw.json +512 -0
  383. package/src/lsp/client.ts +941 -0
  384. package/src/lsp/clients/biome-client.ts +202 -0
  385. package/src/lsp/clients/index.ts +50 -0
  386. package/src/lsp/clients/lsp-linter-client.ts +93 -0
  387. package/src/lsp/clients/swiftlint-client.ts +120 -0
  388. package/src/lsp/config.ts +418 -0
  389. package/src/lsp/defaults.json +999 -0
  390. package/src/lsp/edits.ts +109 -0
  391. package/src/lsp/index.ts +1746 -0
  392. package/src/lsp/lspmux.ts +233 -0
  393. package/src/lsp/render.ts +692 -0
  394. package/src/lsp/startup-events.ts +13 -0
  395. package/src/lsp/types.ts +444 -0
  396. package/src/lsp/utils.ts +691 -0
  397. package/src/main.ts +944 -0
  398. package/src/mcp/client.ts +482 -0
  399. package/src/mcp/config-writer.ts +225 -0
  400. package/src/mcp/config.ts +365 -0
  401. package/src/mcp/discoverable-tool-metadata.ts +202 -0
  402. package/src/mcp/index.ts +29 -0
  403. package/src/mcp/json-rpc.ts +84 -0
  404. package/src/mcp/loader.ts +124 -0
  405. package/src/mcp/manager.ts +1152 -0
  406. package/src/mcp/oauth-discovery.ts +349 -0
  407. package/src/mcp/oauth-flow.ts +387 -0
  408. package/src/mcp/render.ts +157 -0
  409. package/src/mcp/smithery-auth.ts +104 -0
  410. package/src/mcp/smithery-connect.ts +145 -0
  411. package/src/mcp/smithery-registry.ts +477 -0
  412. package/src/mcp/tool-bridge.ts +417 -0
  413. package/src/mcp/tool-cache.ts +117 -0
  414. package/src/mcp/transports/http.ts +475 -0
  415. package/src/mcp/transports/index.ts +6 -0
  416. package/src/mcp/transports/stdio.ts +325 -0
  417. package/src/mcp/types.ts +423 -0
  418. package/src/memories/index.ts +1115 -0
  419. package/src/memories/storage.ts +577 -0
  420. package/src/modes/acp/acp-agent.ts +1361 -0
  421. package/src/modes/acp/acp-event-mapper.ts +538 -0
  422. package/src/modes/acp/acp-mode.ts +15 -0
  423. package/src/modes/acp/index.ts +2 -0
  424. package/src/modes/components/agent-dashboard.ts +1124 -0
  425. package/src/modes/components/assistant-message.ts +213 -0
  426. package/src/modes/components/bash-execution.ts +345 -0
  427. package/src/modes/components/bordered-loader.ts +41 -0
  428. package/src/modes/components/branch-summary-message.ts +45 -0
  429. package/src/modes/components/btw-panel.ts +103 -0
  430. package/src/modes/components/compaction-summary-message.ts +51 -0
  431. package/src/modes/components/context-add-wizard.ts +534 -0
  432. package/src/modes/components/countdown-timer.ts +75 -0
  433. package/src/modes/components/custom-editor.ts +238 -0
  434. package/src/modes/components/custom-message.ts +91 -0
  435. package/src/modes/components/diff.ts +210 -0
  436. package/src/modes/components/dynamic-border.ts +25 -0
  437. package/src/modes/components/extensions/extension-dashboard.ts +326 -0
  438. package/src/modes/components/extensions/extension-list.ts +492 -0
  439. package/src/modes/components/extensions/index.ts +9 -0
  440. package/src/modes/components/extensions/inspector-panel.ts +317 -0
  441. package/src/modes/components/extensions/state-manager.ts +587 -0
  442. package/src/modes/components/extensions/types.ts +191 -0
  443. package/src/modes/components/footer.ts +262 -0
  444. package/src/modes/components/gutter-block.ts +315 -0
  445. package/src/modes/components/history-search.ts +158 -0
  446. package/src/modes/components/hook-editor.ts +151 -0
  447. package/src/modes/components/hook-input.ts +81 -0
  448. package/src/modes/components/hook-message.ts +100 -0
  449. package/src/modes/components/hook-selector.ts +192 -0
  450. package/src/modes/components/index.ts +36 -0
  451. package/src/modes/components/keybinding-hints.ts +65 -0
  452. package/src/modes/components/login-dialog.ts +164 -0
  453. package/src/modes/components/mcp-add-wizard.ts +1314 -0
  454. package/src/modes/components/model-selector.ts +946 -0
  455. package/src/modes/components/oauth-selector.ts +211 -0
  456. package/src/modes/components/plugin-selector.ts +95 -0
  457. package/src/modes/components/plugin-settings.ts +477 -0
  458. package/src/modes/components/plugins/index.ts +5 -0
  459. package/src/modes/components/plugins/plugin-dashboard.ts +523 -0
  460. package/src/modes/components/plugins/plugin-inspector-pane.ts +84 -0
  461. package/src/modes/components/plugins/plugin-list-pane.ts +109 -0
  462. package/src/modes/components/plugins/state-manager.ts +229 -0
  463. package/src/modes/components/plugins/types.ts +51 -0
  464. package/src/modes/components/plugins/utils.ts +6 -0
  465. package/src/modes/components/python-execution.ts +244 -0
  466. package/src/modes/components/queue-mode-selector.ts +56 -0
  467. package/src/modes/components/read-tool-group.ts +137 -0
  468. package/src/modes/components/session-observer-overlay.ts +483 -0
  469. package/src/modes/components/session-selector.ts +343 -0
  470. package/src/modes/components/settings-defs.ts +568 -0
  471. package/src/modes/components/settings-selector.ts +641 -0
  472. package/src/modes/components/show-images-selector.ts +45 -0
  473. package/src/modes/components/skill-message.ts +90 -0
  474. package/src/modes/components/status-line/context-gradient.ts +64 -0
  475. package/src/modes/components/status-line/git-utils.ts +42 -0
  476. package/src/modes/components/status-line/hex-ansi.ts +29 -0
  477. package/src/modes/components/status-line/index.ts +4 -0
  478. package/src/modes/components/status-line/presets.ts +174 -0
  479. package/src/modes/components/status-line/segments.ts +494 -0
  480. package/src/modes/components/status-line/separators.ts +55 -0
  481. package/src/modes/components/status-line/token-rate.ts +66 -0
  482. package/src/modes/components/status-line/types.ts +96 -0
  483. package/src/modes/components/status-line-segment-editor.ts +361 -0
  484. package/src/modes/components/status-line.ts +729 -0
  485. package/src/modes/components/theme-selector.ts +63 -0
  486. package/src/modes/components/thinking-selector.ts +52 -0
  487. package/src/modes/components/todo-reminder.ts +54 -0
  488. package/src/modes/components/tool-execution.ts +781 -0
  489. package/src/modes/components/tree-selector.ts +908 -0
  490. package/src/modes/components/ttsr-notification.ts +80 -0
  491. package/src/modes/components/user-message-selector.ts +141 -0
  492. package/src/modes/components/user-message.ts +100 -0
  493. package/src/modes/components/visual-truncate.ts +63 -0
  494. package/src/modes/components/welcome-checks.ts +313 -0
  495. package/src/modes/components/welcome.ts +270 -0
  496. package/src/modes/controllers/btw-controller.ts +193 -0
  497. package/src/modes/controllers/chord-routing.ts +37 -0
  498. package/src/modes/controllers/command-controller.ts +1293 -0
  499. package/src/modes/controllers/context-command-controller.ts +61 -0
  500. package/src/modes/controllers/event-controller.ts +743 -0
  501. package/src/modes/controllers/extension-ui-controller.ts +983 -0
  502. package/src/modes/controllers/input-controller.ts +877 -0
  503. package/src/modes/controllers/mcp-command-controller.ts +1940 -0
  504. package/src/modes/controllers/selector-controller.ts +1292 -0
  505. package/src/modes/controllers/ssh-command-controller.ts +421 -0
  506. package/src/modes/index.ts +34 -0
  507. package/src/modes/interactive-mode.ts +1781 -0
  508. package/src/modes/oauth-manual-input.ts +42 -0
  509. package/src/modes/print-mode.ts +196 -0
  510. package/src/modes/prompt-action-autocomplete.ts +231 -0
  511. package/src/modes/rpc/host-tools.ts +186 -0
  512. package/src/modes/rpc/rpc-client.ts +762 -0
  513. package/src/modes/rpc/rpc-mode.ts +877 -0
  514. package/src/modes/rpc/rpc-types.ts +322 -0
  515. package/src/modes/session-observer-registry.ts +146 -0
  516. package/src/modes/shared.ts +30 -0
  517. package/src/modes/theme/defaults/index.ts +7 -0
  518. package/src/modes/theme/defaults/xcsh-dark.json +113 -0
  519. package/src/modes/theme/defaults/xcsh-light.json +115 -0
  520. package/src/modes/theme/mermaid-cache.ts +151 -0
  521. package/src/modes/theme/mermaid-palette.ts +39 -0
  522. package/src/modes/theme/theme-schema.json +553 -0
  523. package/src/modes/theme/theme.ts +2563 -0
  524. package/src/modes/types.ts +273 -0
  525. package/src/modes/utils/hotkeys-markdown.ts +60 -0
  526. package/src/modes/utils/keybinding-matchers.ts +30 -0
  527. package/src/modes/utils/read-group-outcome-aggregator.ts +55 -0
  528. package/src/modes/utils/sanitize-error-message.ts +60 -0
  529. package/src/modes/utils/tools-markdown.ts +28 -0
  530. package/src/modes/utils/ui-helpers.ts +734 -0
  531. package/src/plan-mode/approved-plan.ts +55 -0
  532. package/src/plan-mode/state.ts +6 -0
  533. package/src/priority.json +37 -0
  534. package/src/prompts/agents/designer.md +67 -0
  535. package/src/prompts/agents/explore.md +61 -0
  536. package/src/prompts/agents/frontmatter.md +10 -0
  537. package/src/prompts/agents/init.md +36 -0
  538. package/src/prompts/agents/librarian.md +121 -0
  539. package/src/prompts/agents/plan.md +49 -0
  540. package/src/prompts/agents/reviewer.md +127 -0
  541. package/src/prompts/agents/task.md +16 -0
  542. package/src/prompts/ci-green-request.md +35 -0
  543. package/src/prompts/compaction/branch-summary-context.md +5 -0
  544. package/src/prompts/compaction/branch-summary-preamble.md +2 -0
  545. package/src/prompts/compaction/branch-summary.md +30 -0
  546. package/src/prompts/compaction/compaction-short-summary.md +9 -0
  547. package/src/prompts/compaction/compaction-summary-context.md +5 -0
  548. package/src/prompts/compaction/compaction-summary.md +38 -0
  549. package/src/prompts/compaction/compaction-turn-prefix.md +17 -0
  550. package/src/prompts/compaction/compaction-update-summary.md +45 -0
  551. package/src/prompts/memories/consolidation.md +30 -0
  552. package/src/prompts/memories/read-path.md +11 -0
  553. package/src/prompts/memories/stage_one_input.md +6 -0
  554. package/src/prompts/memories/stage_one_system.md +21 -0
  555. package/src/prompts/review-request.md +70 -0
  556. package/src/prompts/system/agent-creation-architect.md +65 -0
  557. package/src/prompts/system/agent-creation-user.md +6 -0
  558. package/src/prompts/system/auto-handoff-threshold-focus.md +1 -0
  559. package/src/prompts/system/btw-user.md +8 -0
  560. package/src/prompts/system/commit-message-system.md +2 -0
  561. package/src/prompts/system/custom-system-prompt.md +82 -0
  562. package/src/prompts/system/eager-todo.md +13 -0
  563. package/src/prompts/system/file-operations.md +10 -0
  564. package/src/prompts/system/handoff-document.md +48 -0
  565. package/src/prompts/system/plan-mode-active.md +114 -0
  566. package/src/prompts/system/plan-mode-approved.md +23 -0
  567. package/src/prompts/system/plan-mode-reference.md +14 -0
  568. package/src/prompts/system/plan-mode-subagent.md +34 -0
  569. package/src/prompts/system/plan-mode-tool-decision-reminder.md +9 -0
  570. package/src/prompts/system/subagent-submit-reminder.md +7 -0
  571. package/src/prompts/system/subagent-system-prompt.md +36 -0
  572. package/src/prompts/system/subagent-user-prompt.md +12 -0
  573. package/src/prompts/system/summarization-system.md +3 -0
  574. package/src/prompts/system/system-prompt.md +698 -0
  575. package/src/prompts/system/title-system.md +2 -0
  576. package/src/prompts/system/ttsr-interrupt.md +7 -0
  577. package/src/prompts/system/web-search.md +28 -0
  578. package/src/prompts/tools/ask.md +28 -0
  579. package/src/prompts/tools/ast-edit.md +48 -0
  580. package/src/prompts/tools/ast-grep.md +54 -0
  581. package/src/prompts/tools/async-result.md +5 -0
  582. package/src/prompts/tools/bash.md +67 -0
  583. package/src/prompts/tools/browser.md +26 -0
  584. package/src/prompts/tools/calculator.md +10 -0
  585. package/src/prompts/tools/cancel-job.md +5 -0
  586. package/src/prompts/tools/catalog-workflow-runner.md +13 -0
  587. package/src/prompts/tools/checkpoint.md +16 -0
  588. package/src/prompts/tools/chunk-edit.md +279 -0
  589. package/src/prompts/tools/debug.md +43 -0
  590. package/src/prompts/tools/display-image.md +23 -0
  591. package/src/prompts/tools/exit-plan-mode.md +41 -0
  592. package/src/prompts/tools/find.md +26 -0
  593. package/src/prompts/tools/gemini-image.md +7 -0
  594. package/src/prompts/tools/grep.md +29 -0
  595. package/src/prompts/tools/hashline.md +131 -0
  596. package/src/prompts/tools/inspect-image-system.md +20 -0
  597. package/src/prompts/tools/inspect-image.md +32 -0
  598. package/src/prompts/tools/lsp.md +33 -0
  599. package/src/prompts/tools/patch.md +77 -0
  600. package/src/prompts/tools/poll.md +5 -0
  601. package/src/prompts/tools/python.md +60 -0
  602. package/src/prompts/tools/read-chunk.md +35 -0
  603. package/src/prompts/tools/read.md +63 -0
  604. package/src/prompts/tools/render-mermaid.md +9 -0
  605. package/src/prompts/tools/replace.md +36 -0
  606. package/src/prompts/tools/resolve.md +8 -0
  607. package/src/prompts/tools/rewind.md +13 -0
  608. package/src/prompts/tools/search-tool-bm25.md +34 -0
  609. package/src/prompts/tools/ssh.md +44 -0
  610. package/src/prompts/tools/task-summary.md +28 -0
  611. package/src/prompts/tools/task.md +131 -0
  612. package/src/prompts/tools/todo-write.md +89 -0
  613. package/src/prompts/tools/vim.md +98 -0
  614. package/src/prompts/tools/web-search.md +10 -0
  615. package/src/prompts/tools/write.md +14 -0
  616. package/src/prompts/tools/xcsh-api.md +283 -0
  617. package/src/resource-management/index.ts +42 -0
  618. package/src/sdk.ts +2123 -0
  619. package/src/secrets/index.ts +146 -0
  620. package/src/secrets/obfuscator.ts +290 -0
  621. package/src/secrets/regex.ts +21 -0
  622. package/src/services/context-env.ts +102 -0
  623. package/src/services/xcsh-api-client.ts +230 -0
  624. package/src/services/xcsh-context-command.ts +803 -0
  625. package/src/services/xcsh-context-display.ts +49 -0
  626. package/src/services/xcsh-context-indicators.ts +17 -0
  627. package/src/services/xcsh-context-segment.ts +39 -0
  628. package/src/services/xcsh-context.ts +1410 -0
  629. package/src/services/xcsh-env.ts +107 -0
  630. package/src/services/xcsh-knowledge.ts +177 -0
  631. package/src/services/xcsh-table.ts +164 -0
  632. package/src/session/agent-session.ts +6690 -0
  633. package/src/session/agent-storage.ts +451 -0
  634. package/src/session/artifacts.ts +132 -0
  635. package/src/session/auth-storage.ts +17 -0
  636. package/src/session/blob-store.ts +135 -0
  637. package/src/session/compaction/branch-summarization.ts +322 -0
  638. package/src/session/compaction/compaction.ts +1388 -0
  639. package/src/session/compaction/index.ts +7 -0
  640. package/src/session/compaction/pruning.ts +91 -0
  641. package/src/session/compaction/utils.ts +184 -0
  642. package/src/session/history-storage.ts +253 -0
  643. package/src/session/messages.ts +597 -0
  644. package/src/session/session-dump-format.ts +203 -0
  645. package/src/session/session-manager.ts +2816 -0
  646. package/src/session/session-storage.ts +370 -0
  647. package/src/session/streaming-output.ts +770 -0
  648. package/src/session/tool-choice-queue.ts +213 -0
  649. package/src/slash-commands/builtin-registry.ts +1614 -0
  650. package/src/slash-commands/export-command.ts +178 -0
  651. package/src/slash-commands/marketplace-install-parser.ts +99 -0
  652. package/src/slash-commands/resource-commands.ts +250 -0
  653. package/src/ssh/config-writer.ts +183 -0
  654. package/src/ssh/connection-manager.ts +445 -0
  655. package/src/ssh/ssh-executor.ts +131 -0
  656. package/src/ssh/sshfs-mount.ts +137 -0
  657. package/src/ssh/utils.ts +8 -0
  658. package/src/stt/downloader.ts +71 -0
  659. package/src/stt/index.ts +3 -0
  660. package/src/stt/recorder.ts +351 -0
  661. package/src/stt/setup.ts +52 -0
  662. package/src/stt/stt-controller.ts +160 -0
  663. package/src/stt/transcribe.py +70 -0
  664. package/src/stt/transcriber.ts +91 -0
  665. package/src/system-prompt.ts +688 -0
  666. package/src/task/agents.ts +166 -0
  667. package/src/task/commands.ts +131 -0
  668. package/src/task/discovery.ts +126 -0
  669. package/src/task/executor.ts +1324 -0
  670. package/src/task/index.ts +1197 -0
  671. package/src/task/isolation-backend.ts +72 -0
  672. package/src/task/name-generator.ts +1577 -0
  673. package/src/task/output-manager.ts +107 -0
  674. package/src/task/parallel.ts +116 -0
  675. package/src/task/render.ts +1035 -0
  676. package/src/task/subprocess-tool-registry.ts +88 -0
  677. package/src/task/template.ts +33 -0
  678. package/src/task/types.ts +230 -0
  679. package/src/task/worktree.ts +580 -0
  680. package/src/task/xcsh-command.ts +26 -0
  681. package/src/thinking.ts +87 -0
  682. package/src/tools/action-renderer.ts +198 -0
  683. package/src/tools/archive-reader.ts +315 -0
  684. package/src/tools/ask.ts +791 -0
  685. package/src/tools/ast-edit.ts +492 -0
  686. package/src/tools/ast-grep.ts +453 -0
  687. package/src/tools/auto-generated-guard.ts +305 -0
  688. package/src/tools/bash-interactive.ts +383 -0
  689. package/src/tools/bash-interceptor.ts +67 -0
  690. package/src/tools/bash-normalize.ts +107 -0
  691. package/src/tools/bash-skill-urls.ts +246 -0
  692. package/src/tools/bash.ts +878 -0
  693. package/src/tools/browser-renderer.ts +129 -0
  694. package/src/tools/browser.ts +1366 -0
  695. package/src/tools/calculator.ts +543 -0
  696. package/src/tools/cancel-job.ts +100 -0
  697. package/src/tools/catalog-workflow-runner.ts +718 -0
  698. package/src/tools/checkpoint.ts +133 -0
  699. package/src/tools/context.ts +39 -0
  700. package/src/tools/debug.ts +1004 -0
  701. package/src/tools/display-image-renderer.ts +96 -0
  702. package/src/tools/display-image.ts +109 -0
  703. package/src/tools/exit-plan-mode.ts +101 -0
  704. package/src/tools/fetch.ts +1462 -0
  705. package/src/tools/find.ts +437 -0
  706. package/src/tools/fs-cache-invalidation.ts +33 -0
  707. package/src/tools/gemini-image.ts +1004 -0
  708. package/src/tools/grep.ts +705 -0
  709. package/src/tools/index.ts +442 -0
  710. package/src/tools/inspect-image-renderer.ts +101 -0
  711. package/src/tools/inspect-image.ts +176 -0
  712. package/src/tools/json-tree.ts +266 -0
  713. package/src/tools/jtd-to-json-schema.ts +199 -0
  714. package/src/tools/jtd-to-typescript.ts +136 -0
  715. package/src/tools/jtd-utils.ts +102 -0
  716. package/src/tools/list-limit.ts +40 -0
  717. package/src/tools/mermaid-renderer.ts +127 -0
  718. package/src/tools/notebook.ts +288 -0
  719. package/src/tools/output-meta.ts +588 -0
  720. package/src/tools/path-utils.ts +560 -0
  721. package/src/tools/plan-mode-guard.ts +41 -0
  722. package/src/tools/poll-tool.ts +177 -0
  723. package/src/tools/puppeteer/00_stealth_tampering.txt +63 -0
  724. package/src/tools/puppeteer/01_stealth_activity.txt +20 -0
  725. package/src/tools/puppeteer/02_stealth_hairline.txt +11 -0
  726. package/src/tools/puppeteer/03_stealth_botd.txt +384 -0
  727. package/src/tools/puppeteer/04_stealth_iframe.txt +81 -0
  728. package/src/tools/puppeteer/05_stealth_webgl.txt +75 -0
  729. package/src/tools/puppeteer/06_stealth_screen.txt +72 -0
  730. package/src/tools/puppeteer/07_stealth_fonts.txt +97 -0
  731. package/src/tools/puppeteer/08_stealth_audio.txt +51 -0
  732. package/src/tools/puppeteer/09_stealth_locale.txt +46 -0
  733. package/src/tools/puppeteer/10_stealth_plugins.txt +206 -0
  734. package/src/tools/puppeteer/11_stealth_hardware.txt +8 -0
  735. package/src/tools/puppeteer/12_stealth_codecs.txt +40 -0
  736. package/src/tools/puppeteer/13_stealth_worker.txt +74 -0
  737. package/src/tools/python.ts +1184 -0
  738. package/src/tools/read.ts +1591 -0
  739. package/src/tools/render-mermaid.ts +93 -0
  740. package/src/tools/render-utils.ts +677 -0
  741. package/src/tools/renderers.ts +82 -0
  742. package/src/tools/report-tool-issue.ts +80 -0
  743. package/src/tools/resolve.ts +196 -0
  744. package/src/tools/review.ts +238 -0
  745. package/src/tools/search-tool-bm25.ts +288 -0
  746. package/src/tools/sqlite-reader.ts +623 -0
  747. package/src/tools/ssh.ts +322 -0
  748. package/src/tools/submit-result.ts +229 -0
  749. package/src/tools/todo-render.ts +33 -0
  750. package/src/tools/todo-write.ts +440 -0
  751. package/src/tools/tool-errors.ts +62 -0
  752. package/src/tools/tool-result.ts +93 -0
  753. package/src/tools/tool-timeouts.ts +30 -0
  754. package/src/tools/vim.ts +981 -0
  755. package/src/tools/write.ts +671 -0
  756. package/src/tools/xcsh-api-renderer.ts +402 -0
  757. package/src/tools/xcsh-api.ts +887 -0
  758. package/src/tui/code-cell.ts +101 -0
  759. package/src/tui/file-list.ts +47 -0
  760. package/src/tui/index.ts +11 -0
  761. package/src/tui/output-block.ts +175 -0
  762. package/src/tui/status-line.ts +39 -0
  763. package/src/tui/tree-list.ts +84 -0
  764. package/src/tui/types.ts +15 -0
  765. package/src/tui/utils.ts +103 -0
  766. package/src/utils/changelog.ts +98 -0
  767. package/src/utils/clipboard.ts +80 -0
  768. package/src/utils/command-args.ts +76 -0
  769. package/src/utils/commit-message-generator.ts +137 -0
  770. package/src/utils/edit-mode.ts +50 -0
  771. package/src/utils/event-bus.ts +33 -0
  772. package/src/utils/external-editor.ts +65 -0
  773. package/src/utils/file-display-mode.ts +37 -0
  774. package/src/utils/file-mentions.ts +376 -0
  775. package/src/utils/fuzzy.ts +108 -0
  776. package/src/utils/git.ts +1495 -0
  777. package/src/utils/gitstatus.ts +140 -0
  778. package/src/utils/image-convert.ts +27 -0
  779. package/src/utils/image-loading.ts +98 -0
  780. package/src/utils/image-passthrough.ts +118 -0
  781. package/src/utils/image-resize.ts +272 -0
  782. package/src/utils/image-viewer.ts +17 -0
  783. package/src/utils/lang-from-path.ts +239 -0
  784. package/src/utils/markit.ts +81 -0
  785. package/src/utils/open.ts +20 -0
  786. package/src/utils/session-color.ts +55 -0
  787. package/src/utils/shell-snapshot.ts +188 -0
  788. package/src/utils/title-generator.ts +193 -0
  789. package/src/utils/tool-choice.ts +28 -0
  790. package/src/utils/tools-manager.ts +356 -0
  791. package/src/vim/buffer.ts +309 -0
  792. package/src/vim/commands.ts +382 -0
  793. package/src/vim/engine.ts +2426 -0
  794. package/src/vim/parser.ts +151 -0
  795. package/src/vim/render.ts +252 -0
  796. package/src/vim/types.ts +197 -0
  797. package/src/web/kagi.ts +174 -0
  798. package/src/web/parallel.ts +346 -0
  799. package/src/web/scrapers/artifacthub.ts +207 -0
  800. package/src/web/scrapers/arxiv.ts +83 -0
  801. package/src/web/scrapers/aur.ts +162 -0
  802. package/src/web/scrapers/biorxiv.ts +133 -0
  803. package/src/web/scrapers/bluesky.ts +262 -0
  804. package/src/web/scrapers/brew.ts +172 -0
  805. package/src/web/scrapers/cheatsh.ts +68 -0
  806. package/src/web/scrapers/chocolatey.ts +196 -0
  807. package/src/web/scrapers/choosealicense.ts +95 -0
  808. package/src/web/scrapers/cisa-kev.ts +87 -0
  809. package/src/web/scrapers/clojars.ts +154 -0
  810. package/src/web/scrapers/coingecko.ts +177 -0
  811. package/src/web/scrapers/crates-io.ts +110 -0
  812. package/src/web/scrapers/crossref.ts +136 -0
  813. package/src/web/scrapers/devto.ts +147 -0
  814. package/src/web/scrapers/discogs.ts +306 -0
  815. package/src/web/scrapers/discourse.ts +197 -0
  816. package/src/web/scrapers/dockerhub.ts +138 -0
  817. package/src/web/scrapers/docs-rs.ts +653 -0
  818. package/src/web/scrapers/fdroid.ts +134 -0
  819. package/src/web/scrapers/firefox-addons.ts +191 -0
  820. package/src/web/scrapers/flathub.ts +223 -0
  821. package/src/web/scrapers/github-gist.ts +58 -0
  822. package/src/web/scrapers/github.ts +452 -0
  823. package/src/web/scrapers/gitlab.ts +401 -0
  824. package/src/web/scrapers/go-pkg.ts +266 -0
  825. package/src/web/scrapers/hackage.ts +140 -0
  826. package/src/web/scrapers/hackernews.ts +189 -0
  827. package/src/web/scrapers/hex.ts +105 -0
  828. package/src/web/scrapers/huggingface.ts +321 -0
  829. package/src/web/scrapers/iacr.ts +89 -0
  830. package/src/web/scrapers/index.ts +252 -0
  831. package/src/web/scrapers/jetbrains-marketplace.ts +159 -0
  832. package/src/web/scrapers/lemmy.ts +203 -0
  833. package/src/web/scrapers/lobsters.ts +175 -0
  834. package/src/web/scrapers/mastodon.ts +292 -0
  835. package/src/web/scrapers/maven.ts +138 -0
  836. package/src/web/scrapers/mdn.ts +169 -0
  837. package/src/web/scrapers/metacpan.ts +222 -0
  838. package/src/web/scrapers/musicbrainz.ts +250 -0
  839. package/src/web/scrapers/npm.ts +98 -0
  840. package/src/web/scrapers/nuget.ts +183 -0
  841. package/src/web/scrapers/nvd.ts +222 -0
  842. package/src/web/scrapers/ollama.ts +239 -0
  843. package/src/web/scrapers/open-vsx.ts +106 -0
  844. package/src/web/scrapers/opencorporates.ts +292 -0
  845. package/src/web/scrapers/openlibrary.ts +336 -0
  846. package/src/web/scrapers/orcid.ts +286 -0
  847. package/src/web/scrapers/osv.ts +176 -0
  848. package/src/web/scrapers/packagist.ts +160 -0
  849. package/src/web/scrapers/pub-dev.ts +143 -0
  850. package/src/web/scrapers/pubmed.ts +211 -0
  851. package/src/web/scrapers/pypi.ts +112 -0
  852. package/src/web/scrapers/rawg.ts +110 -0
  853. package/src/web/scrapers/readthedocs.ts +120 -0
  854. package/src/web/scrapers/reddit.ts +95 -0
  855. package/src/web/scrapers/repology.ts +251 -0
  856. package/src/web/scrapers/rfc.ts +201 -0
  857. package/src/web/scrapers/rubygems.ts +103 -0
  858. package/src/web/scrapers/searchcode.ts +190 -0
  859. package/src/web/scrapers/sec-edgar.ts +261 -0
  860. package/src/web/scrapers/semantic-scholar.ts +171 -0
  861. package/src/web/scrapers/snapcraft.ts +187 -0
  862. package/src/web/scrapers/sourcegraph.ts +336 -0
  863. package/src/web/scrapers/spdx.ts +108 -0
  864. package/src/web/scrapers/spotify.ts +198 -0
  865. package/src/web/scrapers/stackoverflow.ts +120 -0
  866. package/src/web/scrapers/terraform.ts +277 -0
  867. package/src/web/scrapers/tldr.ts +47 -0
  868. package/src/web/scrapers/twitter.ts +93 -0
  869. package/src/web/scrapers/types.ts +267 -0
  870. package/src/web/scrapers/utils.ts +109 -0
  871. package/src/web/scrapers/vimeo.ts +133 -0
  872. package/src/web/scrapers/vscode-marketplace.ts +187 -0
  873. package/src/web/scrapers/w3c.ts +156 -0
  874. package/src/web/scrapers/wikidata.ts +344 -0
  875. package/src/web/scrapers/wikipedia.ts +84 -0
  876. package/src/web/scrapers/youtube.ts +313 -0
  877. package/src/web/search/errors.ts +120 -0
  878. package/src/web/search/index.ts +346 -0
  879. package/src/web/search/params.ts +129 -0
  880. package/src/web/search/provider.ts +97 -0
  881. package/src/web/search/providers/anthropic.ts +339 -0
  882. package/src/web/search/providers/base.ts +35 -0
  883. package/src/web/search/providers/brave.ts +150 -0
  884. package/src/web/search/providers/codex.ts +402 -0
  885. package/src/web/search/providers/exa.ts +264 -0
  886. package/src/web/search/providers/firecrawl.ts +115 -0
  887. package/src/web/search/providers/gemini.ts +566 -0
  888. package/src/web/search/providers/jina.ts +99 -0
  889. package/src/web/search/providers/kagi.ts +70 -0
  890. package/src/web/search/providers/kimi.ts +157 -0
  891. package/src/web/search/providers/parallel.ts +63 -0
  892. package/src/web/search/providers/perplexity.ts +546 -0
  893. package/src/web/search/providers/synthetic.ts +113 -0
  894. package/src/web/search/providers/tavily.ts +162 -0
  895. package/src/web/search/providers/utils.ts +36 -0
  896. package/src/web/search/providers/zai.ts +311 -0
  897. package/src/web/search/render.ts +348 -0
  898. package/src/web/search/types.ts +438 -0
  899. package/src/web/search/utils.ts +17 -0
@@ -0,0 +1,2426 @@
1
+ import type { FileDiagnosticsResult } from "../lsp";
2
+ import { snapshotEqual, type VimBuffer } from "./buffer";
3
+ import { parseExCommand } from "./commands";
4
+ import { replayTokens } from "./parser";
5
+ import type {
6
+ Position,
7
+ VimBufferSnapshot,
8
+ VimInputMode,
9
+ VimKeyToken,
10
+ VimLineRange,
11
+ VimLoadedFile,
12
+ VimPendingInput,
13
+ VimRegister,
14
+ VimSearchState,
15
+ VimSelection,
16
+ VimUndoEntry,
17
+ } from "./types";
18
+ import { clonePosition, maxPosition, minPosition, toPublicMode, VimInputError as VimError } from "./types";
19
+
20
+ export interface VimSaveResult {
21
+ loaded: VimLoadedFile;
22
+ diagnostics?: FileDiagnosticsResult;
23
+ }
24
+
25
+ export interface VimEngineCallbacks {
26
+ beforeMutate: (buffer: VimBuffer) => Promise<void>;
27
+ loadBuffer: (path: string) => Promise<VimLoadedFile>;
28
+ saveBuffer: (buffer: VimBuffer, options?: { force?: boolean }) => Promise<VimSaveResult>;
29
+ }
30
+
31
+ interface PendingChange {
32
+ before: VimBufferSnapshot;
33
+ tokens: string[];
34
+ moveCursorLeftOnEscape: boolean;
35
+ inserted: boolean;
36
+ }
37
+
38
+ interface MotionResult {
39
+ nextIndex: number;
40
+ target: Position;
41
+ inclusive?: boolean;
42
+ linewise?: boolean;
43
+ range?: { start: number; end: number; linewise?: boolean };
44
+ }
45
+
46
+ const WORD_CHAR = /[A-Za-z0-9_]/;
47
+ const DEFAULT_VIEWPORT_HEIGHT = 10;
48
+ const BRACKET_PAIRS = new Map<string, string>([
49
+ ["(", ")"],
50
+ ["[", "]"],
51
+ ["{", "}"],
52
+ ["<", ">"],
53
+ ]);
54
+ const CLOSING_BRACKETS = new Map<string, string>(
55
+ Array.from(BRACKET_PAIRS.entries()).map(([open, close]) => [close, open]),
56
+ );
57
+ const NOOP_Z_COMMANDS = new Set(["a", "A", "c", "C", "m", "M", "o", "O", "r", "R", "v", "x", "X"]);
58
+
59
+ function escapeRegex(value: string): string {
60
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
61
+ }
62
+
63
+ function isWhitespace(char: string): boolean {
64
+ return /\s/.test(char);
65
+ }
66
+
67
+ function isWordChar(char: string): boolean {
68
+ return WORD_CHAR.test(char);
69
+ }
70
+
71
+ function wordCategory(char: string, bigWord: boolean): "space" | "word" | "punct" {
72
+ if (char.length === 0 || isWhitespace(char)) {
73
+ return "space";
74
+ }
75
+ if (bigWord) {
76
+ return "word";
77
+ }
78
+ return isWordChar(char) ? "word" : "punct";
79
+ }
80
+
81
+ function decodeReplacement(replacement: string): string {
82
+ return replacement.replace(/\\\//g, "/").replace(/\\\\/g, "\\");
83
+ }
84
+
85
+ function literalTextToReplayTokens(text: string): string[] {
86
+ const tokens: string[] = [];
87
+ for (const char of text) {
88
+ if (char === "\n") {
89
+ tokens.push("CR");
90
+ continue;
91
+ }
92
+ if (char === "\t") {
93
+ tokens.push("Tab");
94
+ continue;
95
+ }
96
+ tokens.push(char);
97
+ }
98
+ return tokens;
99
+ }
100
+
101
+ // Convert a vim-style search pattern to a JavaScript RegExp.
102
+ // In vim's default ("magic") mode, (, ), {, }, |, + are literal unless backslash-escaped.
103
+ // In JS regex these are metacharacters. Swap the escaping so bare chars are literal
104
+ // and \( etc. become regex groups.
105
+ function vimPatternToJsRegex(pattern: string): string {
106
+ return pattern.replace(/\\([(){}|+])|([(){}|+])/g, (_match, escaped, bare) => {
107
+ if (escaped) return escaped; // \( -> ( (regex group)
108
+ return `\\${bare}`; // ( -> \( (literal paren)
109
+ });
110
+ }
111
+
112
+ function createSearchRegex(pattern: string, flags = "g"): RegExp {
113
+ try {
114
+ return new RegExp(vimPatternToJsRegex(pattern), flags);
115
+ } catch {
116
+ return new RegExp(escapeRegex(pattern), flags);
117
+ }
118
+ }
119
+
120
+ function detectIndentUnit(lines: string[]): string {
121
+ for (const line of lines) {
122
+ if (line.startsWith("\t")) {
123
+ return "\t";
124
+ }
125
+ if (line.startsWith(" ")) {
126
+ return " ";
127
+ }
128
+ }
129
+ return "\t";
130
+ }
131
+
132
+ function normalizeRange(start: number, end: number): { start: number; end: number } {
133
+ return {
134
+ start: Math.min(start, end),
135
+ end: Math.max(start, end),
136
+ };
137
+ }
138
+
139
+ function selectionFromAnchor(buffer: VimBuffer, anchor: Position, linewise: boolean): VimSelection {
140
+ if (linewise) {
141
+ const startLine = Math.min(anchor.line, buffer.cursor.line);
142
+ const endLine = Math.max(anchor.line, buffer.cursor.line);
143
+ return {
144
+ kind: "line",
145
+ start: { line: startLine + 1, col: 1 },
146
+ end: { line: endLine + 1, col: buffer.getLine(endLine).length + 1 },
147
+ };
148
+ }
149
+ const start = minPosition(anchor, buffer.cursor);
150
+ const end = maxPosition(anchor, buffer.cursor);
151
+ return {
152
+ kind: "char",
153
+ start: { line: start.line + 1, col: start.col + 1 },
154
+ end: { line: end.line + 1, col: end.col + 1 },
155
+ };
156
+ }
157
+
158
+ function expandVisualOffsets(
159
+ buffer: VimBuffer,
160
+ anchor: Position,
161
+ linewise: boolean,
162
+ ): { start: number; end: number; linewise: boolean } {
163
+ if (linewise) {
164
+ const startLine = Math.min(anchor.line, buffer.cursor.line);
165
+ const endLine = Math.max(anchor.line, buffer.cursor.line);
166
+ const startOffset = buffer.positionToOffset({ line: startLine, col: 0 });
167
+ const endOffset =
168
+ endLine >= buffer.lastLineIndex()
169
+ ? buffer.getText().length
170
+ : buffer.positionToOffset({ line: endLine + 1, col: 0 });
171
+ return { start: startOffset, end: endOffset, linewise: true };
172
+ }
173
+ const anchorOffset = buffer.positionToOffset(anchor);
174
+ const cursorOffset = buffer.positionToOffset(buffer.cursor);
175
+ const { start, end } = normalizeRange(anchorOffset, cursorOffset);
176
+ return { start, end: end + 1, linewise: false };
177
+ }
178
+
179
+ function nextWordStart(text: string, offset: number, bigWord: boolean): number {
180
+ let index = Math.min(Math.max(offset, 0), text.length);
181
+ if (index >= text.length) {
182
+ return text.length;
183
+ }
184
+
185
+ const currentCategory = wordCategory(text[index] ?? "", bigWord);
186
+ if (currentCategory === "space") {
187
+ while (index < text.length && wordCategory(text[index] ?? "", bigWord) === "space") {
188
+ index += 1;
189
+ }
190
+ return index;
191
+ }
192
+
193
+ while (index < text.length && wordCategory(text[index] ?? "", bigWord) === currentCategory) {
194
+ index += 1;
195
+ }
196
+ while (index < text.length && wordCategory(text[index] ?? "", bigWord) === "space") {
197
+ index += 1;
198
+ }
199
+ return index;
200
+ }
201
+
202
+ function previousWordStart(text: string, offset: number, bigWord: boolean): number {
203
+ let index = Math.min(Math.max(offset - 1, 0), text.length);
204
+ while (index > 0 && wordCategory(text[index] ?? "", bigWord) === "space") {
205
+ index -= 1;
206
+ }
207
+ const category = wordCategory(text[index] ?? "", bigWord);
208
+ while (index > 0 && wordCategory(text[index - 1] ?? "", bigWord) === category) {
209
+ index -= 1;
210
+ }
211
+ return index;
212
+ }
213
+
214
+ function endOfWord(text: string, offset: number, bigWord: boolean): number {
215
+ let index = Math.min(Math.max(offset, 0), text.length);
216
+ while (index < text.length && wordCategory(text[index] ?? "", bigWord) === "space") {
217
+ index += 1;
218
+ }
219
+ const category = wordCategory(text[index] ?? "", bigWord);
220
+ while (index < text.length && wordCategory(text[index] ?? "", bigWord) === category) {
221
+ index += 1;
222
+ }
223
+ return Math.max(0, index - 1);
224
+ }
225
+
226
+ function endOfPreviousWord(text: string, offset: number, bigWord: boolean): number {
227
+ if (text.length === 0) {
228
+ return 0;
229
+ }
230
+
231
+ let index = Math.min(Math.max(offset - 1, 0), text.length - 1);
232
+ while (index >= 0 && wordCategory(text[index] ?? "", bigWord) === "space") {
233
+ index -= 1;
234
+ }
235
+ if (index < 0) {
236
+ return 0;
237
+ }
238
+
239
+ const currentCategory = wordCategory(text[index] ?? "", bigWord);
240
+ while (index >= 0 && wordCategory(text[index] ?? "", bigWord) === currentCategory) {
241
+ index -= 1;
242
+ }
243
+ while (index >= 0 && wordCategory(text[index] ?? "", bigWord) === "space") {
244
+ index -= 1;
245
+ }
246
+
247
+ return Math.max(0, index);
248
+ }
249
+
250
+ function toggleCase(text: string): string {
251
+ let toggled = "";
252
+ for (const char of text) {
253
+ if (char >= "a" && char <= "z") {
254
+ toggled += char.toUpperCase();
255
+ continue;
256
+ }
257
+ if (char >= "A" && char <= "Z") {
258
+ toggled += char.toLowerCase();
259
+ continue;
260
+ }
261
+ toggled += char;
262
+ }
263
+ return toggled;
264
+ }
265
+
266
+ function lastNonBlankColumn(line: string): number {
267
+ for (let index = line.length - 1; index >= 0; index -= 1) {
268
+ if (!isWhitespace(line[index] ?? "")) {
269
+ return index;
270
+ }
271
+ }
272
+ return 0;
273
+ }
274
+
275
+ function findParagraphStart(lines: string[], line: number): number {
276
+ let index = Math.max(0, line - 1);
277
+ while (index > 0 && lines[index]!.trim().length > 0) {
278
+ index -= 1;
279
+ }
280
+ while (index > 0 && lines[index - 1]!.trim().length === 0) {
281
+ index -= 1;
282
+ }
283
+ return index;
284
+ }
285
+
286
+ function findParagraphEnd(lines: string[], line: number): number {
287
+ let index = Math.min(lines.length - 1, line + 1);
288
+ while (index < lines.length - 1 && lines[index]!.trim().length > 0) {
289
+ index += 1;
290
+ }
291
+ while (index < lines.length - 1 && lines[index + 1]!.trim().length === 0) {
292
+ index += 1;
293
+ }
294
+ return index;
295
+ }
296
+
297
+ export class VimEngine {
298
+ buffer: VimBuffer;
299
+ inputMode: VimInputMode = "normal";
300
+ selectionAnchor: Position | null = null;
301
+ register: VimRegister = { kind: "char", text: "" };
302
+ lastSearch: VimSearchState | null = null;
303
+ lastCharFind: { char: string; mode: "f" | "F" | "t" | "T" } | null = null;
304
+ lastVisual: { anchor: Position; cursor: Position; mode: VimInputMode } | null = null;
305
+ lastCommand?: string;
306
+ statusMessage?: string;
307
+ diagnostics?: FileDiagnosticsResult;
308
+ viewportStart = 1;
309
+ closed = false;
310
+
311
+ #callbacks: VimEngineCallbacks;
312
+ #undoStack: VimUndoEntry[] = [];
313
+ #redoStack: VimUndoEntry[] = [];
314
+ #pendingInput = "";
315
+ #lastChangeTokens: string[] | null = null;
316
+ #pendingChange: PendingChange | null = null;
317
+ #stepCallback?: () => Promise<void>;
318
+
319
+ constructor(buffer: VimBuffer, callbacks: VimEngineCallbacks) {
320
+ this.buffer = buffer;
321
+ this.#callbacks = callbacks;
322
+ }
323
+
324
+ clone(callbacks?: Partial<VimEngineCallbacks>): VimEngine {
325
+ const next = new VimEngine(this.buffer.clone(), {
326
+ beforeMutate: callbacks?.beforeMutate ?? this.#callbacks.beforeMutate,
327
+ loadBuffer: callbacks?.loadBuffer ?? this.#callbacks.loadBuffer,
328
+ saveBuffer: callbacks?.saveBuffer ?? this.#callbacks.saveBuffer,
329
+ });
330
+ next.inputMode = this.inputMode;
331
+ next.selectionAnchor = this.selectionAnchor ? clonePosition(this.selectionAnchor) : null;
332
+ next.register = { ...this.register };
333
+ next.lastSearch = this.lastSearch ? { ...this.lastSearch } : null;
334
+ next.lastCharFind = this.lastCharFind ? { ...this.lastCharFind } : null;
335
+ next.lastVisual = this.lastVisual
336
+ ? {
337
+ anchor: clonePosition(this.lastVisual.anchor),
338
+ cursor: clonePosition(this.lastVisual.cursor),
339
+ mode: this.lastVisual.mode,
340
+ }
341
+ : null;
342
+ next.lastCommand = this.lastCommand;
343
+ next.statusMessage = this.statusMessage;
344
+ next.diagnostics = this.diagnostics;
345
+ next.viewportStart = this.viewportStart;
346
+ next.closed = this.closed;
347
+ next.#pendingInput = this.#pendingInput;
348
+ next.#lastChangeTokens = this.#lastChangeTokens ? [...this.#lastChangeTokens] : null;
349
+ next.#pendingChange = this.#pendingChange
350
+ ? {
351
+ before: {
352
+ ...this.#pendingChange.before,
353
+ lines: [...this.#pendingChange.before.lines],
354
+ cursor: clonePosition(this.#pendingChange.before.cursor),
355
+ baseFingerprint: this.#pendingChange.before.baseFingerprint
356
+ ? { ...this.#pendingChange.before.baseFingerprint }
357
+ : null,
358
+ },
359
+ tokens: [...this.#pendingChange.tokens],
360
+ moveCursorLeftOnEscape: this.#pendingChange.moveCursorLeftOnEscape,
361
+ inserted: this.#pendingChange.inserted,
362
+ }
363
+ : null;
364
+ next.#undoStack = this.#undoStack.map(entry => ({
365
+ before: {
366
+ ...entry.before,
367
+ lines: [...entry.before.lines],
368
+ cursor: clonePosition(entry.before.cursor),
369
+ baseFingerprint: entry.before.baseFingerprint ? { ...entry.before.baseFingerprint } : null,
370
+ },
371
+ after: {
372
+ ...entry.after,
373
+ lines: [...entry.after.lines],
374
+ cursor: clonePosition(entry.after.cursor),
375
+ baseFingerprint: entry.after.baseFingerprint ? { ...entry.after.baseFingerprint } : null,
376
+ },
377
+ }));
378
+ next.#redoStack = this.#redoStack.map(entry => ({
379
+ before: {
380
+ ...entry.before,
381
+ lines: [...entry.before.lines],
382
+ cursor: clonePosition(entry.before.cursor),
383
+ baseFingerprint: entry.before.baseFingerprint ? { ...entry.before.baseFingerprint } : null,
384
+ },
385
+ after: {
386
+ ...entry.after,
387
+ lines: [...entry.after.lines],
388
+ cursor: clonePosition(entry.after.cursor),
389
+ baseFingerprint: entry.after.baseFingerprint ? { ...entry.after.baseFingerprint } : null,
390
+ },
391
+ }));
392
+ return next;
393
+ }
394
+
395
+ getPublicMode() {
396
+ return toPublicMode(this.inputMode);
397
+ }
398
+
399
+ getSelection(): VimSelection | undefined {
400
+ if (this.selectionAnchor === null) {
401
+ return undefined;
402
+ }
403
+ return selectionFromAnchor(this.buffer, this.selectionAnchor, this.inputMode === "visual-line");
404
+ }
405
+
406
+ getPendingInput(): VimPendingInput | undefined {
407
+ switch (this.inputMode) {
408
+ case "insert":
409
+ return { kind: "insert", text: "" };
410
+ case "command":
411
+ case "search-forward":
412
+ case "search-backward":
413
+ return { kind: this.inputMode, text: this.#pendingInput };
414
+ default:
415
+ return undefined;
416
+ }
417
+ }
418
+
419
+ rollbackPendingInsert(): void {
420
+ if (this.#pendingChange) {
421
+ this.buffer.restore(this.#pendingChange.before);
422
+ this.#pendingChange = null;
423
+ }
424
+ this.inputMode = "normal";
425
+ this.selectionAnchor = null;
426
+ this.#pendingInput = "";
427
+ }
428
+
429
+ setCursor(line: number, col: number): void {
430
+ this.buffer.setCursor({ line, col });
431
+ }
432
+
433
+ async executeTokens(
434
+ tokens: readonly VimKeyToken[],
435
+ lastCommand?: string,
436
+ onStep?: () => Promise<void>,
437
+ ): Promise<void> {
438
+ const previousStepCallback = this.#stepCallback;
439
+ this.#stepCallback = onStep ?? previousStepCallback;
440
+ this.lastCommand = lastCommand;
441
+ this.statusMessage = undefined;
442
+ this.diagnostics = undefined;
443
+
444
+ try {
445
+ for (let index = 0; index < tokens.length; ) {
446
+ switch (this.inputMode) {
447
+ case "insert":
448
+ index = await this.#executeInsert(tokens, index);
449
+ break;
450
+ case "command":
451
+ case "search-forward":
452
+ case "search-backward":
453
+ index = await this.#executePrompt(tokens, index);
454
+ break;
455
+ case "visual":
456
+ case "visual-line":
457
+ index = await this.#executeVisual(tokens, index);
458
+ break;
459
+ default:
460
+ index = await this.#executeNormal(tokens, index);
461
+ break;
462
+ }
463
+ if (this.closed) {
464
+ break;
465
+ }
466
+ this.#ensureCursorVisible();
467
+ await this.#stepCallback?.();
468
+ }
469
+ } finally {
470
+ this.#stepCallback = previousStepCallback;
471
+ }
472
+ }
473
+
474
+ async close(force: boolean): Promise<void> {
475
+ if (this.buffer.modified && !force) {
476
+ throw new VimError("Unsaved changes; use force to discard");
477
+ }
478
+ this.closed = true;
479
+ this.statusMessage = `Closed ${this.buffer.displayPath}`;
480
+ }
481
+
482
+ #ensureCursorVisible(): void {
483
+ const line = this.buffer.cursor.line + 1;
484
+ if (line < this.viewportStart) {
485
+ this.viewportStart = line;
486
+ return;
487
+ }
488
+ const viewportEnd = this.viewportStart + DEFAULT_VIEWPORT_HEIGHT - 1;
489
+ if (line > viewportEnd) {
490
+ this.viewportStart = Math.max(1, line - DEFAULT_VIEWPORT_HEIGHT + 1);
491
+ }
492
+ }
493
+
494
+ centerViewportOnCursor(size = DEFAULT_VIEWPORT_HEIGHT): void {
495
+ const lineCount = Math.max(this.buffer.lineCount(), 1);
496
+ const clampedSize = Math.max(1, Math.min(size, lineCount));
497
+ const maxStart = Math.max(1, lineCount - clampedSize + 1);
498
+ this.viewportStart = Math.max(1, Math.min(this.buffer.cursor.line + 1 - Math.floor(clampedSize / 2), maxStart));
499
+ }
500
+
501
+ #clearSelection(): void {
502
+ if (this.selectionAnchor && (this.inputMode === "visual" || this.inputMode === "visual-line")) {
503
+ this.lastVisual = {
504
+ anchor: clonePosition(this.selectionAnchor),
505
+ cursor: clonePosition(this.buffer.cursor),
506
+ mode: this.inputMode,
507
+ };
508
+ }
509
+ this.selectionAnchor = null;
510
+ if (this.inputMode === "visual" || this.inputMode === "visual-line") {
511
+ this.inputMode = "normal";
512
+ }
513
+ }
514
+
515
+ async #ensureEditable(): Promise<void> {
516
+ await this.#callbacks.beforeMutate(this.buffer);
517
+ this.diagnostics = undefined;
518
+ this.statusMessage = undefined;
519
+ }
520
+
521
+ #pushUndo(entry: VimUndoEntry, changeTokens?: readonly string[]): void {
522
+ if (snapshotEqual(entry.before, entry.after)) {
523
+ return;
524
+ }
525
+ this.#undoStack.push(entry);
526
+ this.#redoStack = [];
527
+ if (changeTokens && changeTokens.length > 0) {
528
+ this.#lastChangeTokens = [...changeTokens];
529
+ }
530
+ }
531
+
532
+ #beginPendingChange(prefixTokens: readonly string[], moveCursorLeftOnEscape: boolean): void {
533
+ this.#pendingChange = {
534
+ before: this.buffer.createSnapshot(),
535
+ tokens: [...prefixTokens],
536
+ moveCursorLeftOnEscape,
537
+ inserted: false,
538
+ };
539
+ }
540
+
541
+ #markPendingInserted(): void {
542
+ if (this.#pendingChange) {
543
+ this.#pendingChange.inserted = true;
544
+ }
545
+ }
546
+
547
+ #commitPendingChange(): void {
548
+ if (!this.#pendingChange) {
549
+ return;
550
+ }
551
+ const entry: VimUndoEntry = {
552
+ before: this.#pendingChange.before,
553
+ after: this.buffer.createSnapshot(),
554
+ };
555
+ this.#pushUndo(entry, this.#pendingChange.tokens);
556
+ this.#pendingChange = null;
557
+ }
558
+
559
+ async #applyAtomicChange(tokens: readonly string[], mutator: () => void): Promise<void> {
560
+ await this.#ensureEditable();
561
+ const before = this.buffer.createSnapshot();
562
+ mutator();
563
+ this.buffer.modified = true;
564
+ this.#pushUndo({ before, after: this.buffer.createSnapshot() }, tokens);
565
+ }
566
+
567
+ async #startInsertChange(
568
+ tokens: readonly string[],
569
+ mutator?: () => void,
570
+ moveCursorLeftOnEscape = true,
571
+ ): Promise<void> {
572
+ await this.#ensureEditable();
573
+ this.#beginPendingChange(tokens, moveCursorLeftOnEscape);
574
+ mutator?.();
575
+ this.buffer.modified = true;
576
+ this.inputMode = "insert";
577
+ }
578
+
579
+ async #executePrompt(tokens: readonly VimKeyToken[], index: number): Promise<number> {
580
+ const token = tokens[index]!;
581
+ if (token.value === "Esc") {
582
+ this.#pendingInput = "";
583
+ this.inputMode = "normal";
584
+ return index + 1;
585
+ }
586
+ if (token.value === "BS") {
587
+ this.#pendingInput = this.#pendingInput.slice(0, -1);
588
+ return index + 1;
589
+ }
590
+ if (token.value !== "CR") {
591
+ this.#pendingInput += token.value === "Tab" ? "\t" : token.value;
592
+ return index + 1;
593
+ }
594
+
595
+ const input = this.#pendingInput;
596
+ this.#pendingInput = "";
597
+ const mode = this.inputMode;
598
+ this.inputMode = "normal";
599
+ if (mode === "command") {
600
+ await this.#executeEx(input);
601
+ } else {
602
+ await this.#runSearch(input, mode === "search-forward" ? 1 : -1, true);
603
+ }
604
+ return index + 1;
605
+ }
606
+
607
+ #exitInsertMode(): void {
608
+ if (this.#pendingChange) {
609
+ this.#pendingChange.tokens.push("Esc");
610
+ if (this.#pendingChange.moveCursorLeftOnEscape && this.#pendingChange.inserted && this.buffer.cursor.col > 0) {
611
+ this.buffer.setCursor({ line: this.buffer.cursor.line, col: this.buffer.cursor.col - 1 });
612
+ }
613
+ }
614
+ this.inputMode = "normal";
615
+ this.#commitPendingChange();
616
+ }
617
+
618
+ async applyLiteralInsert(text: string, exitInsertMode: boolean): Promise<void> {
619
+ if (this.inputMode !== "insert" || !this.#pendingChange) {
620
+ throw new VimError("Insert payload requires INSERT mode.");
621
+ }
622
+
623
+ if (text.length > 0) {
624
+ const offset = this.buffer.currentOffset();
625
+ this.buffer.replaceOffsets(offset, offset, text, offset + text.length);
626
+ this.buffer.modified = true;
627
+ if (text.includes("\n")) {
628
+ this.buffer.trailingNewline = this.buffer.trailingNewline || text.endsWith("\n");
629
+ }
630
+ this.#pendingChange.tokens.push(...literalTextToReplayTokens(text));
631
+ this.#markPendingInserted();
632
+ }
633
+
634
+ if (exitInsertMode) {
635
+ this.#exitInsertMode();
636
+ }
637
+ }
638
+
639
+ async #executeInsert(tokens: readonly VimKeyToken[], index: number): Promise<number> {
640
+ const token = tokens[index]!;
641
+ if (token.value === "Esc") {
642
+ this.#exitInsertMode();
643
+ return index + 1;
644
+ }
645
+ if (token.value === "CR") {
646
+ const offset = this.buffer.currentOffset();
647
+ this.buffer.replaceOffsets(offset, offset, "\n", offset + 1);
648
+ this.buffer.modified = true;
649
+ this.buffer.trailingNewline = true;
650
+ this.#pendingChange?.tokens.push(token.value);
651
+ this.#markPendingInserted();
652
+ return index + 1;
653
+ }
654
+ if (token.value === "BS") {
655
+ const offset = this.buffer.currentOffset();
656
+ if (offset > 0) {
657
+ this.buffer.deleteOffsets(offset - 1, offset);
658
+ this.buffer.modified = true;
659
+ this.#pendingChange?.tokens.push(token.value);
660
+ this.#markPendingInserted();
661
+ }
662
+ return index + 1;
663
+ }
664
+ if (token.value === "Tab") {
665
+ const offset = this.buffer.currentOffset();
666
+ this.buffer.replaceOffsets(offset, offset, "\t", offset + 1);
667
+ this.buffer.modified = true;
668
+ this.#pendingChange?.tokens.push(token.value);
669
+ this.#markPendingInserted();
670
+ return index + 1;
671
+ }
672
+ if (token.value === "C-w") {
673
+ const offset = this.buffer.currentOffset();
674
+ const text = this.buffer.getText();
675
+ let start = previousWordStart(text, offset, false);
676
+ if (start === offset && start > 0) {
677
+ start -= 1;
678
+ }
679
+ this.buffer.deleteOffsets(start, offset);
680
+ this.buffer.modified = true;
681
+ this.#pendingChange?.tokens.push(token.value);
682
+ this.#markPendingInserted();
683
+ return index + 1;
684
+ }
685
+ if (token.value === "C-u") {
686
+ const offset = this.buffer.currentOffset();
687
+ const lineStart = this.buffer.positionToOffset({ line: this.buffer.cursor.line, col: 0 });
688
+ if (offset > lineStart) {
689
+ this.buffer.deleteOffsets(lineStart, offset);
690
+ this.buffer.modified = true;
691
+ this.#pendingChange?.tokens.push(token.value);
692
+ this.#markPendingInserted();
693
+ }
694
+ return index + 1;
695
+ }
696
+ if (token.value === "C-o") {
697
+ // Execute one normal-mode command, then return to insert
698
+ const nextToken = tokens[index + 1];
699
+ if (!nextToken) {
700
+ return index + 1;
701
+ }
702
+ const savedMode = this.inputMode;
703
+ this.inputMode = "normal";
704
+ const nextIdx = await this.#executeNormal(tokens, index + 1);
705
+ this.inputMode = savedMode;
706
+ return nextIdx;
707
+ }
708
+
709
+ const insertText = token.value;
710
+ const offset = this.buffer.currentOffset();
711
+ this.buffer.replaceOffsets(offset, offset, insertText, offset + insertText.length);
712
+ this.buffer.modified = true;
713
+ this.#pendingChange?.tokens.push(token.value);
714
+ this.#markPendingInserted();
715
+ return index + 1;
716
+ }
717
+
718
+ async #executeVisual(tokens: readonly VimKeyToken[], index: number): Promise<number> {
719
+ const token = tokens[index]!;
720
+ if (token.value === "Esc") {
721
+ this.#clearSelection();
722
+ return index + 1;
723
+ }
724
+ if (token.value === "v") {
725
+ if (this.inputMode === "visual") {
726
+ this.#clearSelection();
727
+ }
728
+ return index + 1;
729
+ }
730
+ if (token.value === "V") {
731
+ this.inputMode = this.inputMode === "visual-line" ? "visual" : "visual-line";
732
+ return index + 1;
733
+ }
734
+ if (token.value === "o") {
735
+ if (this.selectionAnchor) {
736
+ const tmp = clonePosition(this.buffer.cursor);
737
+ this.buffer.setCursor(this.selectionAnchor);
738
+ this.selectionAnchor = tmp;
739
+ }
740
+ return index + 1;
741
+ }
742
+ if (token.value === "J") {
743
+ const visual = expandVisualOffsets(
744
+ this.buffer,
745
+ this.selectionAnchor ?? this.buffer.cursor,
746
+ this.inputMode === "visual-line",
747
+ );
748
+ const startLine = this.buffer.offsetToPosition(visual.start).line;
749
+ const endLine = this.buffer.offsetToPosition(Math.max(visual.start, visual.end - 1)).line;
750
+ await this.#applyAtomicChange(["J"], () => {
751
+ this.buffer.joinLines(startLine, endLine - startLine);
752
+ });
753
+ this.#clearSelection();
754
+ return index + 1;
755
+ }
756
+ if (token.value === "u" || token.value === "U") {
757
+ const visual = expandVisualOffsets(
758
+ this.buffer,
759
+ this.selectionAnchor ?? this.buffer.cursor,
760
+ this.inputMode === "visual-line",
761
+ );
762
+ await this.#applyAtomicChange([token.value], () => {
763
+ const original = this.buffer.getText().slice(visual.start, visual.end);
764
+ const transformed = token.value === "U" ? original.toUpperCase() : original.toLowerCase();
765
+ this.buffer.replaceOffsets(visual.start, visual.end, transformed, visual.start);
766
+ });
767
+ this.#clearSelection();
768
+ return index + 1;
769
+ }
770
+ if (token.value === "p" || token.value === "P") {
771
+ const visual = expandVisualOffsets(
772
+ this.buffer,
773
+ this.selectionAnchor ?? this.buffer.cursor,
774
+ this.inputMode === "visual-line",
775
+ );
776
+ await this.#applyAtomicChange([token.value], () => {
777
+ const removed = this.buffer.getText().slice(visual.start, visual.end);
778
+ const pasteText = this.register.text;
779
+ this.buffer.replaceOffsets(visual.start, visual.end, pasteText, visual.start + pasteText.length);
780
+ this.register = { kind: visual.linewise ? "line" : "char", text: removed };
781
+ });
782
+ this.#clearSelection();
783
+ return index + 1;
784
+ }
785
+ if (token.value === "g") {
786
+ const next = tokens[index + 1];
787
+ if (!next) {
788
+ throw new VimError("g requires a second key", token);
789
+ }
790
+ if (next.value === "J") {
791
+ const visual = expandVisualOffsets(
792
+ this.buffer,
793
+ this.selectionAnchor ?? this.buffer.cursor,
794
+ this.inputMode === "visual-line",
795
+ );
796
+ const startLine = this.buffer.offsetToPosition(visual.start).line;
797
+ const endLine = this.buffer.offsetToPosition(Math.max(visual.start, visual.end - 1)).line;
798
+ await this.#applyAtomicChange(["g", "J"], () => {
799
+ const start = this.buffer.clampLine(startLine);
800
+ const end = this.buffer.clampLine(endLine);
801
+ if (start < end) {
802
+ const joined = this.buffer.lines.slice(start, end + 1).join("");
803
+ this.buffer.lines.splice(start, end - start + 1, joined);
804
+ this.buffer.setCursor({ line: start, col: Math.max(0, joined.length - 1) });
805
+ }
806
+ });
807
+ this.#clearSelection();
808
+ return index + 2;
809
+ }
810
+ if (next.value === "u" || next.value === "U" || next.value === "~") {
811
+ const visual = expandVisualOffsets(
812
+ this.buffer,
813
+ this.selectionAnchor ?? this.buffer.cursor,
814
+ this.inputMode === "visual-line",
815
+ );
816
+ await this.#applyAtomicChange(["g", next.value], () => {
817
+ const original = this.buffer.getText().slice(visual.start, visual.end);
818
+ const transformed =
819
+ next.value === "u"
820
+ ? original.toLowerCase()
821
+ : next.value === "U"
822
+ ? original.toUpperCase()
823
+ : toggleCase(original);
824
+ this.buffer.replaceOffsets(visual.start, visual.end, transformed, visual.start);
825
+ });
826
+ this.#clearSelection();
827
+ return index + 2;
828
+ }
829
+ throw new VimError(`Unsupported g command: g${next.display}`, next);
830
+ }
831
+
832
+ const { count, hasCount, nextIndex } = this.#readCount(tokens, index);
833
+ const opToken = tokens[nextIndex];
834
+ if (!opToken) {
835
+ return nextIndex;
836
+ }
837
+
838
+ switch (opToken.value) {
839
+ case "d":
840
+ case "x":
841
+ case "X":
842
+ case "D":
843
+ case "y":
844
+ case "c":
845
+ case "s":
846
+ case "S":
847
+ case "C":
848
+ case ">":
849
+ case "<":
850
+ case "~": {
851
+ const visual = expandVisualOffsets(
852
+ this.buffer,
853
+ this.selectionAnchor ?? this.buffer.cursor,
854
+ this.inputMode === "visual-line",
855
+ );
856
+ const consumeExtraIndent =
857
+ (opToken.value === ">" || opToken.value === "<") && tokens[nextIndex + 1]?.value === opToken.value;
858
+ const operatorValue =
859
+ opToken.value === "x" || opToken.value === "X" || opToken.value === "D"
860
+ ? "d"
861
+ : opToken.value === "s" || opToken.value === "S" || opToken.value === "C"
862
+ ? "c"
863
+ : opToken.value;
864
+ const visualTokens = consumeExtraIndent ? [opToken.value, opToken.value] : [opToken.value];
865
+ await this.#applyVisualOperator(operatorValue, visual, count, visualTokens);
866
+ return nextIndex + visualTokens.length;
867
+ }
868
+ case "r": {
869
+ const replacement = tokens[nextIndex + 1];
870
+ if (replacement?.value.length !== 1) {
871
+ throw new VimError("Visual replace requires a literal character", opToken);
872
+ }
873
+ const visual = expandVisualOffsets(
874
+ this.buffer,
875
+ this.selectionAnchor ?? this.buffer.cursor,
876
+ this.inputMode === "visual-line",
877
+ );
878
+ await this.#applyAtomicChange(["r", replacement.value], () => {
879
+ const original = this.buffer.getText().slice(visual.start, visual.end);
880
+ let replaced = "";
881
+ for (const char of original) {
882
+ replaced += char === "\n" ? "\n" : replacement.value;
883
+ }
884
+ this.buffer.replaceOffsets(visual.start, visual.end, replaced, visual.start);
885
+ });
886
+ this.#clearSelection();
887
+ return nextIndex + 2;
888
+ }
889
+ default:
890
+ break;
891
+ }
892
+
893
+ const motion = this.#resolveMotion(tokens, nextIndex, count, hasCount);
894
+ this.buffer.setCursor(motion.target);
895
+ return motion.nextIndex;
896
+ }
897
+
898
+ async #applyVisualOperator(
899
+ operator: string,
900
+ visual: { start: number; end: number; linewise: boolean },
901
+ count: number,
902
+ tokens: readonly string[],
903
+ ): Promise<void> {
904
+ switch (operator) {
905
+ case "y": {
906
+ this.register = {
907
+ kind: visual.linewise ? "line" : "char",
908
+ text: this.buffer.getText().slice(visual.start, visual.end),
909
+ };
910
+ this.#clearSelection();
911
+ this.statusMessage = `Yanked ${count} selection${count === 1 ? "" : "s"}`;
912
+ return;
913
+ }
914
+ case "d": {
915
+ await this.#applyAtomicChange(tokens, () => {
916
+ this.register = {
917
+ kind: visual.linewise ? "line" : "char",
918
+ text: this.buffer.getText().slice(visual.start, visual.end),
919
+ };
920
+ this.buffer.deleteOffsets(visual.start, visual.end);
921
+ });
922
+ this.#clearSelection();
923
+ return;
924
+ }
925
+ case "c": {
926
+ await this.#startInsertChange(tokens, () => {
927
+ this.register = {
928
+ kind: visual.linewise ? "line" : "char",
929
+ text: this.buffer.getText().slice(visual.start, visual.end),
930
+ };
931
+ this.buffer.deleteOffsets(visual.start, visual.end);
932
+ });
933
+ this.#clearSelection();
934
+ return;
935
+ }
936
+ case ">":
937
+ case "<": {
938
+ const startLine = this.buffer.offsetToPosition(visual.start).line;
939
+ const endLine = this.buffer.offsetToPosition(Math.max(visual.start, visual.end - 1)).line;
940
+ await this.#applyAtomicChange(tokens, () => {
941
+ this.buffer.indentLines(
942
+ startLine,
943
+ endLine,
944
+ detectIndentUnit(this.buffer.lines),
945
+ operator === ">" ? 1 : -1,
946
+ );
947
+ });
948
+ this.#clearSelection();
949
+ return;
950
+ }
951
+ case "~": {
952
+ await this.#applyAtomicChange(tokens, () => {
953
+ const original = this.buffer.getText().slice(visual.start, visual.end);
954
+ this.buffer.replaceOffsets(visual.start, visual.end, toggleCase(original), visual.start);
955
+ });
956
+ this.#clearSelection();
957
+ return;
958
+ }
959
+ default:
960
+ throw new VimError(`Unsupported visual operator: ${operator}`);
961
+ }
962
+ }
963
+
964
+ async #executeNormal(tokens: readonly VimKeyToken[], index: number): Promise<number> {
965
+ const { count, hasCount, nextIndex } = this.#readCount(tokens, index);
966
+ const token = tokens[nextIndex];
967
+ if (!token) {
968
+ return nextIndex;
969
+ }
970
+
971
+ switch (token.value) {
972
+ case "h":
973
+ this.buffer.setCursor({ line: this.buffer.cursor.line, col: this.buffer.cursor.col - count });
974
+ return nextIndex + 1;
975
+ case "j":
976
+ this.buffer.setCursor({ line: this.buffer.cursor.line + count, col: this.buffer.cursor.col });
977
+ return nextIndex + 1;
978
+ case "k":
979
+ this.buffer.setCursor({ line: this.buffer.cursor.line - count, col: this.buffer.cursor.col });
980
+ return nextIndex + 1;
981
+ case "l":
982
+ case " ":
983
+ this.buffer.setCursor({ line: this.buffer.cursor.line, col: this.buffer.cursor.col + count });
984
+ return nextIndex + 1;
985
+ case "w":
986
+ case "W":
987
+ case "b":
988
+ case "B":
989
+ case "e":
990
+ case "E":
991
+ case "0":
992
+ case "$":
993
+ case "^":
994
+ case "|":
995
+ case ";":
996
+ case ",":
997
+ case "G":
998
+ case "f":
999
+ case "F":
1000
+ case "t":
1001
+ case "T":
1002
+ case "{":
1003
+ case "}":
1004
+ case "%":
1005
+ case "H":
1006
+ case "M":
1007
+ case "+":
1008
+ case "-":
1009
+ case "_":
1010
+ case "L": {
1011
+ const motion = this.#resolveMotion(tokens, nextIndex, count, hasCount);
1012
+ this.buffer.setCursor(motion.target);
1013
+ return motion.nextIndex;
1014
+ }
1015
+ case "*":
1016
+ case "#": {
1017
+ const text = this.buffer.getText();
1018
+ const offset = this.buffer.currentOffset();
1019
+ const cat = wordCategory(text[offset] ?? "", false);
1020
+ if (cat === "space") {
1021
+ throw new VimError("No word under cursor", token);
1022
+ }
1023
+ let start = offset;
1024
+ while (start > 0 && wordCategory(text[start - 1] ?? "", false) === cat) start -= 1;
1025
+ let end = offset;
1026
+ while (end < text.length && wordCategory(text[end] ?? "", false) === cat) end += 1;
1027
+ const word = text.slice(start, end);
1028
+ const pattern = `\\b${escapeRegex(word)}\\b`;
1029
+ const direction = token.value === "*" ? 1 : -1;
1030
+ for (let step = 0; step < count; step += 1) {
1031
+ await this.#runSearch(pattern, direction, true);
1032
+ }
1033
+ return nextIndex + 1;
1034
+ }
1035
+ case "n":
1036
+ await this.#repeatSearch(this.lastSearch?.direction ?? 1, count);
1037
+ return nextIndex + 1;
1038
+ case "N":
1039
+ await this.#repeatSearch(((this.lastSearch?.direction ?? 1) * -1) as 1 | -1, count);
1040
+ return nextIndex + 1;
1041
+ case "/":
1042
+ this.inputMode = "search-forward";
1043
+ this.#pendingInput = "";
1044
+ return nextIndex + 1;
1045
+ case "?":
1046
+ this.inputMode = "search-backward";
1047
+ this.#pendingInput = "";
1048
+ return nextIndex + 1;
1049
+ case ":":
1050
+ this.inputMode = "command";
1051
+ this.#pendingInput = "";
1052
+ return nextIndex + 1;
1053
+ case "v":
1054
+ this.inputMode = "visual";
1055
+ this.selectionAnchor = clonePosition(this.buffer.cursor);
1056
+ return nextIndex + 1;
1057
+ case "V":
1058
+ this.inputMode = "visual-line";
1059
+ this.selectionAnchor = clonePosition(this.buffer.cursor);
1060
+ return nextIndex + 1;
1061
+ case "i":
1062
+ // When count > 1 (e.g. `2i`), interpret as `2Gi` — go to line N then insert.
1063
+ // Models confuse `Ni` with `NGi`; bare `i` with a high count is almost never intended.
1064
+ if (hasCount) {
1065
+ this.buffer.setCursor({ line: Math.min(count, this.buffer.lineCount()) - 1, col: 0 });
1066
+ }
1067
+ await this.#startInsertChange(["i"]);
1068
+ return nextIndex + 1;
1069
+ case "a":
1070
+ this.buffer.setCursor({ line: this.buffer.cursor.line, col: this.buffer.cursor.col + 1 });
1071
+ await this.#startInsertChange(["a"]);
1072
+ return nextIndex + 1;
1073
+ case "I":
1074
+ this.buffer.setCursor({
1075
+ line: this.buffer.cursor.line,
1076
+ col: this.buffer.firstNonBlank(this.buffer.cursor.line),
1077
+ });
1078
+ await this.#startInsertChange(["I"]);
1079
+ return nextIndex + 1;
1080
+ case "A":
1081
+ this.buffer.setCursor({
1082
+ line: this.buffer.cursor.line,
1083
+ col: this.buffer.getLine(this.buffer.cursor.line).length,
1084
+ });
1085
+ await this.#startInsertChange(["A"]);
1086
+ return nextIndex + 1;
1087
+ case "o":
1088
+ // When count > 1 (e.g. `13o`), interpret as `13Go` — go to line N then open below.
1089
+ // Models confuse `No` with `NGo`; bare `o` with a high count is almost never intended.
1090
+ if (hasCount) {
1091
+ this.buffer.setCursor({ line: Math.min(count, this.buffer.lineCount()) - 1, col: 0 });
1092
+ }
1093
+ await this.#startInsertChange(["o"], () => {
1094
+ const line = this.buffer.cursor.line + 1;
1095
+ this.buffer.insertLines(line, [""]);
1096
+ });
1097
+ return nextIndex + 1;
1098
+ case "O":
1099
+ if (hasCount) {
1100
+ this.buffer.setCursor({ line: Math.min(count, this.buffer.lineCount()) - 1, col: 0 });
1101
+ }
1102
+ await this.#startInsertChange(["O"], () => {
1103
+ const line = this.buffer.cursor.line;
1104
+ this.buffer.insertLines(line, [""]);
1105
+ });
1106
+ return nextIndex + 1;
1107
+ case "s":
1108
+ await this.#startInsertChange(["s"], () => {
1109
+ const start = this.buffer.currentOffset();
1110
+ this.register = {
1111
+ kind: "char",
1112
+ text: this.buffer.deleteOffsets(start, Math.min(this.buffer.getText().length, start + count)),
1113
+ };
1114
+ });
1115
+ return nextIndex + 1;
1116
+ case "S":
1117
+ await this.#changeWholeLines(count, ["S"]);
1118
+ return nextIndex + 1;
1119
+ case "x":
1120
+ await this.#applyAtomicChange(["x"], () => {
1121
+ const start = this.buffer.currentOffset();
1122
+ this.register = {
1123
+ kind: "char",
1124
+ text: this.buffer.deleteOffsets(start, Math.min(this.buffer.getText().length, start + count)),
1125
+ };
1126
+ });
1127
+ return nextIndex + 1;
1128
+ case "X":
1129
+ await this.#applyAtomicChange(["X"], () => {
1130
+ const end = this.buffer.currentOffset();
1131
+ const start = Math.max(0, end - count);
1132
+ this.register = { kind: "char", text: this.buffer.deleteOffsets(start, end) };
1133
+ });
1134
+ return nextIndex + 1;
1135
+ case "r": {
1136
+ const replacement = tokens[nextIndex + 1];
1137
+ if (replacement?.value.length !== 1) {
1138
+ throw new VimError("r requires a replacement character", token);
1139
+ }
1140
+ await this.#applyAtomicChange(["r", replacement.value], () => {
1141
+ const start = this.buffer.currentOffset();
1142
+ this.buffer.replaceOffsets(
1143
+ start,
1144
+ Math.min(this.buffer.getText().length, start + count),
1145
+ replacement.value.repeat(count),
1146
+ start,
1147
+ );
1148
+ });
1149
+ return nextIndex + 2;
1150
+ }
1151
+ case "~":
1152
+ await this.#applyAtomicChange(["~"], () => {
1153
+ const start = this.buffer.currentOffset();
1154
+ const end = Math.min(this.buffer.getText().length, start + count);
1155
+ const text = this.buffer.getText().slice(start, end);
1156
+ this.buffer.replaceOffsets(start, end, toggleCase(text), end);
1157
+ });
1158
+ return nextIndex + 1;
1159
+ case "J":
1160
+ await this.#applyAtomicChange(["J"], () => {
1161
+ this.buffer.joinLines(this.buffer.cursor.line, count);
1162
+ });
1163
+ return nextIndex + 1;
1164
+ case "p":
1165
+ case "P":
1166
+ await this.#applyAtomicChange([token.value], () => {
1167
+ this.#paste(token.value === "p", count);
1168
+ });
1169
+ return nextIndex + 1;
1170
+ case "u":
1171
+ await this.#undo(count);
1172
+ return nextIndex + 1;
1173
+ case "C-r":
1174
+ await this.#redo(count);
1175
+ return nextIndex + 1;
1176
+ case ".":
1177
+ await this.#repeatLastChange(count, token);
1178
+ return nextIndex + 1;
1179
+ case "d":
1180
+ case "c":
1181
+ case "y":
1182
+ case ">":
1183
+ case "<":
1184
+ return this.#executeOperator(tokens, nextIndex, count, hasCount, token.value);
1185
+ case "D":
1186
+ await this.#applyAtomicChange(["D"], () => {
1187
+ const start = this.buffer.currentOffset();
1188
+ const line = this.buffer.getLine(this.buffer.cursor.line);
1189
+ const end = start + (line.length - this.buffer.cursor.col);
1190
+ this.register = { kind: "char", text: this.buffer.deleteOffsets(start, end) };
1191
+ });
1192
+ return nextIndex + 1;
1193
+ case "C":
1194
+ await this.#startInsertChange(["C"], () => {
1195
+ const start = this.buffer.currentOffset();
1196
+ const line = this.buffer.getLine(this.buffer.cursor.line);
1197
+ const end = start + (line.length - this.buffer.cursor.col);
1198
+ this.register = { kind: "char", text: this.buffer.deleteOffsets(start, end) };
1199
+ });
1200
+ return nextIndex + 1;
1201
+ case "z": {
1202
+ const zTarget = tokens[nextIndex + 1];
1203
+ if (!zTarget) {
1204
+ throw new VimError("z requires a second key", token);
1205
+ }
1206
+ if (zTarget.value === "z") {
1207
+ this.centerViewportOnCursor();
1208
+ } else if (zTarget.value === "t" || zTarget.value === "CR") {
1209
+ this.viewportStart = this.buffer.cursor.line + 1;
1210
+ this.buffer.setCursor({
1211
+ line: this.buffer.cursor.line,
1212
+ col: this.buffer.firstNonBlank(this.buffer.cursor.line),
1213
+ });
1214
+ } else if (zTarget.value === "b" || zTarget.value === "-") {
1215
+ this.viewportStart = Math.max(1, this.buffer.cursor.line + 1 - (DEFAULT_VIEWPORT_HEIGHT - 1));
1216
+ this.buffer.setCursor({
1217
+ line: this.buffer.cursor.line,
1218
+ col: this.buffer.firstNonBlank(this.buffer.cursor.line),
1219
+ });
1220
+ } else if (zTarget.value === ".") {
1221
+ this.centerViewportOnCursor();
1222
+ this.buffer.setCursor({
1223
+ line: this.buffer.cursor.line,
1224
+ col: this.buffer.firstNonBlank(this.buffer.cursor.line),
1225
+ });
1226
+ } else if (NOOP_Z_COMMANDS.has(zTarget.value)) {
1227
+ this.statusMessage = `Ignored z${zTarget.display} (folds unsupported)`;
1228
+ } else {
1229
+ throw new VimError(`Unsupported z command: z${zTarget.display}`, zTarget);
1230
+ }
1231
+ return nextIndex + 2;
1232
+ }
1233
+ case "C-f":
1234
+ this.buffer.setCursor({
1235
+ line: this.buffer.cursor.line + Math.max(1, (DEFAULT_VIEWPORT_HEIGHT - 2) * count),
1236
+ col: this.buffer.cursor.col,
1237
+ });
1238
+ return nextIndex + 1;
1239
+ case "C-b":
1240
+ this.buffer.setCursor({
1241
+ line: this.buffer.cursor.line - Math.max(1, (DEFAULT_VIEWPORT_HEIGHT - 2) * count),
1242
+ col: this.buffer.cursor.col,
1243
+ });
1244
+ return nextIndex + 1;
1245
+ case "C-d":
1246
+ this.buffer.setCursor({
1247
+ line: this.buffer.cursor.line + Math.max(1, Math.floor(DEFAULT_VIEWPORT_HEIGHT / 2) * count),
1248
+ col: this.buffer.cursor.col,
1249
+ });
1250
+ return nextIndex + 1;
1251
+ case "C-u":
1252
+ this.buffer.setCursor({
1253
+ line: this.buffer.cursor.line - Math.max(1, Math.floor(DEFAULT_VIEWPORT_HEIGHT / 2) * count),
1254
+ col: this.buffer.cursor.col,
1255
+ });
1256
+ return nextIndex + 1;
1257
+ case "Esc":
1258
+ return nextIndex + 1;
1259
+ case "Y": {
1260
+ const start = this.buffer.cursor.line;
1261
+ const end = this.buffer.clampLine(start + count - 1);
1262
+ this.register = { kind: "line", text: this.buffer.lines.slice(start, end + 1).join("\n") };
1263
+ this.statusMessage = `Yanked ${end - start + 1} line${end === start ? "" : "s"}`;
1264
+ return nextIndex + 1;
1265
+ }
1266
+ case "R":
1267
+ await this.#startInsertChange(["R"], undefined, false);
1268
+ return nextIndex + 1;
1269
+ case "g": {
1270
+ const gNext = tokens[nextIndex + 1];
1271
+ if (!gNext) {
1272
+ throw new VimError("g requires a second key", token);
1273
+ }
1274
+ if (gNext.value === "g") {
1275
+ this.buffer.setCursor({ line: hasCount ? Math.max(0, count - 1) : 0, col: 0 });
1276
+ return nextIndex + 2;
1277
+ }
1278
+ if (gNext.value === "v") {
1279
+ if (this.lastVisual) {
1280
+ this.selectionAnchor = clonePosition(this.lastVisual.anchor);
1281
+ this.buffer.setCursor(this.lastVisual.cursor);
1282
+ this.inputMode = this.lastVisual.mode;
1283
+ }
1284
+ return nextIndex + 2;
1285
+ }
1286
+ if (gNext.value === "*" || gNext.value === "#") {
1287
+ const text = this.buffer.getText();
1288
+ const offset = this.buffer.currentOffset();
1289
+ const cat = wordCategory(text[offset] ?? "", false);
1290
+ if (cat === "space") {
1291
+ throw new VimError("No word under cursor", gNext);
1292
+ }
1293
+ let start = offset;
1294
+ while (start > 0 && wordCategory(text[start - 1] ?? "", false) === cat) start -= 1;
1295
+ let end = offset;
1296
+ while (end < text.length && wordCategory(text[end] ?? "", false) === cat) end += 1;
1297
+ const word = text.slice(start, end);
1298
+ const direction = gNext.value === "*" ? 1 : -1;
1299
+ for (let step = 0; step < count; step += 1) {
1300
+ await this.#runSearch(escapeRegex(word), direction, true);
1301
+ }
1302
+ return nextIndex + 2;
1303
+ }
1304
+ if (gNext.value === "U" || gNext.value === "u") {
1305
+ const caseOp = gNext.value;
1306
+ const {
1307
+ count: motionCount,
1308
+ hasCount: hasMotionCount,
1309
+ nextIndex: motionStart,
1310
+ } = this.#readCount(tokens, nextIndex + 2);
1311
+ const motionToken = tokens[motionStart];
1312
+ if (!motionToken) {
1313
+ throw new VimError(`g${caseOp} requires a motion`, gNext);
1314
+ }
1315
+ if ((motionToken.value === "U" && caseOp === "U") || (motionToken.value === "u" && caseOp === "u")) {
1316
+ const effectiveCount = hasMotionCount ? count * motionCount : count;
1317
+ await this.#applyAtomicChange(["g", caseOp, motionToken.value], () => {
1318
+ const start = this.buffer.cursor.line;
1319
+ const end = this.buffer.clampLine(start + effectiveCount - 1);
1320
+ for (let line = start; line <= end; line++) {
1321
+ const content = this.buffer.getLine(line);
1322
+ this.buffer.replaceLine(line, caseOp === "U" ? content.toUpperCase() : content.toLowerCase());
1323
+ }
1324
+ });
1325
+ return motionStart + 1;
1326
+ }
1327
+ const effectiveCount = hasMotionCount ? count * motionCount : count;
1328
+ const motion = this.#resolveMotion(tokens, motionStart, effectiveCount, hasCount || hasMotionCount);
1329
+ const range = this.#resolveMotionRange(motion);
1330
+ await this.#applyAtomicChange(
1331
+ tokens.slice(nextIndex, motion.nextIndex).map(tokenEntry => tokenEntry.value),
1332
+ () => {
1333
+ const text = this.buffer.getText();
1334
+ const slice = text.slice(range.start, range.end);
1335
+ const transformed = caseOp === "U" ? slice.toUpperCase() : slice.toLowerCase();
1336
+ this.buffer.replaceOffsets(range.start, range.end, transformed, range.start);
1337
+ },
1338
+ );
1339
+ return motion.nextIndex;
1340
+ }
1341
+ if (gNext.value === "~") {
1342
+ const {
1343
+ count: motionCount,
1344
+ hasCount: hasMotionCount,
1345
+ nextIndex: motionStart,
1346
+ } = this.#readCount(tokens, nextIndex + 2);
1347
+ const motionToken = tokens[motionStart];
1348
+ if (!motionToken) {
1349
+ throw new VimError("g~ requires a motion", gNext);
1350
+ }
1351
+ if (motionToken.value === "~") {
1352
+ const effectiveCount = hasMotionCount ? count * motionCount : count;
1353
+ await this.#applyAtomicChange(["g", "~", motionToken.value], () => {
1354
+ const start = this.buffer.cursor.line;
1355
+ const end = this.buffer.clampLine(start + effectiveCount - 1);
1356
+ for (let line = start; line <= end; line += 1) {
1357
+ this.buffer.replaceLine(line, toggleCase(this.buffer.getLine(line)));
1358
+ }
1359
+ });
1360
+ return motionStart + 1;
1361
+ }
1362
+ const effectiveCount = hasMotionCount ? count * motionCount : count;
1363
+ const motion = this.#resolveMotion(tokens, motionStart, effectiveCount, hasCount || hasMotionCount);
1364
+ const range = this.#resolveMotionRange(motion);
1365
+ await this.#applyAtomicChange(
1366
+ tokens.slice(nextIndex, motion.nextIndex).map(tokenEntry => tokenEntry.value),
1367
+ () => {
1368
+ const text = this.buffer.getText();
1369
+ const slice = text.slice(range.start, range.end);
1370
+ this.buffer.replaceOffsets(range.start, range.end, toggleCase(slice), range.start);
1371
+ },
1372
+ );
1373
+ return motion.nextIndex;
1374
+ }
1375
+ if (gNext.value === "J") {
1376
+ await this.#applyAtomicChange(["g", "J"], () => {
1377
+ const start = this.buffer.clampLine(this.buffer.cursor.line);
1378
+ const end = this.buffer.clampLine(start + Math.max(count, 1));
1379
+ if (start < end) {
1380
+ const joined = this.buffer.lines.slice(start, end + 1).join("");
1381
+ this.buffer.lines.splice(start, end - start + 1, joined);
1382
+ this.buffer.setCursor({ line: start, col: Math.max(0, joined.length - 1) });
1383
+ }
1384
+ });
1385
+ return nextIndex + 2;
1386
+ }
1387
+ throw new VimError(`Unsupported g command: g${gNext.display}`, gNext);
1388
+ }
1389
+ case "Z": {
1390
+ const zNext = tokens[nextIndex + 1];
1391
+ if (!zNext) {
1392
+ throw new VimError("Z requires a second key", token);
1393
+ }
1394
+ if (zNext.value === "Z") {
1395
+ await this.#executeEx("wq");
1396
+ return nextIndex + 2;
1397
+ }
1398
+ if (zNext.value === "Q") {
1399
+ await this.#executeEx("q!");
1400
+ return nextIndex + 2;
1401
+ }
1402
+ throw new VimError(`Unsupported Z command: Z${zNext.display}`, zNext);
1403
+ }
1404
+ default:
1405
+ throw new VimError(`Unsupported command: ${token.display}`, token);
1406
+ }
1407
+ }
1408
+
1409
+ async #repeatLastChange(count: number, token: VimKeyToken): Promise<void> {
1410
+ if (!this.#lastChangeTokens || this.#lastChangeTokens.length === 0) {
1411
+ throw new VimError("No previous change to repeat", token);
1412
+ }
1413
+ for (let index = 0; index < count; index += 1) {
1414
+ await this.executeTokens(replayTokens(this.#lastChangeTokens), ".");
1415
+ }
1416
+ }
1417
+
1418
+ async #undo(count: number): Promise<void> {
1419
+ await this.#ensureEditable();
1420
+ let applied = 0;
1421
+ for (let index = 0; index < count; index += 1) {
1422
+ const entry = this.#undoStack.pop();
1423
+ if (!entry) {
1424
+ break;
1425
+ }
1426
+ this.#redoStack.push(entry);
1427
+ this.buffer.restore(entry.before);
1428
+ applied += 1;
1429
+ }
1430
+ this.inputMode = "normal";
1431
+ this.selectionAnchor = null;
1432
+ this.#pendingChange = null;
1433
+ this.statusMessage = `Undid ${applied} change${applied === 1 ? "" : "s"}`;
1434
+ }
1435
+
1436
+ async #redo(count: number): Promise<void> {
1437
+ await this.#ensureEditable();
1438
+ let applied = 0;
1439
+ for (let index = 0; index < count; index += 1) {
1440
+ const entry = this.#redoStack.pop();
1441
+ if (!entry) {
1442
+ break;
1443
+ }
1444
+ this.#undoStack.push(entry);
1445
+ this.buffer.restore(entry.after);
1446
+ applied += 1;
1447
+ }
1448
+ this.inputMode = "normal";
1449
+ this.selectionAnchor = null;
1450
+ this.#pendingChange = null;
1451
+ this.statusMessage = `Redid ${applied} change${applied === 1 ? "" : "s"}`;
1452
+ }
1453
+
1454
+ async #executeOperator(
1455
+ tokens: readonly VimKeyToken[],
1456
+ operatorIndex: number,
1457
+ operatorCount: number,
1458
+ hasOperatorCount: boolean,
1459
+ operator: string,
1460
+ ): Promise<number> {
1461
+ const { count: motionCount, hasCount: hasMotionCount, nextIndex } = this.#readCount(tokens, operatorIndex + 1);
1462
+ const token = tokens[nextIndex];
1463
+ if (!token) {
1464
+ throw new VimError(`Operator ${operator} requires a motion`, tokens[operatorIndex]);
1465
+ }
1466
+ const hasAnyCount = hasOperatorCount || hasMotionCount;
1467
+ const effectiveCount = hasMotionCount ? operatorCount * motionCount : operatorCount;
1468
+
1469
+ if (token.value === operator) {
1470
+ if (operator === "d") {
1471
+ await this.#applyAtomicChange([operator, operator], () => {
1472
+ const start = this.buffer.cursor.line;
1473
+ const removed = this.buffer.deleteLines(start, start + Math.max(1, effectiveCount) - 1);
1474
+ this.register = { kind: "line", text: removed.join("\n") };
1475
+ });
1476
+ return nextIndex + 1;
1477
+ }
1478
+ if (operator === "y") {
1479
+ const start = this.buffer.cursor.line;
1480
+ const end = this.buffer.clampLine(start + Math.max(1, effectiveCount) - 1);
1481
+ this.register = { kind: "line", text: this.buffer.lines.slice(start, end + 1).join("\n") };
1482
+ this.statusMessage = `Yanked ${end - start + 1} line${end === start ? "" : "s"}`;
1483
+ return nextIndex + 1;
1484
+ }
1485
+ if (operator === "c") {
1486
+ await this.#changeWholeLines(Math.max(1, effectiveCount), [operator, operator]);
1487
+ return nextIndex + 1;
1488
+ }
1489
+ if (operator === ">" || operator === "<") {
1490
+ await this.#applyAtomicChange([operator, operator], () => {
1491
+ this.buffer.indentLines(
1492
+ this.buffer.cursor.line,
1493
+ this.buffer.cursor.line + Math.max(1, effectiveCount) - 1,
1494
+ detectIndentUnit(this.buffer.lines),
1495
+ operator === ">" ? 1 : -1,
1496
+ );
1497
+ });
1498
+ return nextIndex + 1;
1499
+ }
1500
+ }
1501
+
1502
+ if (token.value === "i" || token.value === "a") {
1503
+ const object = tokens[nextIndex + 1];
1504
+ if (!object) {
1505
+ throw new VimError(`Missing text object after ${operator}${token.value}`, token);
1506
+ }
1507
+ const textObject = this.#resolveTextObject(token.value === "i", object.value, object);
1508
+ await this.#applyOperatorToMotion(
1509
+ operator,
1510
+ { nextIndex: nextIndex + 2, target: this.buffer.cursor, range: textObject },
1511
+ [operator, token.value, object.value],
1512
+ );
1513
+ return nextIndex + 2;
1514
+ }
1515
+
1516
+ // In vim, `cw` and `cW` act like `ce` and `cE` (don't include trailing whitespace)
1517
+ const motionToken = tokens[nextIndex];
1518
+ let motion: MotionResult;
1519
+ if (operator === "c" && motionToken && (motionToken.value === "w" || motionToken.value === "W")) {
1520
+ const eMotionValue = motionToken.value === "w" ? "e" : "E";
1521
+ const syntheticTokens: readonly VimKeyToken[] = [
1522
+ ...tokens.slice(0, nextIndex),
1523
+ { ...motionToken, value: eMotionValue },
1524
+ ...tokens.slice(nextIndex + 1),
1525
+ ];
1526
+ motion = this.#resolveMotion(syntheticTokens, nextIndex, effectiveCount, hasAnyCount);
1527
+ } else {
1528
+ motion = this.#resolveMotion(tokens, nextIndex, effectiveCount, hasAnyCount);
1529
+ }
1530
+ await this.#applyOperatorToMotion(
1531
+ operator,
1532
+ motion,
1533
+ tokens.slice(operatorIndex, motion.nextIndex).map(tokenEntry => tokenEntry.value),
1534
+ );
1535
+ return motion.nextIndex;
1536
+ }
1537
+
1538
+ async #applyOperatorToMotion(operator: string, motion: MotionResult, tokens: readonly string[]): Promise<void> {
1539
+ if (operator === "y") {
1540
+ const range = this.#resolveMotionRange(motion);
1541
+ this.register = {
1542
+ kind: range.linewise ? "line" : "char",
1543
+ text: this.buffer.getText().slice(range.start, range.end),
1544
+ };
1545
+ this.statusMessage = `Yanked ${range.linewise ? "line" : "selection"}`;
1546
+ return;
1547
+ }
1548
+
1549
+ if (operator === ">" || operator === "<") {
1550
+ const range = this.#resolveMotionRange(motion);
1551
+ const startLine = this.buffer.offsetToPosition(range.start).line;
1552
+ const endLine = this.buffer.offsetToPosition(Math.max(range.start, range.end - 1)).line;
1553
+ await this.#applyAtomicChange(tokens, () => {
1554
+ this.buffer.indentLines(startLine, endLine, detectIndentUnit(this.buffer.lines), operator === ">" ? 1 : -1);
1555
+ });
1556
+ return;
1557
+ }
1558
+
1559
+ if (operator === "d") {
1560
+ const range = this.#resolveMotionRange(motion);
1561
+ await this.#applyAtomicChange(tokens, () => {
1562
+ this.register = {
1563
+ kind: range.linewise ? "line" : "char",
1564
+ text: this.buffer.getText().slice(range.start, range.end),
1565
+ };
1566
+ this.buffer.deleteOffsets(range.start, range.end);
1567
+ });
1568
+ return;
1569
+ }
1570
+
1571
+ if (operator === "c") {
1572
+ const range = this.#resolveMotionRange(motion);
1573
+ await this.#startInsertChange(tokens, () => {
1574
+ this.register = {
1575
+ kind: range.linewise ? "line" : "char",
1576
+ text: this.buffer.getText().slice(range.start, range.end),
1577
+ };
1578
+ this.buffer.deleteOffsets(range.start, range.end);
1579
+ });
1580
+ return;
1581
+ }
1582
+ }
1583
+
1584
+ async #changeWholeLines(count: number, tokens: readonly string[]): Promise<void> {
1585
+ await this.#startInsertChange(tokens, () => {
1586
+ const start = this.buffer.cursor.line;
1587
+ const end = this.buffer.clampLine(start + count - 1);
1588
+ const removed = this.buffer.lines.slice(start, end + 1);
1589
+ this.register = { kind: "line", text: removed.join("\n") };
1590
+ this.buffer.lines.splice(start, end - start + 1, "");
1591
+ if (this.buffer.lines.length === 0) {
1592
+ this.buffer.lines = [""];
1593
+ }
1594
+ this.buffer.setCursor({ line: Math.min(start, this.buffer.lastLineIndex()), col: 0 });
1595
+ });
1596
+ }
1597
+
1598
+ #resolveMotionRange(motion: MotionResult): { start: number; end: number; linewise: boolean } {
1599
+ if (motion.range) {
1600
+ return {
1601
+ start: motion.range.start,
1602
+ end: motion.range.end,
1603
+ linewise: motion.range.linewise ?? false,
1604
+ };
1605
+ }
1606
+
1607
+ if (motion.linewise) {
1608
+ const startLine = Math.min(this.buffer.cursor.line, motion.target.line);
1609
+ const endLine = Math.max(this.buffer.cursor.line, motion.target.line);
1610
+ const start = this.buffer.positionToOffset({ line: startLine, col: 0 });
1611
+ const end =
1612
+ endLine >= this.buffer.lastLineIndex()
1613
+ ? this.buffer.getText().length
1614
+ : this.buffer.positionToOffset({ line: endLine + 1, col: 0 });
1615
+ return { start, end, linewise: true };
1616
+ }
1617
+
1618
+ const from = this.buffer.positionToOffset(this.buffer.cursor);
1619
+ const to = this.buffer.positionToOffset(motion.target);
1620
+ const normalized = normalizeRange(from, to);
1621
+ return {
1622
+ start: normalized.start,
1623
+ end: normalized.end + (motion.inclusive === false ? 0 : 1),
1624
+ linewise: false,
1625
+ };
1626
+ }
1627
+
1628
+ #resolveMotion(tokens: readonly VimKeyToken[], index: number, count: number, hasCount = true): MotionResult {
1629
+ const token = tokens[index];
1630
+ if (!token) {
1631
+ throw new VimError("Missing motion");
1632
+ }
1633
+
1634
+ const text = this.buffer.getText();
1635
+ switch (token.value) {
1636
+ case "h":
1637
+ return {
1638
+ nextIndex: index + 1,
1639
+ target: { line: this.buffer.cursor.line, col: this.buffer.cursor.col - count },
1640
+ };
1641
+ case "j":
1642
+ return {
1643
+ nextIndex: index + 1,
1644
+ target: { line: this.buffer.cursor.line + count, col: this.buffer.cursor.col },
1645
+ linewise: true,
1646
+ };
1647
+ case "k":
1648
+ return {
1649
+ nextIndex: index + 1,
1650
+ target: { line: this.buffer.cursor.line - count, col: this.buffer.cursor.col },
1651
+ linewise: true,
1652
+ };
1653
+ case "l":
1654
+ case " ":
1655
+ return {
1656
+ nextIndex: index + 1,
1657
+ target: { line: this.buffer.cursor.line, col: this.buffer.cursor.col + count },
1658
+ };
1659
+ case "w":
1660
+ case "W": {
1661
+ let offset = this.buffer.currentOffset();
1662
+ for (let step = 0; step < count; step += 1) {
1663
+ offset = nextWordStart(text, step === 0 ? offset + 1 : offset, token.value === "W");
1664
+ }
1665
+ return { nextIndex: index + 1, target: this.buffer.offsetToPosition(offset), inclusive: false };
1666
+ }
1667
+ case "b":
1668
+ case "B": {
1669
+ let offset = this.buffer.currentOffset();
1670
+ for (let step = 0; step < count; step += 1) {
1671
+ offset = previousWordStart(text, offset, token.value === "B");
1672
+ }
1673
+ return { nextIndex: index + 1, target: this.buffer.offsetToPosition(offset) };
1674
+ }
1675
+ case "e":
1676
+ case "E": {
1677
+ let offset = this.buffer.currentOffset();
1678
+ for (let step = 0; step < count; step += 1) {
1679
+ offset = endOfWord(text, step === 0 ? offset : offset + 1, token.value === "E");
1680
+ }
1681
+ return { nextIndex: index + 1, target: this.buffer.offsetToPosition(offset) };
1682
+ }
1683
+ case "0":
1684
+ return { nextIndex: index + 1, target: { line: this.buffer.cursor.line, col: 0 } };
1685
+ case "^":
1686
+ return {
1687
+ nextIndex: index + 1,
1688
+ target: { line: this.buffer.cursor.line, col: this.buffer.firstNonBlank(this.buffer.cursor.line) },
1689
+ };
1690
+ case "|":
1691
+ return {
1692
+ nextIndex: index + 1,
1693
+ target: { line: this.buffer.cursor.line, col: Math.max(0, count - 1) },
1694
+ };
1695
+ case "$":
1696
+ return {
1697
+ nextIndex: index + 1,
1698
+ target: {
1699
+ line: this.buffer.cursor.line,
1700
+ col: Math.max(0, this.buffer.getLine(this.buffer.cursor.line).length - 1),
1701
+ },
1702
+ };
1703
+ case "+": {
1704
+ const targetLine = this.buffer.clampLine(this.buffer.cursor.line + count);
1705
+ return {
1706
+ nextIndex: index + 1,
1707
+ target: { line: targetLine, col: this.buffer.firstNonBlank(targetLine) },
1708
+ linewise: true,
1709
+ };
1710
+ }
1711
+ case "-": {
1712
+ const targetLine = this.buffer.clampLine(this.buffer.cursor.line - count);
1713
+ return {
1714
+ nextIndex: index + 1,
1715
+ target: { line: targetLine, col: this.buffer.firstNonBlank(targetLine) },
1716
+ linewise: true,
1717
+ };
1718
+ }
1719
+ case "_": {
1720
+ const targetLine = this.buffer.clampLine(this.buffer.cursor.line + (count - 1));
1721
+ return {
1722
+ nextIndex: index + 1,
1723
+ target: { line: targetLine, col: this.buffer.firstNonBlank(targetLine) },
1724
+ linewise: true,
1725
+ };
1726
+ }
1727
+ case "g": {
1728
+ const next = tokens[index + 1];
1729
+ if (!next) {
1730
+ throw new VimError("Unsupported g motion", token);
1731
+ }
1732
+ if (next.value === "g") {
1733
+ return {
1734
+ nextIndex: index + 2,
1735
+ target: { line: hasCount ? Math.max(0, count - 1) : 0, col: 0 },
1736
+ linewise: true,
1737
+ };
1738
+ }
1739
+ if (next.value === "e" || next.value === "E") {
1740
+ let offset = this.buffer.currentOffset();
1741
+ for (let step = 0; step < count; step += 1) {
1742
+ offset = endOfPreviousWord(text, offset, next.value === "E");
1743
+ }
1744
+ return { nextIndex: index + 2, target: this.buffer.offsetToPosition(offset) };
1745
+ }
1746
+ if (next.value === "_") {
1747
+ const targetLine = this.buffer.clampLine(this.buffer.cursor.line + (count - 1));
1748
+ return {
1749
+ nextIndex: index + 2,
1750
+ target: { line: targetLine, col: lastNonBlankColumn(this.buffer.getLine(targetLine)) },
1751
+ };
1752
+ }
1753
+ throw new VimError("Unsupported g motion", token);
1754
+ }
1755
+ case "G":
1756
+ return {
1757
+ nextIndex: index + 1,
1758
+ target: { line: hasCount ? count - 1 : this.buffer.lastLineIndex(), col: 0 },
1759
+ linewise: true,
1760
+ };
1761
+ case "f":
1762
+ case "F":
1763
+ case "t":
1764
+ case "T": {
1765
+ const searchToken = tokens[index + 1];
1766
+ if (searchToken?.value.length !== 1) {
1767
+ throw new VimError(`${token.value} requires a literal character`, token);
1768
+ }
1769
+ this.lastCharFind = { char: searchToken.value, mode: token.value as "f" | "F" | "t" | "T" };
1770
+ const line = this.buffer.getLine(this.buffer.cursor.line);
1771
+ const cursorCol = this.buffer.cursor.col;
1772
+ let matchIndex = -1;
1773
+ if (token.value === "f" || token.value === "t") {
1774
+ let start = cursorCol + 1;
1775
+ for (let step = 0; step < count; step += 1) {
1776
+ matchIndex = line.indexOf(searchToken.value, start);
1777
+ if (matchIndex === -1) break;
1778
+ start = matchIndex + 1;
1779
+ }
1780
+ if (matchIndex === -1) {
1781
+ throw new VimError(`Character not found: ${searchToken.value}`, searchToken);
1782
+ }
1783
+ if (token.value === "t") {
1784
+ matchIndex -= 1;
1785
+ }
1786
+ } else {
1787
+ let start = Math.max(0, cursorCol - 1);
1788
+ for (let step = 0; step < count; step += 1) {
1789
+ matchIndex = line.lastIndexOf(searchToken.value, start);
1790
+ if (matchIndex === -1) break;
1791
+ start = matchIndex - 1;
1792
+ }
1793
+ if (matchIndex === -1) {
1794
+ throw new VimError(`Character not found: ${searchToken.value}`, searchToken);
1795
+ }
1796
+ if (token.value === "T") {
1797
+ matchIndex += 1;
1798
+ }
1799
+ }
1800
+ return {
1801
+ nextIndex: index + 2,
1802
+ target: { line: this.buffer.cursor.line, col: Math.max(0, matchIndex) },
1803
+ };
1804
+ }
1805
+ case "{":
1806
+ return {
1807
+ nextIndex: index + 1,
1808
+ target: { line: findParagraphStart(this.buffer.lines, this.buffer.cursor.line), col: 0 },
1809
+ linewise: true,
1810
+ };
1811
+ case "}":
1812
+ return {
1813
+ nextIndex: index + 1,
1814
+ target: { line: findParagraphEnd(this.buffer.lines, this.buffer.cursor.line), col: 0 },
1815
+ linewise: true,
1816
+ };
1817
+ case "%": {
1818
+ const match = this.#findMatchingBracket();
1819
+ return { nextIndex: index + 1, target: match };
1820
+ }
1821
+ case "H":
1822
+ return {
1823
+ nextIndex: index + 1,
1824
+ target: { line: Math.max(0, this.viewportStart - 1), col: 0 },
1825
+ linewise: true,
1826
+ };
1827
+ case "M":
1828
+ return {
1829
+ nextIndex: index + 1,
1830
+ target: { line: Math.max(0, this.viewportStart - 1 + 20), col: 0 },
1831
+ linewise: true,
1832
+ };
1833
+ case "L":
1834
+ return {
1835
+ nextIndex: index + 1,
1836
+ target: { line: Math.max(0, this.viewportStart - 1 + 39), col: 0 },
1837
+ linewise: true,
1838
+ };
1839
+ case ";":
1840
+ case ",": {
1841
+ if (!this.lastCharFind) {
1842
+ throw new VimError(
1843
+ "No previous character search. If you meant an ex-command range like `:4,5d`, add the `:` prefix and `<CR>` suffix.",
1844
+ token,
1845
+ );
1846
+ }
1847
+ let mode = this.lastCharFind.mode;
1848
+ if (token.value === ",") {
1849
+ const reverseMap: Record<string, "f" | "F" | "t" | "T"> = { f: "F", F: "f", t: "T", T: "t" };
1850
+ mode = reverseMap[mode]!;
1851
+ }
1852
+ const line = this.buffer.getLine(this.buffer.cursor.line);
1853
+ const cursorCol = this.buffer.cursor.col;
1854
+ let matchIndex = -1;
1855
+ if (mode === "f" || mode === "t") {
1856
+ let start = cursorCol + 1;
1857
+ for (let step = 0; step < count; step += 1) {
1858
+ matchIndex = line.indexOf(this.lastCharFind.char, start);
1859
+ if (matchIndex === -1) break;
1860
+ start = matchIndex + 1;
1861
+ }
1862
+ if (matchIndex !== -1 && mode === "t") matchIndex -= 1;
1863
+ } else {
1864
+ let start = Math.max(0, cursorCol - 1);
1865
+ for (let step = 0; step < count; step += 1) {
1866
+ matchIndex = line.lastIndexOf(this.lastCharFind.char, start);
1867
+ if (matchIndex === -1) break;
1868
+ start = matchIndex - 1;
1869
+ }
1870
+ if (matchIndex !== -1 && mode === "T") matchIndex += 1;
1871
+ }
1872
+ if (matchIndex === -1) {
1873
+ throw new VimError(`Character not found: ${this.lastCharFind.char}`, token);
1874
+ }
1875
+ return {
1876
+ nextIndex: index + 1,
1877
+ target: { line: this.buffer.cursor.line, col: Math.max(0, matchIndex) },
1878
+ };
1879
+ }
1880
+ default:
1881
+ throw new VimError(`Unsupported motion: ${token.display}`, token);
1882
+ }
1883
+ }
1884
+
1885
+ #resolveTextObject(
1886
+ inner: boolean,
1887
+ objectToken: string,
1888
+ sourceToken: VimKeyToken,
1889
+ ): { start: number; end: number; linewise?: boolean } {
1890
+ if (objectToken === "w" || objectToken === "W") {
1891
+ return this.#resolveWordTextObject(inner, objectToken === "W");
1892
+ }
1893
+ if (objectToken === '"' || objectToken === "'" || objectToken === "`") {
1894
+ return this.#resolveQuoteTextObject(inner, objectToken, sourceToken);
1895
+ }
1896
+
1897
+ if (objectToken === "p") {
1898
+ return this.#resolveParagraphTextObject(inner);
1899
+ }
1900
+
1901
+ const normalized =
1902
+ objectToken === ")"
1903
+ ? "("
1904
+ : objectToken === "}"
1905
+ ? "{"
1906
+ : objectToken === "]"
1907
+ ? "["
1908
+ : objectToken === ">"
1909
+ ? "<"
1910
+ : objectToken;
1911
+ if (!BRACKET_PAIRS.has(normalized)) {
1912
+ throw new VimError(`Unsupported text object: ${objectToken}`, sourceToken);
1913
+ }
1914
+ return this.#resolveBracketTextObject(inner, normalized, sourceToken);
1915
+ }
1916
+
1917
+ #resolveWordTextObject(inner: boolean, bigWord: boolean): { start: number; end: number } {
1918
+ const text = this.buffer.getText();
1919
+ const cursor = this.buffer.currentOffset();
1920
+ let start = cursor;
1921
+ if (wordCategory(text[start] ?? "", bigWord) === "space") {
1922
+ start = nextWordStart(text, start, bigWord);
1923
+ }
1924
+ const category = wordCategory(text[start] ?? "", bigWord);
1925
+ while (start > 0 && wordCategory(text[start - 1] ?? "", bigWord) === category) {
1926
+ start -= 1;
1927
+ }
1928
+ let end = start;
1929
+ while (end < text.length && wordCategory(text[end] ?? "", bigWord) === category) {
1930
+ end += 1;
1931
+ }
1932
+ if (!inner) {
1933
+ while (end < text.length && wordCategory(text[end] ?? "", bigWord) === "space") {
1934
+ end += 1;
1935
+ }
1936
+ while (start > 0 && wordCategory(text[start - 1] ?? "", bigWord) === "space") {
1937
+ start -= 1;
1938
+ }
1939
+ }
1940
+ return { start, end };
1941
+ }
1942
+
1943
+ #resolveParagraphTextObject(inner: boolean): { start: number; end: number; linewise: boolean } {
1944
+ const lines = this.buffer.lines;
1945
+ const cursorLine = this.buffer.cursor.line;
1946
+ let start = cursorLine;
1947
+ let end = cursorLine;
1948
+ // Find paragraph boundaries (delimited by blank lines)
1949
+ if (lines[cursorLine]?.trim().length === 0) {
1950
+ // On a blank line: select contiguous blank lines
1951
+ while (start > 0 && lines[start - 1]!.trim().length === 0) start -= 1;
1952
+ while (end < lines.length - 1 && lines[end + 1]!.trim().length === 0) end += 1;
1953
+ if (!inner) {
1954
+ // Include following non-blank paragraph
1955
+ while (end < lines.length - 1 && lines[end + 1]!.trim().length > 0) end += 1;
1956
+ }
1957
+ } else {
1958
+ // On a non-blank line: select contiguous non-blank lines
1959
+ while (start > 0 && lines[start - 1]!.trim().length > 0) start -= 1;
1960
+ while (end < lines.length - 1 && lines[end + 1]!.trim().length > 0) end += 1;
1961
+ if (!inner) {
1962
+ // Include trailing blank lines
1963
+ while (end < lines.length - 1 && lines[end + 1]!.trim().length === 0) end += 1;
1964
+ }
1965
+ }
1966
+ const startOffset = this.buffer.positionToOffset({ line: start, col: 0 });
1967
+ const endOffset =
1968
+ end >= this.buffer.lastLineIndex()
1969
+ ? this.buffer.getText().length
1970
+ : this.buffer.positionToOffset({ line: end + 1, col: 0 });
1971
+ return { start: startOffset, end: endOffset, linewise: true };
1972
+ }
1973
+
1974
+ #resolveQuoteTextObject(inner: boolean, quote: string, sourceToken: VimKeyToken): { start: number; end: number } {
1975
+ const line = this.buffer.getLine(this.buffer.cursor.line);
1976
+ const col = this.buffer.cursor.col;
1977
+ const before = line.lastIndexOf(quote, col);
1978
+ const after = line.indexOf(quote, col + (line[col] === quote ? 1 : 0));
1979
+ if (before === -1 || after === -1 || before === after) {
1980
+ throw new VimError(`Quote text object not found for ${quote}`, sourceToken);
1981
+ }
1982
+ const startCol = inner ? before + 1 : before;
1983
+ const endCol = inner ? after : after + 1;
1984
+ return {
1985
+ start: this.buffer.positionToOffset({ line: this.buffer.cursor.line, col: startCol }),
1986
+ end: this.buffer.positionToOffset({ line: this.buffer.cursor.line, col: endCol }),
1987
+ };
1988
+ }
1989
+
1990
+ #resolveBracketTextObject(inner: boolean, open: string, sourceToken: VimKeyToken): { start: number; end: number } {
1991
+ const close = BRACKET_PAIRS.get(open)!;
1992
+ const text = this.buffer.getText();
1993
+ const cursor = this.buffer.currentOffset();
1994
+ let start = -1;
1995
+ let depth = 0;
1996
+ for (let index = cursor; index >= 0; index -= 1) {
1997
+ const char = text[index] ?? "";
1998
+ if (char === close) {
1999
+ depth += 1;
2000
+ } else if (char === open) {
2001
+ if (depth === 0) {
2002
+ start = index;
2003
+ break;
2004
+ }
2005
+ depth -= 1;
2006
+ }
2007
+ }
2008
+ if (start === -1) {
2009
+ throw new VimError(`Text object ${open}${close} not found`, sourceToken);
2010
+ }
2011
+ let end = -1;
2012
+ depth = 0;
2013
+ for (let index = start; index < text.length; index += 1) {
2014
+ const char = text[index] ?? "";
2015
+ if (char === open) {
2016
+ depth += 1;
2017
+ } else if (char === close) {
2018
+ depth -= 1;
2019
+ if (depth === 0) {
2020
+ end = index;
2021
+ break;
2022
+ }
2023
+ }
2024
+ }
2025
+ if (end === -1) {
2026
+ throw new VimError(`Text object ${open}${close} not found`, sourceToken);
2027
+ }
2028
+ return {
2029
+ start: inner ? start + 1 : start,
2030
+ end: inner ? end : end + 1,
2031
+ };
2032
+ }
2033
+
2034
+ #findMatchingBracket(): Position {
2035
+ const text = this.buffer.getText();
2036
+ const cursor = this.buffer.currentOffset();
2037
+ let offset = cursor;
2038
+ let char = text[offset] ?? "";
2039
+ if (!BRACKET_PAIRS.has(char) && !CLOSING_BRACKETS.has(char)) {
2040
+ offset += 1;
2041
+ char = text[offset] ?? "";
2042
+ }
2043
+ if (BRACKET_PAIRS.has(char)) {
2044
+ const close = BRACKET_PAIRS.get(char)!;
2045
+ let depth = 0;
2046
+ for (let index = offset; index < text.length; index += 1) {
2047
+ const current = text[index] ?? "";
2048
+ if (current === char) depth += 1;
2049
+ if (current === close) {
2050
+ depth -= 1;
2051
+ if (depth === 0) {
2052
+ return this.buffer.offsetToPosition(index);
2053
+ }
2054
+ }
2055
+ }
2056
+ }
2057
+ if (CLOSING_BRACKETS.has(char)) {
2058
+ const open = CLOSING_BRACKETS.get(char)!;
2059
+ let depth = 0;
2060
+ for (let index = offset; index >= 0; index -= 1) {
2061
+ const current = text[index] ?? "";
2062
+ if (current === char) depth += 1;
2063
+ if (current === open) {
2064
+ depth -= 1;
2065
+ if (depth === 0) {
2066
+ return this.buffer.offsetToPosition(index);
2067
+ }
2068
+ }
2069
+ }
2070
+ }
2071
+ throw new VimError("Matching bracket not found");
2072
+ }
2073
+
2074
+ async #runSearch(pattern: string, direction: 1 | -1, updateState: boolean): Promise<void> {
2075
+ const text = this.buffer.getText();
2076
+ const regex = createSearchRegex(pattern, "g");
2077
+ const cursor = this.buffer.currentOffset();
2078
+ let matchOffset = -1;
2079
+
2080
+ if (direction > 0) {
2081
+ regex.lastIndex = Math.min(text.length, cursor + 1);
2082
+ const match = regex.exec(text);
2083
+ if (match && match.index >= 0) {
2084
+ matchOffset = match.index;
2085
+ } else {
2086
+ regex.lastIndex = 0;
2087
+ const wrapMatch = regex.exec(text);
2088
+ if (wrapMatch && wrapMatch.index >= 0) {
2089
+ matchOffset = wrapMatch.index;
2090
+ }
2091
+ }
2092
+ } else {
2093
+ const matches = Array.from(text.matchAll(regex));
2094
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
2095
+ const match = matches[index];
2096
+ if ((match.index ?? -1) < cursor) {
2097
+ matchOffset = match.index ?? -1;
2098
+ break;
2099
+ }
2100
+ }
2101
+ if (matchOffset === -1 && matches.length > 0) {
2102
+ matchOffset = matches[matches.length - 1]?.index ?? -1;
2103
+ }
2104
+ }
2105
+
2106
+ if (matchOffset === -1) {
2107
+ throw new VimError(`Pattern not found: ${pattern}`);
2108
+ }
2109
+
2110
+ this.buffer.setCursor(this.buffer.offsetToPosition(matchOffset));
2111
+ this.statusMessage = `${direction > 0 ? "/" : "?"}${pattern}`;
2112
+ if (updateState) {
2113
+ this.lastSearch = { pattern, direction };
2114
+ }
2115
+ }
2116
+
2117
+ async #repeatSearch(direction: 1 | -1, count: number): Promise<void> {
2118
+ if (!this.lastSearch) {
2119
+ throw new VimError("No previous search");
2120
+ }
2121
+ for (let index = 0; index < count; index += 1) {
2122
+ await this.#runSearch(this.lastSearch.pattern, direction, false);
2123
+ }
2124
+ this.lastSearch = { pattern: this.lastSearch.pattern, direction };
2125
+ }
2126
+
2127
+ #resolveExRange(
2128
+ range: VimLineRange | "all" | undefined,
2129
+ defaultStart: number,
2130
+ defaultEnd = defaultStart,
2131
+ ): VimLineRange {
2132
+ const totalLines = Math.max(1, this.buffer.lineCount());
2133
+ if (range === "all") {
2134
+ return { start: 1, end: totalLines };
2135
+ }
2136
+ const next = range ?? { start: defaultStart, end: defaultEnd };
2137
+ const start = Math.max(1, Math.min(next.start, totalLines));
2138
+ const end = Math.max(start, Math.min(next.end, totalLines));
2139
+ return { start, end };
2140
+ }
2141
+
2142
+ async #executeEx(input: string): Promise<void> {
2143
+ const command = parseExCommand(input, {
2144
+ currentLine: this.buffer.cursor.line + 1,
2145
+ lastLine: this.buffer.lineCount(),
2146
+ });
2147
+ switch (command.kind) {
2148
+ case "goto-line":
2149
+ this.buffer.setCursor({ line: Math.max(0, command.line - 1), col: 0 });
2150
+ this.statusMessage = `Line ${command.line}`;
2151
+ return;
2152
+ case "write": {
2153
+ const result = await this.#callbacks.saveBuffer(this.buffer, { force: command.force });
2154
+ this.buffer.markSaved(result.loaded);
2155
+ this.diagnostics = result.diagnostics;
2156
+ this.statusMessage = result.diagnostics
2157
+ ? `Wrote ${this.buffer.displayPath} (${result.diagnostics.summary})`
2158
+ : `Wrote ${this.buffer.displayPath}`;
2159
+ this.#undoStack = [];
2160
+ this.#redoStack = [];
2161
+ return;
2162
+ }
2163
+ case "update":
2164
+ if (!this.buffer.modified) {
2165
+ this.statusMessage = `${this.buffer.displayPath} unchanged`;
2166
+ return;
2167
+ }
2168
+ await this.#executeEx(command.force ? "w!" : "w");
2169
+ return;
2170
+ case "write-quit":
2171
+ await this.#executeEx(command.force ? "w!" : "w");
2172
+ this.closed = true;
2173
+ this.statusMessage = `Wrote and closed ${this.buffer.displayPath}`;
2174
+ return;
2175
+ case "quit":
2176
+ if (this.buffer.modified && !command.force) {
2177
+ throw new VimError("Unsaved changes; use :q! to discard");
2178
+ }
2179
+ this.closed = true;
2180
+ this.statusMessage = `Closed ${this.buffer.displayPath}`;
2181
+ return;
2182
+ case "edit": {
2183
+ if (this.buffer.modified && !command.force) {
2184
+ throw new VimError("Unsaved changes; use :e! to reload or force open");
2185
+ }
2186
+ const next = await this.#callbacks.loadBuffer(command.path ?? this.buffer.displayPath);
2187
+ this.buffer.replaceLoadedFile(next);
2188
+ this.inputMode = "normal";
2189
+ this.selectionAnchor = null;
2190
+ this.#pendingInput = "";
2191
+ this.#pendingChange = null;
2192
+ this.#undoStack = [];
2193
+ this.#redoStack = [];
2194
+ this.statusMessage = command.path
2195
+ ? `Opened ${this.buffer.displayPath}`
2196
+ : `Reloaded ${this.buffer.displayPath}`;
2197
+ return;
2198
+ }
2199
+ case "substitute": {
2200
+ const range = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2201
+ const startLine = range.start;
2202
+ const endLine = range.end;
2203
+ const regexFlags = command.flags.includes("i") ? "gi" : "g";
2204
+ const regex = createSearchRegex(command.pattern, regexFlags);
2205
+ let replacements = 0;
2206
+ await this.#applyAtomicChange([":substitute"], () => {
2207
+ for (let lineIndex = startLine - 1; lineIndex <= endLine - 1; lineIndex += 1) {
2208
+ const line = this.buffer.getLine(lineIndex);
2209
+ let lineReplacements = 0;
2210
+ const nextLine = line.replace(regex, match => {
2211
+ if (!command.flags.includes("g") && lineReplacements > 0) {
2212
+ return match;
2213
+ }
2214
+ lineReplacements += 1;
2215
+ replacements += 1;
2216
+ return decodeReplacement(command.replacement).replace(/&/g, match);
2217
+ });
2218
+ this.buffer.replaceLine(lineIndex, nextLine);
2219
+ regex.lastIndex = 0;
2220
+ }
2221
+ });
2222
+ if (replacements === 0) {
2223
+ throw new VimError(`Pattern not found: ${command.pattern}`);
2224
+ }
2225
+ this.statusMessage = `${replacements} substitution${replacements === 1 ? "" : "s"}`;
2226
+ return;
2227
+ }
2228
+ case "delete": {
2229
+ const range = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2230
+ await this.#applyAtomicChange([":delete"], () => {
2231
+ const removed = this.buffer.deleteLines(range.start - 1, range.end - 1);
2232
+ this.register = { kind: "line", text: removed.join("\n") };
2233
+ });
2234
+ this.statusMessage = `Deleted ${range.end - range.start + 1} line${range.end === range.start ? "" : "s"}`;
2235
+ return;
2236
+ }
2237
+ case "yank": {
2238
+ const range = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2239
+ this.register = {
2240
+ kind: "line",
2241
+ text: this.buffer.lines.slice(range.start - 1, range.end).join("\n"),
2242
+ };
2243
+ this.statusMessage = `Yanked ${range.end - range.start + 1} line${range.end === range.start ? "" : "s"}`;
2244
+ return;
2245
+ }
2246
+ case "put": {
2247
+ if (!this.register.text) {
2248
+ this.statusMessage = "Register empty";
2249
+ return;
2250
+ }
2251
+ const anchorRange = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2252
+ const anchorLine = command.before ? anchorRange.start : anchorRange.end;
2253
+ const lines = this.register.text.split("\n");
2254
+ await this.#applyAtomicChange([":put"], () => {
2255
+ const insertAt = command.before
2256
+ ? Math.max(0, anchorLine - 1)
2257
+ : Math.min(anchorLine, this.buffer.lineCount());
2258
+ this.buffer.insertLines(insertAt, lines);
2259
+ });
2260
+ this.statusMessage = `Put ${lines.length} line${lines.length === 1 ? "" : "s"}`;
2261
+ return;
2262
+ }
2263
+ case "copy": {
2264
+ const totalLines = this.buffer.lineCount();
2265
+ const range = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2266
+ const dest = Math.max(0, Math.min(command.destination, totalLines));
2267
+ await this.#applyAtomicChange([":copy"], () => {
2268
+ const lines = this.buffer.lines.slice(range.start - 1, range.end);
2269
+ this.buffer.insertLines(dest, lines);
2270
+ });
2271
+ this.statusMessage = `Copied ${range.end - range.start + 1} line${range.end === range.start ? "" : "s"}`;
2272
+ return;
2273
+ }
2274
+ case "move": {
2275
+ const totalLines = this.buffer.lineCount();
2276
+ const range = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2277
+ const dest = Math.max(0, Math.min(command.destination, totalLines));
2278
+ await this.#applyAtomicChange([":move"], () => {
2279
+ const lines = this.buffer.lines.splice(range.start - 1, range.end - range.start + 1);
2280
+ const adjustedDest = dest > range.end - 1 ? dest - lines.length : dest;
2281
+ this.buffer.lines.splice(adjustedDest, 0, ...lines);
2282
+ if (this.buffer.lines.length === 0) this.buffer.lines = [""];
2283
+ this.buffer.setCursor({ line: adjustedDest, col: 0 });
2284
+ });
2285
+ this.statusMessage = `Moved ${range.end - range.start + 1} line${range.end === range.start ? "" : "s"}`;
2286
+ return;
2287
+ }
2288
+ case "sort": {
2289
+ const range = this.#resolveExRange(command.range ?? "all", 1, this.buffer.lineCount());
2290
+ const startLine = range.start;
2291
+ const endLine = range.end;
2292
+ const reverse = command.flags.includes("!");
2293
+ const ignoreCase = command.flags.includes("i");
2294
+ await this.#applyAtomicChange([":sort"], () => {
2295
+ const slice = this.buffer.lines.slice(startLine - 1, endLine);
2296
+ slice.sort((a, b) => {
2297
+ const left = ignoreCase ? a.toLowerCase() : a;
2298
+ const right = ignoreCase ? b.toLowerCase() : b;
2299
+ return left < right ? -1 : left > right ? 1 : 0;
2300
+ });
2301
+ if (reverse) slice.reverse();
2302
+ for (let i = 0; i < slice.length; i++) {
2303
+ this.buffer.lines[startLine - 1 + i] = slice[i]!;
2304
+ }
2305
+ });
2306
+ this.statusMessage = `Sorted ${endLine - startLine + 1} line${endLine === startLine ? "" : "s"}`;
2307
+ return;
2308
+ }
2309
+ case "join": {
2310
+ const currentLine = this.buffer.cursor.line + 1;
2311
+ const baseRange = this.#resolveExRange(
2312
+ command.range,
2313
+ currentLine,
2314
+ command.range ? undefined : Math.min(this.buffer.lineCount(), currentLine + 1),
2315
+ );
2316
+ const startLine = baseRange.start;
2317
+ const endLine =
2318
+ baseRange.start === baseRange.end ? Math.min(this.buffer.lineCount(), baseRange.end + 1) : baseRange.end;
2319
+ const lineCount = endLine - startLine + 1;
2320
+ if (lineCount < 2) {
2321
+ this.statusMessage = "Nothing to join";
2322
+ return;
2323
+ }
2324
+ await this.#applyAtomicChange([":join"], () => {
2325
+ const startIndex = startLine - 1;
2326
+ if (command.trimWhitespace) {
2327
+ this.buffer.joinLines(startIndex, lineCount - 1);
2328
+ return;
2329
+ }
2330
+ const joined = this.buffer.lines.slice(startIndex, endLine).join("");
2331
+ this.buffer.lines.splice(startIndex, lineCount, joined);
2332
+ this.buffer.setCursor({ line: startIndex, col: Math.max(0, joined.length - 1) });
2333
+ });
2334
+ this.statusMessage = `Joined ${lineCount} lines`;
2335
+ return;
2336
+ }
2337
+ case "append": {
2338
+ const anchorRange = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2339
+ const anchorLine = anchorRange.end;
2340
+ const lines = command.text.length > 0 ? command.text.split("\n") : [""];
2341
+ await this.#applyAtomicChange([":append"], () => {
2342
+ const insertAt = Math.min(anchorLine, this.buffer.lineCount());
2343
+ this.buffer.insertLines(insertAt, lines);
2344
+ });
2345
+ this.statusMessage = `Appended ${lines.length} line${lines.length === 1 ? "" : "s"}`;
2346
+ return;
2347
+ }
2348
+ case "insert-before": {
2349
+ const anchorRange = this.#resolveExRange(command.range, this.buffer.cursor.line + 1);
2350
+ const anchorLine = anchorRange.start;
2351
+ const lines = command.text.length > 0 ? command.text.split("\n") : [""];
2352
+ await this.#applyAtomicChange([":insert"], () => {
2353
+ const insertAt = Math.max(0, anchorLine - 1);
2354
+ this.buffer.insertLines(insertAt, lines);
2355
+ });
2356
+ this.statusMessage = `Inserted ${lines.length} line${lines.length === 1 ? "" : "s"}`;
2357
+ return;
2358
+ }
2359
+ case "global": {
2360
+ const regex = createSearchRegex(command.pattern);
2361
+ const range = this.#resolveExRange(command.range ?? "all", 1, this.buffer.lineCount());
2362
+ await this.#applyAtomicChange([":global"], () => {
2363
+ const linesToProcess: number[] = [];
2364
+ for (let i = range.start - 1; i <= range.end - 1; i += 1) {
2365
+ const matches = regex.test(this.buffer.getLine(i));
2366
+ regex.lastIndex = 0;
2367
+ if (command.invert ? !matches : matches) {
2368
+ linesToProcess.push(i);
2369
+ }
2370
+ }
2371
+ if (command.command === "d" || command.command === "delete") {
2372
+ // Delete matching lines in reverse to preserve indices
2373
+ for (let i = linesToProcess.length - 1; i >= 0; i--) {
2374
+ this.buffer.lines.splice(linesToProcess[i]!, 1);
2375
+ }
2376
+ if (this.buffer.lines.length === 0) this.buffer.lines = [""];
2377
+ this.buffer.clampCursor();
2378
+ this.buffer.trailingNewline = true;
2379
+ } else {
2380
+ throw new VimError(`Unsupported :global sub-command: ${command.command}`);
2381
+ }
2382
+ });
2383
+ this.statusMessage = `Global: processed ${command.pattern}`;
2384
+ return;
2385
+ }
2386
+ }
2387
+ }
2388
+
2389
+ #paste(after: boolean, count: number): void {
2390
+ if (!this.register.text) {
2391
+ return;
2392
+ }
2393
+ if (this.register.kind === "line") {
2394
+ const lines = this.register.text.split("\n");
2395
+ const insertAt = after ? this.buffer.cursor.line + 1 : this.buffer.cursor.line;
2396
+ for (let iteration = 0; iteration < count; iteration += 1) {
2397
+ this.buffer.insertLines(insertAt + iteration * lines.length, lines);
2398
+ }
2399
+ return;
2400
+ }
2401
+ const text = this.register.text.repeat(count);
2402
+ const offset = this.buffer.currentOffset() + (after ? 1 : 0);
2403
+ this.buffer.replaceOffsets(offset, offset, text, offset + text.length);
2404
+ }
2405
+
2406
+ #readCount(tokens: readonly VimKeyToken[], index: number): { count: number; hasCount: boolean; nextIndex: number } {
2407
+ let cursor = index;
2408
+ let digits = "";
2409
+ while (cursor < tokens.length) {
2410
+ const value = tokens[cursor]?.value ?? "";
2411
+ if (!/^\d$/.test(value)) {
2412
+ break;
2413
+ }
2414
+ if (digits.length === 0 && value === "0") {
2415
+ break;
2416
+ }
2417
+ digits += value;
2418
+ cursor += 1;
2419
+ }
2420
+ return {
2421
+ count: digits.length > 0 ? Number.parseInt(digits, 10) : 1,
2422
+ hasCount: digits.length > 0,
2423
+ nextIndex: cursor,
2424
+ };
2425
+ }
2426
+ }