@google/gemini-cli 0.10.0-preview.3 → 0.11.0-nightly.20251021.e72c00cf

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 (472) hide show
  1. package/README.md +9 -0
  2. package/dist/google-gemini-cli-0.11.0-nightly.20251020.a96f0659.tgz +0 -0
  3. package/dist/index.js +5 -5
  4. package/dist/index.js.map +1 -1
  5. package/dist/package.json +2 -2
  6. package/dist/src/commands/extensions/disable.js +8 -5
  7. package/dist/src/commands/extensions/disable.js.map +1 -1
  8. package/dist/src/commands/extensions/enable.js +5 -3
  9. package/dist/src/commands/extensions/enable.js.map +1 -1
  10. package/dist/src/commands/extensions/examples/mcp-server/example.d.ts +6 -0
  11. package/dist/src/commands/extensions/examples/mcp-server/example.js +46 -0
  12. package/dist/src/commands/extensions/examples/mcp-server/example.js.map +1 -0
  13. package/dist/src/commands/extensions/install.d.ts +1 -0
  14. package/dist/src/commands/extensions/install.js +18 -4
  15. package/dist/src/commands/extensions/install.js.map +1 -1
  16. package/dist/src/commands/extensions/link.js +3 -2
  17. package/dist/src/commands/extensions/link.js.map +1 -1
  18. package/dist/src/commands/extensions/list.js +7 -5
  19. package/dist/src/commands/extensions/list.js.map +1 -1
  20. package/dist/src/commands/extensions/new.js +14 -20
  21. package/dist/src/commands/extensions/new.js.map +1 -1
  22. package/dist/src/commands/extensions/uninstall.js +3 -2
  23. package/dist/src/commands/extensions/uninstall.js.map +1 -1
  24. package/dist/src/commands/extensions/update.js +17 -17
  25. package/dist/src/commands/extensions/update.js.map +1 -1
  26. package/dist/src/commands/mcp/add.js +7 -4
  27. package/dist/src/commands/mcp/add.js.map +1 -1
  28. package/dist/src/commands/mcp/add.test.d.ts +6 -0
  29. package/dist/src/commands/mcp/add.test.js +244 -0
  30. package/dist/src/commands/mcp/add.test.js.map +1 -0
  31. package/dist/src/commands/mcp/list.js +6 -4
  32. package/dist/src/commands/mcp/list.js.map +1 -1
  33. package/dist/src/commands/mcp/list.test.d.ts +6 -0
  34. package/dist/src/commands/mcp/list.test.js +117 -0
  35. package/dist/src/commands/mcp/list.test.js.map +1 -0
  36. package/dist/src/commands/mcp/remove.test.d.ts +6 -0
  37. package/dist/src/commands/mcp/remove.test.js +175 -0
  38. package/dist/src/commands/mcp/remove.test.js.map +1 -0
  39. package/dist/src/commands/mcp.test.d.ts +6 -0
  40. package/dist/src/commands/mcp.test.js +62 -0
  41. package/dist/src/commands/mcp.test.js.map +1 -0
  42. package/dist/src/config/auth.js +3 -1
  43. package/dist/src/config/auth.js.map +1 -1
  44. package/dist/src/config/auth.test.js +3 -1
  45. package/dist/src/config/auth.test.js.map +1 -1
  46. package/dist/src/config/config.d.ts +2 -14
  47. package/dist/src/config/config.integration.test.d.ts +6 -0
  48. package/dist/src/config/config.integration.test.js +321 -0
  49. package/dist/src/config/config.integration.test.js.map +1 -0
  50. package/dist/src/config/config.js +38 -102
  51. package/dist/src/config/config.js.map +1 -1
  52. package/dist/src/config/config.test.d.ts +6 -0
  53. package/dist/src/config/config.test.js +1961 -0
  54. package/dist/src/config/config.test.js.map +1 -0
  55. package/dist/src/config/extension.d.ts +4 -15
  56. package/dist/src/config/extension.js +84 -97
  57. package/dist/src/config/extension.js.map +1 -1
  58. package/dist/src/config/extension.test.d.ts +6 -0
  59. package/dist/src/config/extension.test.js +1076 -0
  60. package/dist/src/config/extension.test.js.map +1 -0
  61. package/dist/src/config/extensions/extensionEnablement.d.ts +1 -1
  62. package/dist/src/config/extensions/extensionEnablement.js +4 -3
  63. package/dist/src/config/extensions/extensionEnablement.js.map +1 -1
  64. package/dist/src/config/extensions/extensionEnablement.test.js +21 -18
  65. package/dist/src/config/extensions/extensionEnablement.test.js.map +1 -1
  66. package/dist/src/config/extensions/github.d.ts +18 -9
  67. package/dist/src/config/extensions/github.js +106 -29
  68. package/dist/src/config/extensions/github.js.map +1 -1
  69. package/dist/src/config/extensions/github.test.js +20 -17
  70. package/dist/src/config/extensions/github.test.js.map +1 -1
  71. package/dist/src/config/extensions/update.d.ts +5 -4
  72. package/dist/src/config/extensions/update.js +10 -6
  73. package/dist/src/config/extensions/update.js.map +1 -1
  74. package/dist/src/config/extensions/update.test.js +57 -57
  75. package/dist/src/config/extensions/update.test.js.map +1 -1
  76. package/dist/src/config/extensions/variableSchema.d.ts +2 -0
  77. package/dist/src/config/extensions/variableSchema.js.map +1 -1
  78. package/dist/src/config/keyBindings.d.ts +2 -1
  79. package/dist/src/config/keyBindings.js +4 -2
  80. package/dist/src/config/keyBindings.js.map +1 -1
  81. package/dist/src/config/policy.d.ts +3 -2
  82. package/dist/src/config/policy.js +32 -23
  83. package/dist/src/config/policy.js.map +1 -1
  84. package/dist/src/config/policy.test.js +60 -36
  85. package/dist/src/config/policy.test.js.map +1 -1
  86. package/dist/src/config/sandboxConfig.d.ts +0 -1
  87. package/dist/src/config/sandboxConfig.js +1 -3
  88. package/dist/src/config/sandboxConfig.js.map +1 -1
  89. package/dist/src/config/settings.js +3 -1
  90. package/dist/src/config/settings.js.map +1 -1
  91. package/dist/src/config/settings.test.d.ts +6 -0
  92. package/dist/src/config/settings.test.js +1937 -0
  93. package/dist/src/config/settings.test.js.map +1 -0
  94. package/dist/src/config/settingsSchema.d.ts +1 -1
  95. package/dist/src/config/settingsSchema.js +1 -1
  96. package/dist/src/config/settingsSchema.js.map +1 -1
  97. package/dist/src/config/settingsSchema.test.js +1 -1
  98. package/dist/src/config/settingsSchema.test.js.map +1 -1
  99. package/dist/src/gemini.js +30 -24
  100. package/dist/src/gemini.js.map +1 -1
  101. package/dist/src/gemini.test.js +42 -11
  102. package/dist/src/gemini.test.js.map +1 -1
  103. package/dist/src/generated/git-commit.d.ts +2 -2
  104. package/dist/src/generated/git-commit.js +2 -2
  105. package/dist/src/generated/git-commit.js.map +1 -1
  106. package/dist/src/nonInteractiveCli.js +92 -4
  107. package/dist/src/nonInteractiveCli.js.map +1 -1
  108. package/dist/src/nonInteractiveCli.test.d.ts +6 -0
  109. package/dist/src/nonInteractiveCli.test.js +711 -0
  110. package/dist/src/nonInteractiveCli.test.js.map +1 -0
  111. package/dist/src/services/FileCommandLoader.test.d.ts +6 -0
  112. package/dist/src/services/FileCommandLoader.test.js +971 -0
  113. package/dist/src/services/FileCommandLoader.test.js.map +1 -0
  114. package/dist/src/services/prompt-processors/argumentProcessor.test.d.ts +6 -0
  115. package/dist/src/services/prompt-processors/argumentProcessor.test.js +40 -0
  116. package/dist/src/services/prompt-processors/argumentProcessor.test.js.map +1 -0
  117. package/dist/src/services/prompt-processors/atFileProcessor.js +3 -2
  118. package/dist/src/services/prompt-processors/atFileProcessor.js.map +1 -1
  119. package/dist/src/services/prompt-processors/shellProcessor.test.d.ts +6 -0
  120. package/dist/src/services/prompt-processors/shellProcessor.test.js +482 -0
  121. package/dist/src/services/prompt-processors/shellProcessor.test.js.map +1 -0
  122. package/dist/src/test-utils/render.d.ts +2 -1
  123. package/dist/src/test-utils/render.js +5 -2
  124. package/dist/src/test-utils/render.js.map +1 -1
  125. package/dist/src/ui/App.test.d.ts +6 -0
  126. package/dist/src/ui/App.test.js +110 -0
  127. package/dist/src/ui/App.test.js.map +1 -0
  128. package/dist/src/ui/AppContainer.js +43 -28
  129. package/dist/src/ui/AppContainer.js.map +1 -1
  130. package/dist/src/ui/AppContainer.test.js +35 -9
  131. package/dist/src/ui/AppContainer.test.js.map +1 -1
  132. package/dist/src/ui/auth/AuthDialog.d.ts +1 -1
  133. package/dist/src/ui/auth/AuthDialog.js +3 -1
  134. package/dist/src/ui/auth/AuthDialog.js.map +1 -1
  135. package/dist/src/ui/auth/useAuth.d.ts +1 -1
  136. package/dist/src/ui/auth/useAuth.js +3 -1
  137. package/dist/src/ui/auth/useAuth.js.map +1 -1
  138. package/dist/src/ui/commands/aboutCommand.js +1 -1
  139. package/dist/src/ui/commands/aboutCommand.test.d.ts +6 -0
  140. package/dist/src/ui/commands/aboutCommand.test.js +130 -0
  141. package/dist/src/ui/commands/aboutCommand.test.js.map +1 -0
  142. package/dist/src/ui/commands/authCommand.js +1 -1
  143. package/dist/src/ui/commands/authCommand.test.d.ts +6 -0
  144. package/dist/src/ui/commands/authCommand.test.js +30 -0
  145. package/dist/src/ui/commands/authCommand.test.js.map +1 -0
  146. package/dist/src/ui/commands/bugCommand.js +1 -1
  147. package/dist/src/ui/commands/bugCommand.test.d.ts +6 -0
  148. package/dist/src/ui/commands/bugCommand.test.js +105 -0
  149. package/dist/src/ui/commands/bugCommand.test.js.map +1 -0
  150. package/dist/src/ui/commands/chatCommand.js +1 -1
  151. package/dist/src/ui/commands/chatCommand.js.map +1 -1
  152. package/dist/src/ui/commands/chatCommand.test.d.ts +6 -0
  153. package/dist/src/ui/commands/chatCommand.test.js +555 -0
  154. package/dist/src/ui/commands/chatCommand.test.js.map +1 -0
  155. package/dist/src/ui/commands/clearCommand.js +1 -1
  156. package/dist/src/ui/commands/clearCommand.test.d.ts +6 -0
  157. package/dist/src/ui/commands/clearCommand.test.js +76 -0
  158. package/dist/src/ui/commands/clearCommand.test.js.map +1 -0
  159. package/dist/src/ui/commands/compressCommand.js +1 -1
  160. package/dist/src/ui/commands/compressCommand.js.map +1 -1
  161. package/dist/src/ui/commands/compressCommand.test.d.ts +6 -0
  162. package/dist/src/ui/commands/compressCommand.test.js +98 -0
  163. package/dist/src/ui/commands/compressCommand.test.js.map +1 -0
  164. package/dist/src/ui/commands/copyCommand.test.d.ts +6 -0
  165. package/dist/src/ui/commands/copyCommand.test.js +242 -0
  166. package/dist/src/ui/commands/copyCommand.test.js.map +1 -0
  167. package/dist/src/ui/commands/corgiCommand.js +1 -1
  168. package/dist/src/ui/commands/corgiCommand.js.map +1 -1
  169. package/dist/src/ui/commands/corgiCommand.test.d.ts +6 -0
  170. package/dist/src/ui/commands/corgiCommand.test.js +28 -0
  171. package/dist/src/ui/commands/corgiCommand.test.js.map +1 -0
  172. package/dist/src/ui/commands/directoryCommand.js +1 -1
  173. package/dist/src/ui/commands/directoryCommand.js.map +1 -1
  174. package/dist/src/ui/commands/directoryCommand.test.d.ts +6 -0
  175. package/dist/src/ui/commands/directoryCommand.test.js +144 -0
  176. package/dist/src/ui/commands/directoryCommand.test.js.map +1 -0
  177. package/dist/src/ui/commands/docsCommand.js +1 -1
  178. package/dist/src/ui/commands/docsCommand.test.d.ts +6 -0
  179. package/dist/src/ui/commands/docsCommand.test.js +72 -0
  180. package/dist/src/ui/commands/docsCommand.test.js.map +1 -0
  181. package/dist/src/ui/commands/editorCommand.js +1 -1
  182. package/dist/src/ui/commands/editorCommand.test.d.ts +6 -0
  183. package/dist/src/ui/commands/editorCommand.test.js +27 -0
  184. package/dist/src/ui/commands/editorCommand.test.js.map +1 -0
  185. package/dist/src/ui/commands/extensionsCommand.test.d.ts +6 -0
  186. package/dist/src/ui/commands/extensionsCommand.test.js +241 -0
  187. package/dist/src/ui/commands/extensionsCommand.test.js.map +1 -0
  188. package/dist/src/ui/commands/helpCommand.js +1 -1
  189. package/dist/src/ui/commands/helpCommand.test.d.ts +6 -0
  190. package/dist/src/ui/commands/helpCommand.test.js +42 -0
  191. package/dist/src/ui/commands/helpCommand.test.js.map +1 -0
  192. package/dist/src/ui/commands/ideCommand.js +6 -6
  193. package/dist/src/ui/commands/ideCommand.test.d.ts +6 -0
  194. package/dist/src/ui/commands/ideCommand.test.js +203 -0
  195. package/dist/src/ui/commands/ideCommand.test.js.map +1 -0
  196. package/dist/src/ui/commands/initCommand.js +1 -1
  197. package/dist/src/ui/commands/initCommand.js.map +1 -1
  198. package/dist/src/ui/commands/initCommand.test.d.ts +6 -0
  199. package/dist/src/ui/commands/initCommand.test.js +80 -0
  200. package/dist/src/ui/commands/initCommand.test.js.map +1 -0
  201. package/dist/src/ui/commands/mcpCommand.js +98 -88
  202. package/dist/src/ui/commands/mcpCommand.js.map +1 -1
  203. package/dist/src/ui/commands/mcpCommand.test.d.ts +6 -0
  204. package/dist/src/ui/commands/mcpCommand.test.js +148 -0
  205. package/dist/src/ui/commands/mcpCommand.test.js.map +1 -0
  206. package/dist/src/ui/commands/memoryCommand.js +6 -6
  207. package/dist/src/ui/commands/memoryCommand.js.map +1 -1
  208. package/dist/src/ui/commands/memoryCommand.test.d.ts +6 -0
  209. package/dist/src/ui/commands/memoryCommand.test.js +266 -0
  210. package/dist/src/ui/commands/memoryCommand.test.js.map +1 -0
  211. package/dist/src/ui/commands/privacyCommand.js +1 -1
  212. package/dist/src/ui/commands/privacyCommand.test.d.ts +6 -0
  213. package/dist/src/ui/commands/privacyCommand.test.js +32 -0
  214. package/dist/src/ui/commands/privacyCommand.test.js.map +1 -0
  215. package/dist/src/ui/commands/quitCommand.js +1 -1
  216. package/dist/src/ui/commands/quitCommand.test.d.ts +6 -0
  217. package/dist/src/ui/commands/quitCommand.test.js +50 -0
  218. package/dist/src/ui/commands/quitCommand.test.js.map +1 -0
  219. package/dist/src/ui/commands/restoreCommand.test.d.ts +6 -0
  220. package/dist/src/ui/commands/restoreCommand.test.js +190 -0
  221. package/dist/src/ui/commands/restoreCommand.test.js.map +1 -0
  222. package/dist/src/ui/commands/settingsCommand.test.d.ts +6 -0
  223. package/dist/src/ui/commands/settingsCommand.test.js +30 -0
  224. package/dist/src/ui/commands/settingsCommand.test.js.map +1 -0
  225. package/dist/src/ui/commands/setupGithubCommand.test.js +1 -2
  226. package/dist/src/ui/commands/setupGithubCommand.test.js.map +1 -1
  227. package/dist/src/ui/commands/statsCommand.js +3 -3
  228. package/dist/src/ui/commands/statsCommand.js.map +1 -1
  229. package/dist/src/ui/commands/statsCommand.test.d.ts +6 -0
  230. package/dist/src/ui/commands/statsCommand.test.js +53 -0
  231. package/dist/src/ui/commands/statsCommand.test.js.map +1 -0
  232. package/dist/src/ui/commands/terminalSetupCommand.test.d.ts +6 -0
  233. package/dist/src/ui/commands/terminalSetupCommand.test.js +66 -0
  234. package/dist/src/ui/commands/terminalSetupCommand.test.js.map +1 -0
  235. package/dist/src/ui/commands/themeCommand.js +1 -1
  236. package/dist/src/ui/commands/themeCommand.test.d.ts +6 -0
  237. package/dist/src/ui/commands/themeCommand.test.js +32 -0
  238. package/dist/src/ui/commands/themeCommand.test.js.map +1 -0
  239. package/dist/src/ui/commands/toolsCommand.js +1 -1
  240. package/dist/src/ui/commands/toolsCommand.test.d.ts +6 -0
  241. package/dist/src/ui/commands/toolsCommand.test.js +100 -0
  242. package/dist/src/ui/commands/toolsCommand.test.js.map +1 -0
  243. package/dist/src/ui/commands/vimCommand.js +1 -1
  244. package/dist/src/ui/components/Composer.js +5 -3
  245. package/dist/src/ui/components/Composer.js.map +1 -1
  246. package/dist/src/ui/components/Composer.test.js +16 -1
  247. package/dist/src/ui/components/Composer.test.js.map +1 -1
  248. package/dist/src/ui/components/ContextSummaryDisplay.d.ts +0 -1
  249. package/dist/src/ui/components/ContextSummaryDisplay.js +2 -12
  250. package/dist/src/ui/components/ContextSummaryDisplay.js.map +1 -1
  251. package/dist/src/ui/components/ContextSummaryDisplay.test.d.ts +6 -0
  252. package/dist/src/ui/components/ContextSummaryDisplay.test.js +66 -0
  253. package/dist/src/ui/components/ContextSummaryDisplay.test.js.map +1 -0
  254. package/dist/src/ui/components/DialogManager.js +1 -5
  255. package/dist/src/ui/components/DialogManager.js.map +1 -1
  256. package/dist/src/ui/components/EditorSettingsDialog.js +1 -1
  257. package/dist/src/ui/components/EditorSettingsDialog.js.map +1 -1
  258. package/dist/src/ui/components/FolderTrustDialog.test.js +7 -3
  259. package/dist/src/ui/components/FolderTrustDialog.test.js.map +1 -1
  260. package/dist/src/ui/components/Footer.js +1 -1
  261. package/dist/src/ui/components/Footer.js.map +1 -1
  262. package/dist/src/ui/components/Footer.test.d.ts +6 -0
  263. package/dist/src/ui/components/Footer.test.js +231 -0
  264. package/dist/src/ui/components/Footer.test.js.map +1 -0
  265. package/dist/src/ui/components/InputPrompt.d.ts +4 -0
  266. package/dist/src/ui/components/InputPrompt.js +53 -4
  267. package/dist/src/ui/components/InputPrompt.js.map +1 -1
  268. package/dist/src/ui/components/InputPrompt.test.d.ts +6 -0
  269. package/dist/src/ui/components/InputPrompt.test.js +1716 -0
  270. package/dist/src/ui/components/InputPrompt.test.js.map +1 -0
  271. package/dist/src/ui/components/ModelStatsDisplay.test.d.ts +6 -0
  272. package/dist/src/ui/components/ModelStatsDisplay.test.js +285 -0
  273. package/dist/src/ui/components/ModelStatsDisplay.test.js.map +1 -0
  274. package/dist/src/ui/components/PermissionsModifyTrustDialog.js +22 -18
  275. package/dist/src/ui/components/PermissionsModifyTrustDialog.js.map +1 -1
  276. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js +10 -2
  277. package/dist/src/ui/components/PermissionsModifyTrustDialog.test.js.map +1 -1
  278. package/dist/src/ui/components/QueuedMessageDisplay.js +3 -3
  279. package/dist/src/ui/components/QueuedMessageDisplay.js.map +1 -1
  280. package/dist/src/ui/components/QueuedMessageDisplay.test.js +4 -0
  281. package/dist/src/ui/components/QueuedMessageDisplay.test.js.map +1 -1
  282. package/dist/src/ui/components/RawMarkdownIndicator.d.ts +7 -0
  283. package/dist/src/ui/components/RawMarkdownIndicator.js +8 -0
  284. package/dist/src/ui/components/RawMarkdownIndicator.js.map +1 -0
  285. package/dist/src/ui/components/SessionSummaryDisplay.test.d.ts +6 -0
  286. package/dist/src/ui/components/SessionSummaryDisplay.test.js +74 -0
  287. package/dist/src/ui/components/SessionSummaryDisplay.test.js.map +1 -0
  288. package/dist/src/ui/components/SettingsDialog.js +8 -8
  289. package/dist/src/ui/components/SettingsDialog.js.map +1 -1
  290. package/dist/src/ui/components/SettingsDialog.test.js +189 -76
  291. package/dist/src/ui/components/SettingsDialog.test.js.map +1 -1
  292. package/dist/src/ui/components/StatsDisplay.test.d.ts +6 -0
  293. package/dist/src/ui/components/StatsDisplay.test.js +351 -0
  294. package/dist/src/ui/components/StatsDisplay.test.js.map +1 -0
  295. package/dist/src/ui/components/ThemeDialog.d.ts +4 -2
  296. package/dist/src/ui/components/ThemeDialog.js +3 -3
  297. package/dist/src/ui/components/ThemeDialog.js.map +1 -1
  298. package/dist/src/ui/components/ThemeDialog.test.js +13 -0
  299. package/dist/src/ui/components/ThemeDialog.test.js.map +1 -1
  300. package/dist/src/ui/components/ToolStatsDisplay.test.d.ts +6 -0
  301. package/dist/src/ui/components/ToolStatsDisplay.test.js +227 -0
  302. package/dist/src/ui/components/ToolStatsDisplay.test.js.map +1 -0
  303. package/dist/src/ui/components/messages/GeminiMessage.js +3 -1
  304. package/dist/src/ui/components/messages/GeminiMessage.js.map +1 -1
  305. package/dist/src/ui/components/messages/GeminiMessage.test.d.ts +6 -0
  306. package/dist/src/ui/components/messages/GeminiMessage.test.js +35 -0
  307. package/dist/src/ui/components/messages/GeminiMessage.test.js.map +1 -0
  308. package/dist/src/ui/components/messages/GeminiMessageContent.js +3 -1
  309. package/dist/src/ui/components/messages/GeminiMessageContent.js.map +1 -1
  310. package/dist/src/ui/components/messages/Todo.d.ts +7 -0
  311. package/dist/src/ui/components/messages/Todo.js +59 -0
  312. package/dist/src/ui/components/messages/Todo.js.map +1 -0
  313. package/dist/src/ui/components/messages/Todo.test.d.ts +6 -0
  314. package/dist/src/ui/components/messages/Todo.test.js +113 -0
  315. package/dist/src/ui/components/messages/Todo.test.js.map +1 -0
  316. package/dist/src/ui/components/messages/ToolGroupMessage.js +1 -1
  317. package/dist/src/ui/components/messages/ToolGroupMessage.js.map +1 -1
  318. package/dist/src/ui/components/messages/ToolMessage.js +8 -3
  319. package/dist/src/ui/components/messages/ToolMessage.js.map +1 -1
  320. package/dist/src/ui/components/messages/ToolMessage.test.js +2 -2
  321. package/dist/src/ui/components/messages/ToolMessage.test.js.map +1 -1
  322. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.d.ts +6 -0
  323. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js +30 -0
  324. package/dist/src/ui/components/messages/ToolMessageRawMarkdown.test.js.map +1 -0
  325. package/dist/src/ui/components/messages/UserShellMessage.js +1 -1
  326. package/dist/src/ui/components/messages/UserShellMessage.js.map +1 -1
  327. package/dist/src/ui/components/shared/BaseSelectionList.test.js +1 -1
  328. package/dist/src/ui/components/shared/BaseSelectionList.test.js.map +1 -1
  329. package/dist/src/ui/components/shared/text-buffer.test.d.ts +6 -0
  330. package/dist/src/ui/components/shared/text-buffer.test.js +1554 -0
  331. package/dist/src/ui/components/shared/text-buffer.test.js.map +1 -0
  332. package/dist/src/ui/components/shared/vim-buffer-actions.test.d.ts +6 -0
  333. package/dist/src/ui/components/shared/vim-buffer-actions.test.js +951 -0
  334. package/dist/src/ui/components/shared/vim-buffer-actions.test.js.map +1 -0
  335. package/dist/src/ui/components/views/ExtensionsList.js +3 -4
  336. package/dist/src/ui/components/views/ExtensionsList.js.map +1 -1
  337. package/dist/src/ui/components/views/ExtensionsList.test.js +2 -9
  338. package/dist/src/ui/components/views/ExtensionsList.test.js.map +1 -1
  339. package/dist/src/ui/components/views/McpStatus.d.ts +0 -1
  340. package/dist/src/ui/components/views/McpStatus.js +4 -4
  341. package/dist/src/ui/components/views/McpStatus.js.map +1 -1
  342. package/dist/src/ui/components/views/McpStatus.test.js +0 -5
  343. package/dist/src/ui/components/views/McpStatus.test.js.map +1 -1
  344. package/dist/src/ui/components/views/ToolsList.test.js +4 -4
  345. package/dist/src/ui/components/views/ToolsList.test.js.map +1 -1
  346. package/dist/src/ui/contexts/KeypressContext.d.ts +1 -0
  347. package/dist/src/ui/contexts/KeypressContext.js +176 -50
  348. package/dist/src/ui/contexts/KeypressContext.js.map +1 -1
  349. package/dist/src/ui/contexts/KeypressContext.test.js +413 -14
  350. package/dist/src/ui/contexts/KeypressContext.test.js.map +1 -1
  351. package/dist/src/ui/contexts/SessionContext.test.d.ts +6 -0
  352. package/dist/src/ui/contexts/SessionContext.test.js +177 -0
  353. package/dist/src/ui/contexts/SessionContext.test.js.map +1 -0
  354. package/dist/src/ui/contexts/UIActionsContext.d.ts +5 -4
  355. package/dist/src/ui/contexts/UIActionsContext.js.map +1 -1
  356. package/dist/src/ui/contexts/UIStateContext.d.ts +3 -3
  357. package/dist/src/ui/contexts/UIStateContext.js.map +1 -1
  358. package/dist/src/ui/hooks/slashCommandProcessor.test.d.ts +6 -0
  359. package/dist/src/ui/hooks/slashCommandProcessor.test.js +779 -0
  360. package/dist/src/ui/hooks/slashCommandProcessor.test.js.map +1 -0
  361. package/dist/src/ui/hooks/useAtCompletion.js +2 -2
  362. package/dist/src/ui/hooks/useAtCompletion.js.map +1 -1
  363. package/dist/src/ui/hooks/useAtCompletion.test.d.ts +6 -0
  364. package/dist/src/ui/hooks/useAtCompletion.test.js +385 -0
  365. package/dist/src/ui/hooks/useAtCompletion.test.js.map +1 -0
  366. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js +0 -1
  367. package/dist/src/ui/hooks/useAutoAcceptIndicator.test.js.map +1 -1
  368. package/dist/src/ui/hooks/useCommandCompletion.d.ts +1 -1
  369. package/dist/src/ui/hooks/useCommandCompletion.js +5 -3
  370. package/dist/src/ui/hooks/useCommandCompletion.js.map +1 -1
  371. package/dist/src/ui/hooks/useCommandCompletion.test.d.ts +6 -0
  372. package/dist/src/ui/hooks/useCommandCompletion.test.js +375 -0
  373. package/dist/src/ui/hooks/useCommandCompletion.test.js.map +1 -0
  374. package/dist/src/ui/hooks/useConsoleMessages.test.d.ts +6 -0
  375. package/dist/src/ui/hooks/useConsoleMessages.test.js +110 -0
  376. package/dist/src/ui/hooks/useConsoleMessages.test.js.map +1 -0
  377. package/dist/src/ui/hooks/useExtensionUpdates.d.ts +2 -1
  378. package/dist/src/ui/hooks/useExtensionUpdates.js +5 -3
  379. package/dist/src/ui/hooks/useExtensionUpdates.js.map +1 -1
  380. package/dist/src/ui/hooks/useExtensionUpdates.test.js +22 -13
  381. package/dist/src/ui/hooks/useExtensionUpdates.test.js.map +1 -1
  382. package/dist/src/ui/hooks/useFocus.test.d.ts +6 -0
  383. package/dist/src/ui/hooks/useFocus.test.js +115 -0
  384. package/dist/src/ui/hooks/useFocus.test.js.map +1 -0
  385. package/dist/src/ui/hooks/useFolderTrust.test.d.ts +6 -0
  386. package/dist/src/ui/hooks/useFolderTrust.test.js +164 -0
  387. package/dist/src/ui/hooks/useFolderTrust.test.js.map +1 -0
  388. package/dist/src/ui/hooks/useGeminiStream.js +33 -31
  389. package/dist/src/ui/hooks/useGeminiStream.js.map +1 -1
  390. package/dist/src/ui/hooks/useGeminiStream.test.d.ts +6 -0
  391. package/dist/src/ui/hooks/useGeminiStream.test.js +1936 -0
  392. package/dist/src/ui/hooks/useGeminiStream.test.js.map +1 -0
  393. package/dist/src/ui/hooks/useKeypress.test.d.ts +6 -0
  394. package/dist/src/ui/hooks/useKeypress.test.js +234 -0
  395. package/dist/src/ui/hooks/useKeypress.test.js.map +1 -0
  396. package/dist/src/ui/hooks/useLoadingIndicator.test.js +5 -0
  397. package/dist/src/ui/hooks/useLoadingIndicator.test.js.map +1 -1
  398. package/dist/src/ui/hooks/useMessageQueue.d.ts +1 -0
  399. package/dist/src/ui/hooks/useMessageQueue.js +14 -0
  400. package/dist/src/ui/hooks/useMessageQueue.js.map +1 -1
  401. package/dist/src/ui/hooks/useMessageQueue.test.js +121 -0
  402. package/dist/src/ui/hooks/useMessageQueue.test.js.map +1 -1
  403. package/dist/src/ui/hooks/usePhraseCycler.d.ts +1 -0
  404. package/dist/src/ui/hooks/usePhraseCycler.js +156 -5
  405. package/dist/src/ui/hooks/usePhraseCycler.js.map +1 -1
  406. package/dist/src/ui/hooks/usePhraseCycler.test.d.ts +6 -0
  407. package/dist/src/ui/hooks/usePhraseCycler.test.js +155 -0
  408. package/dist/src/ui/hooks/usePhraseCycler.test.js.map +1 -0
  409. package/dist/src/ui/hooks/useThemeCommand.d.ts +2 -1
  410. package/dist/src/ui/hooks/useThemeCommand.js +6 -0
  411. package/dist/src/ui/hooks/useThemeCommand.js.map +1 -1
  412. package/dist/src/ui/hooks/useToolScheduler.test.js +3 -0
  413. package/dist/src/ui/hooks/useToolScheduler.test.js.map +1 -1
  414. package/dist/src/ui/hooks/vim.test.d.ts +6 -0
  415. package/dist/src/ui/hooks/vim.test.js +1389 -0
  416. package/dist/src/ui/hooks/vim.test.js.map +1 -0
  417. package/dist/src/ui/keyMatchers.test.js +9 -3
  418. package/dist/src/ui/keyMatchers.test.js.map +1 -1
  419. package/dist/src/ui/themes/theme.test.d.ts +6 -0
  420. package/dist/src/ui/themes/theme.test.js +85 -0
  421. package/dist/src/ui/themes/theme.test.js.map +1 -0
  422. package/dist/src/ui/types.d.ts +0 -1
  423. package/dist/src/ui/types.js.map +1 -1
  424. package/dist/src/ui/utils/CodeColorizer.d.ts +1 -1
  425. package/dist/src/ui/utils/CodeColorizer.js +4 -2
  426. package/dist/src/ui/utils/CodeColorizer.js.map +1 -1
  427. package/dist/src/ui/utils/MarkdownDisplay.d.ts +1 -0
  428. package/dist/src/ui/utils/MarkdownDisplay.js +8 -1
  429. package/dist/src/ui/utils/MarkdownDisplay.js.map +1 -1
  430. package/dist/src/ui/utils/commandUtils.js +18 -2
  431. package/dist/src/ui/utils/commandUtils.js.map +1 -1
  432. package/dist/src/ui/utils/commandUtils.test.js +61 -6
  433. package/dist/src/ui/utils/commandUtils.test.js.map +1 -1
  434. package/dist/src/ui/utils/computeStats.js +5 -2
  435. package/dist/src/ui/utils/computeStats.js.map +1 -1
  436. package/dist/src/ui/utils/computeStats.test.d.ts +6 -0
  437. package/dist/src/ui/utils/computeStats.test.js +262 -0
  438. package/dist/src/ui/utils/computeStats.test.js.map +1 -0
  439. package/dist/src/ui/utils/updateCheck.d.ts +2 -1
  440. package/dist/src/ui/utils/updateCheck.js +4 -1
  441. package/dist/src/ui/utils/updateCheck.js.map +1 -1
  442. package/dist/src/ui/utils/updateCheck.test.js +25 -10
  443. package/dist/src/ui/utils/updateCheck.test.js.map +1 -1
  444. package/dist/src/utils/cleanup.test.d.ts +6 -0
  445. package/dist/src/utils/cleanup.test.js +49 -0
  446. package/dist/src/utils/cleanup.test.js.map +1 -0
  447. package/dist/src/utils/errors.d.ts +1 -0
  448. package/dist/src/utils/errors.js +66 -5
  449. package/dist/src/utils/errors.js.map +1 -1
  450. package/dist/src/utils/handleAutoUpdate.test.d.ts +6 -0
  451. package/dist/src/utils/handleAutoUpdate.test.js +225 -0
  452. package/dist/src/utils/handleAutoUpdate.test.js.map +1 -0
  453. package/dist/src/utils/sandbox-macos-permissive-open.sb +3 -1
  454. package/dist/src/utils/startupWarnings.test.d.ts +6 -0
  455. package/dist/src/utils/startupWarnings.test.js +61 -0
  456. package/dist/src/utils/startupWarnings.test.js.map +1 -0
  457. package/dist/src/validateNonInterActiveAuth.js +2 -2
  458. package/dist/src/validateNonInterActiveAuth.js.map +1 -1
  459. package/dist/src/validateNonInterActiveAuth.test.d.ts +6 -0
  460. package/dist/src/validateNonInterActiveAuth.test.js +336 -0
  461. package/dist/src/validateNonInterActiveAuth.test.js.map +1 -0
  462. package/dist/src/zed-integration/zedIntegration.js +1 -3
  463. package/dist/src/zed-integration/zedIntegration.js.map +1 -1
  464. package/dist/tsconfig.tsbuildinfo +1 -1
  465. package/package.json +3 -3
  466. package/dist/google-gemini-cli-0.10.0-preview.2.tgz +0 -0
  467. package/dist/src/ui/components/WorkspaceMigrationDialog.d.ts +0 -11
  468. package/dist/src/ui/components/WorkspaceMigrationDialog.js +0 -44
  469. package/dist/src/ui/components/WorkspaceMigrationDialog.js.map +0 -1
  470. package/dist/src/ui/hooks/useWorkspaceMigration.d.ts +0 -13
  471. package/dist/src/ui/hooks/useWorkspaceMigration.js +0 -59
  472. package/dist/src/ui/hooks/useWorkspaceMigration.js.map +0 -1
@@ -0,0 +1,1389 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025 Google LLC
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
7
+ import { renderHook, act } from '@testing-library/react';
8
+ import { useVim } from './vim.js';
9
+ import { textBufferReducer } from '../components/shared/text-buffer.js';
10
+ // Mock the VimModeContext
11
+ const mockVimContext = {
12
+ vimEnabled: true,
13
+ vimMode: 'NORMAL',
14
+ toggleVimEnabled: vi.fn(),
15
+ setVimMode: vi.fn(),
16
+ };
17
+ vi.mock('../contexts/VimModeContext.js', () => ({
18
+ useVimMode: () => mockVimContext,
19
+ VimModeProvider: ({ children }) => children,
20
+ }));
21
+ // Helper to create a full Key object from partial data
22
+ const createKey = (partial) => ({
23
+ name: partial.name || '',
24
+ sequence: partial.sequence || '',
25
+ ctrl: partial.ctrl || false,
26
+ meta: partial.meta || false,
27
+ shift: partial.shift || false,
28
+ paste: partial.paste || false,
29
+ ...partial,
30
+ });
31
+ const createMockTextBufferState = (partial) => {
32
+ const lines = partial.lines || [''];
33
+ return {
34
+ lines,
35
+ cursorRow: 0,
36
+ cursorCol: 0,
37
+ preferredCol: null,
38
+ undoStack: [],
39
+ redoStack: [],
40
+ clipboard: null,
41
+ selectionAnchor: null,
42
+ viewportWidth: 80,
43
+ viewportHeight: 24,
44
+ visualLayout: {
45
+ visualLines: lines,
46
+ logicalToVisualMap: lines.map((_, i) => [[i, 0]]),
47
+ visualToLogicalMap: lines.map((_, i) => [i, 0]),
48
+ },
49
+ ...partial,
50
+ };
51
+ };
52
+ // Test constants
53
+ const TEST_SEQUENCES = {
54
+ ESCAPE: createKey({ sequence: '\u001b', name: 'escape' }),
55
+ LEFT: createKey({ sequence: 'h' }),
56
+ RIGHT: createKey({ sequence: 'l' }),
57
+ UP: createKey({ sequence: 'k' }),
58
+ DOWN: createKey({ sequence: 'j' }),
59
+ INSERT: createKey({ sequence: 'i' }),
60
+ APPEND: createKey({ sequence: 'a' }),
61
+ DELETE_CHAR: createKey({ sequence: 'x' }),
62
+ DELETE: createKey({ sequence: 'd' }),
63
+ CHANGE: createKey({ sequence: 'c' }),
64
+ WORD_FORWARD: createKey({ sequence: 'w' }),
65
+ WORD_BACKWARD: createKey({ sequence: 'b' }),
66
+ WORD_END: createKey({ sequence: 'e' }),
67
+ LINE_START: createKey({ sequence: '0' }),
68
+ LINE_END: createKey({ sequence: '$' }),
69
+ REPEAT: createKey({ sequence: '.' }),
70
+ };
71
+ describe('useVim hook', () => {
72
+ let mockBuffer;
73
+ let mockHandleFinalSubmit;
74
+ const createMockBuffer = (text = 'hello world', cursor = [0, 5]) => {
75
+ const cursorState = { pos: cursor };
76
+ const lines = text.split('\n');
77
+ return {
78
+ lines,
79
+ get cursor() {
80
+ return cursorState.pos;
81
+ },
82
+ set cursor(newPos) {
83
+ cursorState.pos = newPos;
84
+ },
85
+ text,
86
+ move: vi.fn().mockImplementation((direction) => {
87
+ let [row, col] = cursorState.pos;
88
+ const line = lines[row] || '';
89
+ if (direction === 'left') {
90
+ col = Math.max(0, col - 1);
91
+ }
92
+ else if (direction === 'right') {
93
+ col = Math.min(line.length, col + 1);
94
+ }
95
+ else if (direction === 'home') {
96
+ col = 0;
97
+ }
98
+ else if (direction === 'end') {
99
+ col = line.length;
100
+ }
101
+ cursorState.pos = [row, col];
102
+ }),
103
+ del: vi.fn(),
104
+ moveToOffset: vi.fn(),
105
+ insert: vi.fn(),
106
+ newline: vi.fn(),
107
+ replaceRangeByOffset: vi.fn(),
108
+ handleInput: vi.fn(),
109
+ setText: vi.fn(),
110
+ // Vim-specific methods
111
+ vimDeleteWordForward: vi.fn(),
112
+ vimDeleteWordBackward: vi.fn(),
113
+ vimDeleteWordEnd: vi.fn(),
114
+ vimChangeWordForward: vi.fn(),
115
+ vimChangeWordBackward: vi.fn(),
116
+ vimChangeWordEnd: vi.fn(),
117
+ vimDeleteLine: vi.fn(),
118
+ vimChangeLine: vi.fn(),
119
+ vimDeleteToEndOfLine: vi.fn(),
120
+ vimChangeToEndOfLine: vi.fn(),
121
+ vimChangeMovement: vi.fn(),
122
+ vimMoveLeft: vi.fn(),
123
+ vimMoveRight: vi.fn(),
124
+ vimMoveUp: vi.fn(),
125
+ vimMoveDown: vi.fn(),
126
+ vimMoveWordForward: vi.fn(),
127
+ vimMoveWordBackward: vi.fn(),
128
+ vimMoveWordEnd: vi.fn(),
129
+ vimDeleteChar: vi.fn(),
130
+ vimInsertAtCursor: vi.fn(),
131
+ vimAppendAtCursor: vi.fn().mockImplementation(() => {
132
+ // Append moves cursor right (vim 'a' behavior - position after current char)
133
+ const [row, col] = cursorState.pos;
134
+ // In vim, 'a' moves cursor to position after current character
135
+ // This allows inserting at the end of the line
136
+ cursorState.pos = [row, col + 1];
137
+ }),
138
+ vimOpenLineBelow: vi.fn(),
139
+ vimOpenLineAbove: vi.fn(),
140
+ vimAppendAtLineEnd: vi.fn(),
141
+ vimInsertAtLineStart: vi.fn(),
142
+ vimMoveToLineStart: vi.fn(),
143
+ vimMoveToLineEnd: vi.fn(),
144
+ vimMoveToFirstNonWhitespace: vi.fn(),
145
+ vimMoveToFirstLine: vi.fn(),
146
+ vimMoveToLastLine: vi.fn(),
147
+ vimMoveToLine: vi.fn(),
148
+ vimEscapeInsertMode: vi.fn().mockImplementation(() => {
149
+ // Escape moves cursor left unless at beginning of line
150
+ const [row, col] = cursorState.pos;
151
+ if (col > 0) {
152
+ cursorState.pos = [row, col - 1];
153
+ }
154
+ }),
155
+ };
156
+ };
157
+ const renderVimHook = (buffer) => renderHook(() => useVim((buffer || mockBuffer), mockHandleFinalSubmit));
158
+ const exitInsertMode = (result) => {
159
+ act(() => {
160
+ result.current.handleInput(TEST_SEQUENCES.ESCAPE);
161
+ });
162
+ };
163
+ beforeEach(() => {
164
+ vi.clearAllMocks();
165
+ mockHandleFinalSubmit = vi.fn();
166
+ mockBuffer = createMockBuffer();
167
+ // Reset mock context to default state
168
+ mockVimContext.vimEnabled = true;
169
+ mockVimContext.vimMode = 'NORMAL';
170
+ mockVimContext.toggleVimEnabled.mockClear();
171
+ mockVimContext.setVimMode.mockClear();
172
+ });
173
+ describe('Mode switching', () => {
174
+ it('should start in NORMAL mode', () => {
175
+ const { result } = renderVimHook();
176
+ expect(result.current.mode).toBe('NORMAL');
177
+ });
178
+ it('should switch to INSERT mode with i command', () => {
179
+ const { result } = renderVimHook();
180
+ act(() => {
181
+ result.current.handleInput(TEST_SEQUENCES.INSERT);
182
+ });
183
+ expect(result.current.mode).toBe('INSERT');
184
+ expect(mockVimContext.setVimMode).toHaveBeenCalledWith('INSERT');
185
+ });
186
+ it('should switch back to NORMAL mode with Escape', () => {
187
+ const { result } = renderVimHook();
188
+ act(() => {
189
+ result.current.handleInput(TEST_SEQUENCES.INSERT);
190
+ });
191
+ expect(result.current.mode).toBe('INSERT');
192
+ exitInsertMode(result);
193
+ expect(result.current.mode).toBe('NORMAL');
194
+ });
195
+ it('should properly handle escape followed immediately by a command', () => {
196
+ const testBuffer = createMockBuffer('hello world test', [0, 6]);
197
+ const { result } = renderVimHook(testBuffer);
198
+ act(() => {
199
+ result.current.handleInput(createKey({ sequence: 'i' }));
200
+ });
201
+ expect(result.current.mode).toBe('INSERT');
202
+ vi.clearAllMocks();
203
+ exitInsertMode(result);
204
+ expect(result.current.mode).toBe('NORMAL');
205
+ act(() => {
206
+ result.current.handleInput(createKey({ sequence: 'b' }));
207
+ });
208
+ expect(testBuffer.vimMoveWordBackward).toHaveBeenCalledWith(1);
209
+ });
210
+ });
211
+ describe('Navigation commands', () => {
212
+ it('should handle h (left movement)', () => {
213
+ const { result } = renderVimHook();
214
+ act(() => {
215
+ result.current.handleInput(createKey({ sequence: 'h' }));
216
+ });
217
+ expect(mockBuffer.vimMoveLeft).toHaveBeenCalledWith(1);
218
+ });
219
+ it('should handle l (right movement)', () => {
220
+ const { result } = renderVimHook();
221
+ act(() => {
222
+ result.current.handleInput(createKey({ sequence: 'l' }));
223
+ });
224
+ expect(mockBuffer.vimMoveRight).toHaveBeenCalledWith(1);
225
+ });
226
+ it('should handle j (down movement)', () => {
227
+ const testBuffer = createMockBuffer('first line\nsecond line');
228
+ const { result } = renderVimHook(testBuffer);
229
+ act(() => {
230
+ result.current.handleInput(createKey({ sequence: 'j' }));
231
+ });
232
+ expect(testBuffer.vimMoveDown).toHaveBeenCalledWith(1);
233
+ });
234
+ it('should handle k (up movement)', () => {
235
+ const testBuffer = createMockBuffer('first line\nsecond line');
236
+ const { result } = renderVimHook(testBuffer);
237
+ act(() => {
238
+ result.current.handleInput(createKey({ sequence: 'k' }));
239
+ });
240
+ expect(testBuffer.vimMoveUp).toHaveBeenCalledWith(1);
241
+ });
242
+ it('should handle 0 (move to start of line)', () => {
243
+ const { result } = renderVimHook();
244
+ act(() => {
245
+ result.current.handleInput(createKey({ sequence: '0' }));
246
+ });
247
+ expect(mockBuffer.vimMoveToLineStart).toHaveBeenCalled();
248
+ });
249
+ it('should handle $ (move to end of line)', () => {
250
+ const { result } = renderVimHook();
251
+ act(() => {
252
+ result.current.handleInput(createKey({ sequence: '$' }));
253
+ });
254
+ expect(mockBuffer.vimMoveToLineEnd).toHaveBeenCalled();
255
+ });
256
+ });
257
+ describe('Mode switching commands', () => {
258
+ it('should handle a (append after cursor)', () => {
259
+ const { result } = renderVimHook();
260
+ act(() => {
261
+ result.current.handleInput(createKey({ sequence: 'a' }));
262
+ });
263
+ expect(mockBuffer.vimAppendAtCursor).toHaveBeenCalled();
264
+ expect(result.current.mode).toBe('INSERT');
265
+ });
266
+ it('should handle A (append at end of line)', () => {
267
+ const { result } = renderVimHook();
268
+ act(() => {
269
+ result.current.handleInput(createKey({ sequence: 'A' }));
270
+ });
271
+ expect(mockBuffer.vimAppendAtLineEnd).toHaveBeenCalled();
272
+ expect(result.current.mode).toBe('INSERT');
273
+ });
274
+ it('should handle o (open line below)', () => {
275
+ const { result } = renderVimHook();
276
+ act(() => {
277
+ result.current.handleInput(createKey({ sequence: 'o' }));
278
+ });
279
+ expect(mockBuffer.vimOpenLineBelow).toHaveBeenCalled();
280
+ expect(result.current.mode).toBe('INSERT');
281
+ });
282
+ it('should handle O (open line above)', () => {
283
+ const { result } = renderVimHook();
284
+ act(() => {
285
+ result.current.handleInput(createKey({ sequence: 'O' }));
286
+ });
287
+ expect(mockBuffer.vimOpenLineAbove).toHaveBeenCalled();
288
+ expect(result.current.mode).toBe('INSERT');
289
+ });
290
+ });
291
+ describe('Edit commands', () => {
292
+ it('should handle x (delete character)', () => {
293
+ const { result } = renderVimHook();
294
+ vi.clearAllMocks();
295
+ act(() => {
296
+ result.current.handleInput(createKey({ sequence: 'x' }));
297
+ });
298
+ expect(mockBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
299
+ });
300
+ it('should move cursor left when deleting last character on line (vim behavior)', () => {
301
+ const testBuffer = createMockBuffer('hello', [0, 4]);
302
+ const { result } = renderVimHook(testBuffer);
303
+ act(() => {
304
+ result.current.handleInput(createKey({ sequence: 'x' }));
305
+ });
306
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
307
+ });
308
+ it('should handle first d key (sets pending state)', () => {
309
+ const { result } = renderVimHook();
310
+ act(() => {
311
+ result.current.handleInput(createKey({ sequence: 'd' }));
312
+ });
313
+ expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
314
+ });
315
+ });
316
+ describe('Count handling', () => {
317
+ it('should handle count input and return to count 0 after command', () => {
318
+ const { result } = renderVimHook();
319
+ act(() => {
320
+ const handled = result.current.handleInput(createKey({ sequence: '3' }));
321
+ expect(handled).toBe(true);
322
+ });
323
+ act(() => {
324
+ const handled = result.current.handleInput(createKey({ sequence: 'h' }));
325
+ expect(handled).toBe(true);
326
+ });
327
+ expect(mockBuffer.vimMoveLeft).toHaveBeenCalledWith(3);
328
+ });
329
+ it('should only delete 1 character with x command when no count is specified', () => {
330
+ const testBuffer = createMockBuffer();
331
+ const { result } = renderVimHook(testBuffer);
332
+ act(() => {
333
+ result.current.handleInput(createKey({ sequence: 'x' }));
334
+ });
335
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
336
+ });
337
+ });
338
+ describe('Word movement', () => {
339
+ it('should properly initialize vim hook with word movement support', () => {
340
+ const testBuffer = createMockBuffer('cat elephant mouse', [0, 0]);
341
+ const { result } = renderVimHook(testBuffer);
342
+ expect(result.current.vimModeEnabled).toBe(true);
343
+ expect(result.current.mode).toBe('NORMAL');
344
+ expect(result.current.handleInput).toBeDefined();
345
+ });
346
+ it('should support vim mode and basic operations across multiple lines', () => {
347
+ const testBuffer = createMockBuffer('first line word\nsecond line word', [0, 11]);
348
+ const { result } = renderVimHook(testBuffer);
349
+ expect(result.current.vimModeEnabled).toBe(true);
350
+ expect(result.current.mode).toBe('NORMAL');
351
+ expect(result.current.handleInput).toBeDefined();
352
+ expect(testBuffer.replaceRangeByOffset).toBeDefined();
353
+ expect(testBuffer.moveToOffset).toBeDefined();
354
+ });
355
+ it('should handle w (next word)', () => {
356
+ const testBuffer = createMockBuffer('hello world test');
357
+ const { result } = renderVimHook(testBuffer);
358
+ act(() => {
359
+ result.current.handleInput(createKey({ sequence: 'w' }));
360
+ });
361
+ expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
362
+ });
363
+ it('should handle b (previous word)', () => {
364
+ const testBuffer = createMockBuffer('hello world test', [0, 6]);
365
+ const { result } = renderVimHook(testBuffer);
366
+ act(() => {
367
+ result.current.handleInput(createKey({ sequence: 'b' }));
368
+ });
369
+ expect(testBuffer.vimMoveWordBackward).toHaveBeenCalledWith(1);
370
+ });
371
+ it('should handle e (end of word)', () => {
372
+ const testBuffer = createMockBuffer('hello world test');
373
+ const { result } = renderVimHook(testBuffer);
374
+ act(() => {
375
+ result.current.handleInput(createKey({ sequence: 'e' }));
376
+ });
377
+ expect(testBuffer.vimMoveWordEnd).toHaveBeenCalledWith(1);
378
+ });
379
+ it('should handle w when cursor is on the last word', () => {
380
+ const testBuffer = createMockBuffer('hello world', [0, 8]);
381
+ const { result } = renderVimHook(testBuffer);
382
+ act(() => {
383
+ result.current.handleInput(createKey({ sequence: 'w' }));
384
+ });
385
+ expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
386
+ });
387
+ it('should handle first c key (sets pending change state)', () => {
388
+ const { result } = renderVimHook();
389
+ act(() => {
390
+ result.current.handleInput(createKey({ sequence: 'c' }));
391
+ });
392
+ expect(result.current.mode).toBe('NORMAL');
393
+ expect(mockBuffer.del).not.toHaveBeenCalled();
394
+ });
395
+ it('should clear pending state on invalid command sequence (df)', () => {
396
+ const { result } = renderVimHook();
397
+ act(() => {
398
+ result.current.handleInput(createKey({ sequence: 'd' }));
399
+ result.current.handleInput(createKey({ sequence: 'f' }));
400
+ });
401
+ expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
402
+ expect(mockBuffer.del).not.toHaveBeenCalled();
403
+ });
404
+ it('should clear pending state with Escape in NORMAL mode', () => {
405
+ const { result } = renderVimHook();
406
+ act(() => {
407
+ result.current.handleInput(createKey({ sequence: 'd' }));
408
+ });
409
+ exitInsertMode(result);
410
+ expect(mockBuffer.replaceRangeByOffset).not.toHaveBeenCalled();
411
+ });
412
+ });
413
+ describe('Disabled vim mode', () => {
414
+ it('should not respond to vim commands when disabled', () => {
415
+ mockVimContext.vimEnabled = false;
416
+ const { result } = renderVimHook(mockBuffer);
417
+ act(() => {
418
+ result.current.handleInput(createKey({ sequence: 'h' }));
419
+ });
420
+ expect(mockBuffer.move).not.toHaveBeenCalled();
421
+ });
422
+ });
423
+ // These tests are no longer applicable at the hook level
424
+ describe('Command repeat system', () => {
425
+ it('should repeat x command from current cursor position', () => {
426
+ const testBuffer = createMockBuffer('abcd\nefgh\nijkl', [0, 1]);
427
+ const { result } = renderVimHook(testBuffer);
428
+ act(() => {
429
+ result.current.handleInput(createKey({ sequence: 'x' }));
430
+ });
431
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
432
+ testBuffer.cursor = [1, 2];
433
+ act(() => {
434
+ result.current.handleInput(createKey({ sequence: '.' }));
435
+ });
436
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
437
+ });
438
+ it('should repeat dd command from current position', () => {
439
+ const testBuffer = createMockBuffer('line1\nline2\nline3', [1, 0]);
440
+ const { result } = renderVimHook(testBuffer);
441
+ act(() => {
442
+ result.current.handleInput(createKey({ sequence: 'd' }));
443
+ });
444
+ act(() => {
445
+ result.current.handleInput(createKey({ sequence: 'd' }));
446
+ });
447
+ expect(testBuffer.vimDeleteLine).toHaveBeenCalledTimes(1);
448
+ testBuffer.cursor = [0, 0];
449
+ act(() => {
450
+ result.current.handleInput(createKey({ sequence: '.' }));
451
+ });
452
+ expect(testBuffer.vimDeleteLine).toHaveBeenCalledTimes(2);
453
+ });
454
+ it('should repeat ce command from current position', () => {
455
+ const testBuffer = createMockBuffer('word', [0, 0]);
456
+ const { result } = renderVimHook(testBuffer);
457
+ act(() => {
458
+ result.current.handleInput(createKey({ sequence: 'c' }));
459
+ });
460
+ act(() => {
461
+ result.current.handleInput(createKey({ sequence: 'e' }));
462
+ });
463
+ expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledTimes(1);
464
+ // Exit INSERT mode to complete the command
465
+ exitInsertMode(result);
466
+ testBuffer.cursor = [0, 2];
467
+ act(() => {
468
+ result.current.handleInput(createKey({ sequence: '.' }));
469
+ });
470
+ expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledTimes(2);
471
+ });
472
+ it('should repeat cc command from current position', () => {
473
+ const testBuffer = createMockBuffer('line1\nline2\nline3', [1, 2]);
474
+ const { result } = renderVimHook(testBuffer);
475
+ act(() => {
476
+ result.current.handleInput(createKey({ sequence: 'c' }));
477
+ });
478
+ act(() => {
479
+ result.current.handleInput(createKey({ sequence: 'c' }));
480
+ });
481
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledTimes(1);
482
+ // Exit INSERT mode to complete the command
483
+ exitInsertMode(result);
484
+ testBuffer.cursor = [0, 1];
485
+ act(() => {
486
+ result.current.handleInput(createKey({ sequence: '.' }));
487
+ });
488
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledTimes(2);
489
+ });
490
+ it('should repeat cw command from current position', () => {
491
+ const testBuffer = createMockBuffer('hello world test', [0, 6]);
492
+ const { result } = renderVimHook(testBuffer);
493
+ act(() => {
494
+ result.current.handleInput(createKey({ sequence: 'c' }));
495
+ });
496
+ act(() => {
497
+ result.current.handleInput(createKey({ sequence: 'w' }));
498
+ });
499
+ expect(testBuffer.vimChangeWordForward).toHaveBeenCalledTimes(1);
500
+ // Exit INSERT mode to complete the command
501
+ exitInsertMode(result);
502
+ testBuffer.cursor = [0, 0];
503
+ act(() => {
504
+ result.current.handleInput(createKey({ sequence: '.' }));
505
+ });
506
+ expect(testBuffer.vimChangeWordForward).toHaveBeenCalledTimes(2);
507
+ });
508
+ it('should repeat D command from current position', () => {
509
+ const testBuffer = createMockBuffer('hello world test', [0, 6]);
510
+ const { result } = renderVimHook(testBuffer);
511
+ act(() => {
512
+ result.current.handleInput(createKey({ sequence: 'D' }));
513
+ });
514
+ expect(testBuffer.vimDeleteToEndOfLine).toHaveBeenCalledTimes(1);
515
+ testBuffer.cursor = [0, 2];
516
+ vi.clearAllMocks(); // Clear all mocks instead of just one method
517
+ act(() => {
518
+ result.current.handleInput(createKey({ sequence: '.' }));
519
+ });
520
+ expect(testBuffer.vimDeleteToEndOfLine).toHaveBeenCalledTimes(1);
521
+ });
522
+ it('should repeat C command from current position', () => {
523
+ const testBuffer = createMockBuffer('hello world test', [0, 6]);
524
+ const { result } = renderVimHook(testBuffer);
525
+ act(() => {
526
+ result.current.handleInput(createKey({ sequence: 'C' }));
527
+ });
528
+ expect(testBuffer.vimChangeToEndOfLine).toHaveBeenCalledTimes(1);
529
+ // Exit INSERT mode to complete the command
530
+ exitInsertMode(result);
531
+ testBuffer.cursor = [0, 2];
532
+ act(() => {
533
+ result.current.handleInput(createKey({ sequence: '.' }));
534
+ });
535
+ expect(testBuffer.vimChangeToEndOfLine).toHaveBeenCalledTimes(2);
536
+ });
537
+ it('should repeat command after cursor movement', () => {
538
+ const testBuffer = createMockBuffer('test text', [0, 0]);
539
+ const { result } = renderVimHook(testBuffer);
540
+ act(() => {
541
+ result.current.handleInput(createKey({ sequence: 'x' }));
542
+ });
543
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
544
+ testBuffer.cursor = [0, 2];
545
+ act(() => {
546
+ result.current.handleInput(createKey({ sequence: '.' }));
547
+ });
548
+ expect(testBuffer.vimDeleteChar).toHaveBeenCalledWith(1);
549
+ });
550
+ it('should move cursor to the correct position after exiting INSERT mode with "a"', () => {
551
+ const testBuffer = createMockBuffer('hello world', [0, 10]);
552
+ const { result } = renderVimHook(testBuffer);
553
+ act(() => {
554
+ result.current.handleInput(createKey({ sequence: 'a' }));
555
+ });
556
+ expect(result.current.mode).toBe('INSERT');
557
+ expect(testBuffer.cursor).toEqual([0, 11]);
558
+ exitInsertMode(result);
559
+ expect(result.current.mode).toBe('NORMAL');
560
+ expect(testBuffer.cursor).toEqual([0, 10]);
561
+ });
562
+ });
563
+ describe('Special characters and edge cases', () => {
564
+ it('should handle ^ (move to first non-whitespace character)', () => {
565
+ const testBuffer = createMockBuffer(' hello world', [0, 5]);
566
+ const { result } = renderVimHook(testBuffer);
567
+ act(() => {
568
+ result.current.handleInput(createKey({ sequence: '^' }));
569
+ });
570
+ expect(testBuffer.vimMoveToFirstNonWhitespace).toHaveBeenCalled();
571
+ });
572
+ it('should handle G without count (go to last line)', () => {
573
+ const testBuffer = createMockBuffer('line1\nline2\nline3', [0, 0]);
574
+ const { result } = renderVimHook(testBuffer);
575
+ act(() => {
576
+ result.current.handleInput(createKey({ sequence: 'G' }));
577
+ });
578
+ expect(testBuffer.vimMoveToLastLine).toHaveBeenCalled();
579
+ });
580
+ it('should handle gg (go to first line)', () => {
581
+ const testBuffer = createMockBuffer('line1\nline2\nline3', [2, 0]);
582
+ const { result } = renderVimHook(testBuffer);
583
+ // First 'g' sets pending state
584
+ act(() => {
585
+ result.current.handleInput(createKey({ sequence: 'g' }));
586
+ });
587
+ // Second 'g' executes the command
588
+ act(() => {
589
+ result.current.handleInput(createKey({ sequence: 'g' }));
590
+ });
591
+ expect(testBuffer.vimMoveToFirstLine).toHaveBeenCalled();
592
+ });
593
+ it('should handle count with movement commands', () => {
594
+ const testBuffer = createMockBuffer('hello world test', [0, 0]);
595
+ const { result } = renderVimHook(testBuffer);
596
+ act(() => {
597
+ result.current.handleInput(createKey({ sequence: '3' }));
598
+ });
599
+ act(() => {
600
+ result.current.handleInput(TEST_SEQUENCES.WORD_FORWARD);
601
+ });
602
+ expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(3);
603
+ });
604
+ });
605
+ describe('Vim word operations', () => {
606
+ describe('dw (delete word forward)', () => {
607
+ it('should delete from cursor to start of next word', () => {
608
+ const testBuffer = createMockBuffer('hello world test', [0, 0]);
609
+ const { result } = renderVimHook(testBuffer);
610
+ act(() => {
611
+ result.current.handleInput(createKey({ sequence: 'd' }));
612
+ });
613
+ act(() => {
614
+ result.current.handleInput(createKey({ sequence: 'w' }));
615
+ });
616
+ expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(1);
617
+ });
618
+ it('should actually delete the complete word including trailing space', () => {
619
+ // This test uses the real text-buffer reducer instead of mocks
620
+ const initialState = createMockTextBufferState({
621
+ lines: ['hello world test'],
622
+ cursorRow: 0,
623
+ cursorCol: 0,
624
+ preferredCol: null,
625
+ undoStack: [],
626
+ redoStack: [],
627
+ clipboard: null,
628
+ selectionAnchor: null,
629
+ });
630
+ const result = textBufferReducer(initialState, {
631
+ type: 'vim_delete_word_forward',
632
+ payload: { count: 1 },
633
+ });
634
+ // Should delete "hello " (word + space), leaving "world test"
635
+ expect(result.lines).toEqual(['world test']);
636
+ expect(result.cursorRow).toBe(0);
637
+ expect(result.cursorCol).toBe(0);
638
+ });
639
+ it('should delete word from middle of word correctly', () => {
640
+ const initialState = createMockTextBufferState({
641
+ lines: ['hello world test'],
642
+ cursorRow: 0,
643
+ cursorCol: 2, // cursor on 'l' in "hello"
644
+ preferredCol: null,
645
+ undoStack: [],
646
+ redoStack: [],
647
+ clipboard: null,
648
+ selectionAnchor: null,
649
+ });
650
+ const result = textBufferReducer(initialState, {
651
+ type: 'vim_delete_word_forward',
652
+ payload: { count: 1 },
653
+ });
654
+ // Should delete "llo " (rest of word + space), leaving "he world test"
655
+ expect(result.lines).toEqual(['heworld test']);
656
+ expect(result.cursorRow).toBe(0);
657
+ expect(result.cursorCol).toBe(2);
658
+ });
659
+ it('should handle dw at end of line', () => {
660
+ const initialState = createMockTextBufferState({
661
+ lines: ['hello world'],
662
+ cursorRow: 0,
663
+ cursorCol: 6, // cursor on 'w' in "world"
664
+ preferredCol: null,
665
+ undoStack: [],
666
+ redoStack: [],
667
+ clipboard: null,
668
+ selectionAnchor: null,
669
+ });
670
+ const result = textBufferReducer(initialState, {
671
+ type: 'vim_delete_word_forward',
672
+ payload: { count: 1 },
673
+ });
674
+ // Should delete "world" (no trailing space at end), leaving "hello "
675
+ expect(result.lines).toEqual(['hello ']);
676
+ expect(result.cursorRow).toBe(0);
677
+ expect(result.cursorCol).toBe(6);
678
+ });
679
+ it('should delete multiple words with count', () => {
680
+ const testBuffer = createMockBuffer('one two three four', [0, 0]);
681
+ const { result } = renderVimHook(testBuffer);
682
+ act(() => {
683
+ result.current.handleInput(createKey({ sequence: '2' }));
684
+ });
685
+ act(() => {
686
+ result.current.handleInput(createKey({ sequence: 'd' }));
687
+ });
688
+ act(() => {
689
+ result.current.handleInput(createKey({ sequence: 'w' }));
690
+ });
691
+ expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(2);
692
+ });
693
+ it('should record command for repeat with dot', () => {
694
+ const testBuffer = createMockBuffer('hello world test', [0, 0]);
695
+ const { result } = renderVimHook(testBuffer);
696
+ // Execute dw
697
+ act(() => {
698
+ result.current.handleInput(createKey({ sequence: 'd' }));
699
+ });
700
+ act(() => {
701
+ result.current.handleInput(createKey({ sequence: 'w' }));
702
+ });
703
+ vi.clearAllMocks();
704
+ // Execute dot repeat
705
+ act(() => {
706
+ result.current.handleInput(createKey({ sequence: '.' }));
707
+ });
708
+ expect(testBuffer.vimDeleteWordForward).toHaveBeenCalledWith(1);
709
+ });
710
+ });
711
+ describe('de (delete word end)', () => {
712
+ it('should delete from cursor to end of current word', () => {
713
+ const testBuffer = createMockBuffer('hello world test', [0, 1]);
714
+ const { result } = renderVimHook(testBuffer);
715
+ act(() => {
716
+ result.current.handleInput(createKey({ sequence: 'd' }));
717
+ });
718
+ act(() => {
719
+ result.current.handleInput(createKey({ sequence: 'e' }));
720
+ });
721
+ expect(testBuffer.vimDeleteWordEnd).toHaveBeenCalledWith(1);
722
+ });
723
+ it('should handle count with de', () => {
724
+ const testBuffer = createMockBuffer('one two three four', [0, 0]);
725
+ const { result } = renderVimHook(testBuffer);
726
+ act(() => {
727
+ result.current.handleInput(createKey({ sequence: '3' }));
728
+ });
729
+ act(() => {
730
+ result.current.handleInput(createKey({ sequence: 'd' }));
731
+ });
732
+ act(() => {
733
+ result.current.handleInput(createKey({ sequence: 'e' }));
734
+ });
735
+ expect(testBuffer.vimDeleteWordEnd).toHaveBeenCalledWith(3);
736
+ });
737
+ });
738
+ describe('cw (change word forward)', () => {
739
+ it('should change from cursor to start of next word and enter INSERT mode', () => {
740
+ const testBuffer = createMockBuffer('hello world test', [0, 0]);
741
+ const { result } = renderVimHook(testBuffer);
742
+ act(() => {
743
+ result.current.handleInput(createKey({ sequence: 'c' }));
744
+ });
745
+ act(() => {
746
+ result.current.handleInput(createKey({ sequence: 'w' }));
747
+ });
748
+ expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(1);
749
+ expect(result.current.mode).toBe('INSERT');
750
+ expect(mockVimContext.setVimMode).toHaveBeenCalledWith('INSERT');
751
+ });
752
+ it('should handle count with cw', () => {
753
+ const testBuffer = createMockBuffer('one two three four', [0, 0]);
754
+ const { result } = renderVimHook(testBuffer);
755
+ act(() => {
756
+ result.current.handleInput(createKey({ sequence: '2' }));
757
+ });
758
+ act(() => {
759
+ result.current.handleInput(createKey({ sequence: 'c' }));
760
+ });
761
+ act(() => {
762
+ result.current.handleInput(createKey({ sequence: 'w' }));
763
+ });
764
+ expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(2);
765
+ expect(result.current.mode).toBe('INSERT');
766
+ });
767
+ it('should be repeatable with dot', () => {
768
+ const testBuffer = createMockBuffer('hello world test more', [0, 0]);
769
+ const { result } = renderVimHook(testBuffer);
770
+ // Execute cw
771
+ act(() => {
772
+ result.current.handleInput(createKey({ sequence: 'c' }));
773
+ });
774
+ act(() => {
775
+ result.current.handleInput(createKey({ sequence: 'w' }));
776
+ });
777
+ // Exit INSERT mode
778
+ exitInsertMode(result);
779
+ vi.clearAllMocks();
780
+ mockVimContext.setVimMode.mockClear();
781
+ // Execute dot repeat
782
+ act(() => {
783
+ result.current.handleInput(createKey({ sequence: '.' }));
784
+ });
785
+ expect(testBuffer.vimChangeWordForward).toHaveBeenCalledWith(1);
786
+ expect(result.current.mode).toBe('INSERT');
787
+ });
788
+ });
789
+ describe('ce (change word end)', () => {
790
+ it('should change from cursor to end of word and enter INSERT mode', () => {
791
+ const testBuffer = createMockBuffer('hello world test', [0, 1]);
792
+ const { result } = renderVimHook(testBuffer);
793
+ act(() => {
794
+ result.current.handleInput(createKey({ sequence: 'c' }));
795
+ });
796
+ act(() => {
797
+ result.current.handleInput(createKey({ sequence: 'e' }));
798
+ });
799
+ expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledWith(1);
800
+ expect(result.current.mode).toBe('INSERT');
801
+ });
802
+ it('should handle count with ce', () => {
803
+ const testBuffer = createMockBuffer('one two three four', [0, 0]);
804
+ const { result } = renderVimHook(testBuffer);
805
+ act(() => {
806
+ result.current.handleInput(createKey({ sequence: '2' }));
807
+ });
808
+ act(() => {
809
+ result.current.handleInput(createKey({ sequence: 'c' }));
810
+ });
811
+ act(() => {
812
+ result.current.handleInput(createKey({ sequence: 'e' }));
813
+ });
814
+ expect(testBuffer.vimChangeWordEnd).toHaveBeenCalledWith(2);
815
+ expect(result.current.mode).toBe('INSERT');
816
+ });
817
+ });
818
+ describe('cc (change line)', () => {
819
+ it('should change entire line and enter INSERT mode', () => {
820
+ const testBuffer = createMockBuffer('hello world\nsecond line', [0, 5]);
821
+ const { result } = renderVimHook(testBuffer);
822
+ act(() => {
823
+ result.current.handleInput(createKey({ sequence: 'c' }));
824
+ });
825
+ act(() => {
826
+ result.current.handleInput(createKey({ sequence: 'c' }));
827
+ });
828
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
829
+ expect(result.current.mode).toBe('INSERT');
830
+ });
831
+ it('should change multiple lines with count', () => {
832
+ const testBuffer = createMockBuffer('line1\nline2\nline3\nline4', [1, 0]);
833
+ const { result } = renderVimHook(testBuffer);
834
+ act(() => {
835
+ result.current.handleInput(createKey({ sequence: '3' }));
836
+ });
837
+ act(() => {
838
+ result.current.handleInput(createKey({ sequence: 'c' }));
839
+ });
840
+ act(() => {
841
+ result.current.handleInput(createKey({ sequence: 'c' }));
842
+ });
843
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(3);
844
+ expect(result.current.mode).toBe('INSERT');
845
+ });
846
+ it('should be repeatable with dot', () => {
847
+ const testBuffer = createMockBuffer('line1\nline2\nline3', [0, 0]);
848
+ const { result } = renderVimHook(testBuffer);
849
+ // Execute cc
850
+ act(() => {
851
+ result.current.handleInput(createKey({ sequence: 'c' }));
852
+ });
853
+ act(() => {
854
+ result.current.handleInput(createKey({ sequence: 'c' }));
855
+ });
856
+ // Exit INSERT mode
857
+ exitInsertMode(result);
858
+ vi.clearAllMocks();
859
+ mockVimContext.setVimMode.mockClear();
860
+ // Execute dot repeat
861
+ act(() => {
862
+ result.current.handleInput(createKey({ sequence: '.' }));
863
+ });
864
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
865
+ expect(result.current.mode).toBe('INSERT');
866
+ });
867
+ });
868
+ describe('db (delete word backward)', () => {
869
+ it('should delete from cursor to start of previous word', () => {
870
+ const testBuffer = createMockBuffer('hello world test', [0, 11]);
871
+ const { result } = renderVimHook(testBuffer);
872
+ act(() => {
873
+ result.current.handleInput(createKey({ sequence: 'd' }));
874
+ });
875
+ act(() => {
876
+ result.current.handleInput(createKey({ sequence: 'b' }));
877
+ });
878
+ expect(testBuffer.vimDeleteWordBackward).toHaveBeenCalledWith(1);
879
+ });
880
+ it('should handle count with db', () => {
881
+ const testBuffer = createMockBuffer('one two three four', [0, 18]);
882
+ const { result } = renderVimHook(testBuffer);
883
+ act(() => {
884
+ result.current.handleInput(createKey({ sequence: '2' }));
885
+ });
886
+ act(() => {
887
+ result.current.handleInput(createKey({ sequence: 'd' }));
888
+ });
889
+ act(() => {
890
+ result.current.handleInput(createKey({ sequence: 'b' }));
891
+ });
892
+ expect(testBuffer.vimDeleteWordBackward).toHaveBeenCalledWith(2);
893
+ });
894
+ });
895
+ describe('cb (change word backward)', () => {
896
+ it('should change from cursor to start of previous word and enter INSERT mode', () => {
897
+ const testBuffer = createMockBuffer('hello world test', [0, 11]);
898
+ const { result } = renderVimHook(testBuffer);
899
+ act(() => {
900
+ result.current.handleInput(createKey({ sequence: 'c' }));
901
+ });
902
+ act(() => {
903
+ result.current.handleInput(createKey({ sequence: 'b' }));
904
+ });
905
+ expect(testBuffer.vimChangeWordBackward).toHaveBeenCalledWith(1);
906
+ expect(result.current.mode).toBe('INSERT');
907
+ });
908
+ it('should handle count with cb', () => {
909
+ const testBuffer = createMockBuffer('one two three four', [0, 18]);
910
+ const { result } = renderVimHook(testBuffer);
911
+ act(() => {
912
+ result.current.handleInput(createKey({ sequence: '3' }));
913
+ });
914
+ act(() => {
915
+ result.current.handleInput(createKey({ sequence: 'c' }));
916
+ });
917
+ act(() => {
918
+ result.current.handleInput(createKey({ sequence: 'b' }));
919
+ });
920
+ expect(testBuffer.vimChangeWordBackward).toHaveBeenCalledWith(3);
921
+ expect(result.current.mode).toBe('INSERT');
922
+ });
923
+ });
924
+ describe('Pending state handling', () => {
925
+ it('should clear pending delete state after dw', () => {
926
+ const testBuffer = createMockBuffer('hello world', [0, 0]);
927
+ const { result } = renderVimHook(testBuffer);
928
+ // Press 'd' to enter pending delete state
929
+ act(() => {
930
+ result.current.handleInput(createKey({ sequence: 'd' }));
931
+ });
932
+ // Complete with 'w'
933
+ act(() => {
934
+ result.current.handleInput(createKey({ sequence: 'w' }));
935
+ });
936
+ // Next 'd' should start a new pending state, not continue the previous one
937
+ act(() => {
938
+ result.current.handleInput(createKey({ sequence: 'd' }));
939
+ });
940
+ // This should trigger dd (delete line), not an error
941
+ act(() => {
942
+ result.current.handleInput(createKey({ sequence: 'd' }));
943
+ });
944
+ expect(testBuffer.vimDeleteLine).toHaveBeenCalledWith(1);
945
+ });
946
+ it('should clear pending change state after cw', () => {
947
+ const testBuffer = createMockBuffer('hello world', [0, 0]);
948
+ const { result } = renderVimHook(testBuffer);
949
+ // Execute cw
950
+ act(() => {
951
+ result.current.handleInput(createKey({ sequence: 'c' }));
952
+ });
953
+ act(() => {
954
+ result.current.handleInput(createKey({ sequence: 'w' }));
955
+ });
956
+ // Exit INSERT mode
957
+ exitInsertMode(result);
958
+ // Next 'c' should start a new pending state
959
+ act(() => {
960
+ result.current.handleInput(createKey({ sequence: 'c' }));
961
+ });
962
+ act(() => {
963
+ result.current.handleInput(createKey({ sequence: 'c' }));
964
+ });
965
+ expect(testBuffer.vimChangeLine).toHaveBeenCalledWith(1);
966
+ });
967
+ it('should clear pending state with escape', () => {
968
+ const testBuffer = createMockBuffer('hello world', [0, 0]);
969
+ const { result } = renderVimHook(testBuffer);
970
+ // Enter pending delete state
971
+ act(() => {
972
+ result.current.handleInput(createKey({ sequence: 'd' }));
973
+ });
974
+ // Press escape to clear pending state
975
+ act(() => {
976
+ result.current.handleInput(createKey({ name: 'escape' }));
977
+ });
978
+ // Now 'w' should just move cursor, not delete
979
+ act(() => {
980
+ result.current.handleInput(createKey({ sequence: 'w' }));
981
+ });
982
+ expect(testBuffer.vimDeleteWordForward).not.toHaveBeenCalled();
983
+ // w should move to next word after clearing pending state
984
+ expect(testBuffer.vimMoveWordForward).toHaveBeenCalledWith(1);
985
+ });
986
+ });
987
+ describe('NORMAL mode escape behavior', () => {
988
+ it('should pass escape through when no pending operator is active', () => {
989
+ mockVimContext.vimMode = 'NORMAL';
990
+ const { result } = renderVimHook();
991
+ const handled = result.current.handleInput(createKey({ name: 'escape' }));
992
+ expect(handled).toBe(false);
993
+ });
994
+ it('should handle escape and clear pending operator', () => {
995
+ mockVimContext.vimMode = 'NORMAL';
996
+ const { result } = renderVimHook();
997
+ act(() => {
998
+ result.current.handleInput(createKey({ sequence: 'd' }));
999
+ });
1000
+ let handled;
1001
+ act(() => {
1002
+ handled = result.current.handleInput(createKey({ name: 'escape' }));
1003
+ });
1004
+ expect(handled).toBe(true);
1005
+ });
1006
+ });
1007
+ });
1008
+ describe('Shell command pass-through', () => {
1009
+ it('should pass through ctrl+r in INSERT mode', () => {
1010
+ mockVimContext.vimMode = 'INSERT';
1011
+ const { result } = renderVimHook();
1012
+ const handled = result.current.handleInput(createKey({ name: 'r', ctrl: true }));
1013
+ expect(handled).toBe(false);
1014
+ });
1015
+ it('should pass through ! in INSERT mode when buffer is empty', () => {
1016
+ mockVimContext.vimMode = 'INSERT';
1017
+ const emptyBuffer = createMockBuffer('');
1018
+ const { result } = renderVimHook(emptyBuffer);
1019
+ const handled = result.current.handleInput(createKey({ sequence: '!' }));
1020
+ expect(handled).toBe(false);
1021
+ });
1022
+ it('should handle ! as input in INSERT mode when buffer is not empty', () => {
1023
+ mockVimContext.vimMode = 'INSERT';
1024
+ const nonEmptyBuffer = createMockBuffer('not empty');
1025
+ const { result } = renderVimHook(nonEmptyBuffer);
1026
+ const key = createKey({ sequence: '!', name: '!' });
1027
+ act(() => {
1028
+ result.current.handleInput(key);
1029
+ });
1030
+ expect(nonEmptyBuffer.handleInput).toHaveBeenCalledWith(expect.objectContaining(key));
1031
+ });
1032
+ });
1033
+ // Line operations (dd, cc) are tested in text-buffer.test.ts
1034
+ describe('Reducer-based integration tests', () => {
1035
+ describe('de (delete word end)', () => {
1036
+ it('should delete from cursor to end of current word', () => {
1037
+ const initialState = createMockTextBufferState({
1038
+ lines: ['hello world test'],
1039
+ cursorRow: 0,
1040
+ cursorCol: 1, // cursor on 'e' in "hello"
1041
+ preferredCol: null,
1042
+ undoStack: [],
1043
+ redoStack: [],
1044
+ clipboard: null,
1045
+ selectionAnchor: null,
1046
+ });
1047
+ const result = textBufferReducer(initialState, {
1048
+ type: 'vim_delete_word_end',
1049
+ payload: { count: 1 },
1050
+ });
1051
+ // Should delete "ello" (from cursor to end of word), leaving "h world test"
1052
+ expect(result.lines).toEqual(['h world test']);
1053
+ expect(result.cursorRow).toBe(0);
1054
+ expect(result.cursorCol).toBe(1);
1055
+ });
1056
+ it('should delete multiple word ends with count', () => {
1057
+ const initialState = createMockTextBufferState({
1058
+ lines: ['hello world test more'],
1059
+ cursorRow: 0,
1060
+ cursorCol: 1, // cursor on 'e' in "hello"
1061
+ preferredCol: null,
1062
+ undoStack: [],
1063
+ redoStack: [],
1064
+ clipboard: null,
1065
+ selectionAnchor: null,
1066
+ });
1067
+ const result = textBufferReducer(initialState, {
1068
+ type: 'vim_delete_word_end',
1069
+ payload: { count: 2 },
1070
+ });
1071
+ // Should delete "ello world" (to end of second word), leaving "h test more"
1072
+ expect(result.lines).toEqual(['h test more']);
1073
+ expect(result.cursorRow).toBe(0);
1074
+ expect(result.cursorCol).toBe(1);
1075
+ });
1076
+ });
1077
+ describe('db (delete word backward)', () => {
1078
+ it('should delete from cursor to start of previous word', () => {
1079
+ const initialState = createMockTextBufferState({
1080
+ lines: ['hello world test'],
1081
+ cursorRow: 0,
1082
+ cursorCol: 11, // cursor on 't' in "test"
1083
+ preferredCol: null,
1084
+ undoStack: [],
1085
+ redoStack: [],
1086
+ clipboard: null,
1087
+ selectionAnchor: null,
1088
+ });
1089
+ const result = textBufferReducer(initialState, {
1090
+ type: 'vim_delete_word_backward',
1091
+ payload: { count: 1 },
1092
+ });
1093
+ // Should delete "world" (previous word only), leaving "hello test"
1094
+ expect(result.lines).toEqual(['hello test']);
1095
+ expect(result.cursorRow).toBe(0);
1096
+ expect(result.cursorCol).toBe(6);
1097
+ });
1098
+ it('should delete multiple words backward with count', () => {
1099
+ const initialState = createMockTextBufferState({
1100
+ lines: ['hello world test more'],
1101
+ cursorRow: 0,
1102
+ cursorCol: 17, // cursor on 'm' in "more"
1103
+ preferredCol: null,
1104
+ undoStack: [],
1105
+ redoStack: [],
1106
+ clipboard: null,
1107
+ selectionAnchor: null,
1108
+ });
1109
+ const result = textBufferReducer(initialState, {
1110
+ type: 'vim_delete_word_backward',
1111
+ payload: { count: 2 },
1112
+ });
1113
+ // Should delete "world test " (two words backward), leaving "hello more"
1114
+ expect(result.lines).toEqual(['hello more']);
1115
+ expect(result.cursorRow).toBe(0);
1116
+ expect(result.cursorCol).toBe(6);
1117
+ });
1118
+ });
1119
+ describe('cw (change word forward)', () => {
1120
+ it('should delete from cursor to start of next word', () => {
1121
+ const initialState = createMockTextBufferState({
1122
+ lines: ['hello world test'],
1123
+ cursorRow: 0,
1124
+ cursorCol: 0, // cursor on 'h' in "hello"
1125
+ preferredCol: null,
1126
+ undoStack: [],
1127
+ redoStack: [],
1128
+ clipboard: null,
1129
+ selectionAnchor: null,
1130
+ });
1131
+ const result = textBufferReducer(initialState, {
1132
+ type: 'vim_change_word_forward',
1133
+ payload: { count: 1 },
1134
+ });
1135
+ // Should delete "hello " (word + space), leaving "world test"
1136
+ expect(result.lines).toEqual(['world test']);
1137
+ expect(result.cursorRow).toBe(0);
1138
+ expect(result.cursorCol).toBe(0);
1139
+ });
1140
+ it('should change multiple words with count', () => {
1141
+ const initialState = createMockTextBufferState({
1142
+ lines: ['hello world test more'],
1143
+ cursorRow: 0,
1144
+ cursorCol: 0,
1145
+ preferredCol: null,
1146
+ undoStack: [],
1147
+ redoStack: [],
1148
+ clipboard: null,
1149
+ selectionAnchor: null,
1150
+ });
1151
+ const result = textBufferReducer(initialState, {
1152
+ type: 'vim_change_word_forward',
1153
+ payload: { count: 2 },
1154
+ });
1155
+ // Should delete "hello world " (two words), leaving "test more"
1156
+ expect(result.lines).toEqual(['test more']);
1157
+ expect(result.cursorRow).toBe(0);
1158
+ expect(result.cursorCol).toBe(0);
1159
+ });
1160
+ });
1161
+ describe('ce (change word end)', () => {
1162
+ it('should change from cursor to end of current word', () => {
1163
+ const initialState = createMockTextBufferState({
1164
+ lines: ['hello world test'],
1165
+ cursorRow: 0,
1166
+ cursorCol: 1, // cursor on 'e' in "hello"
1167
+ preferredCol: null,
1168
+ undoStack: [],
1169
+ redoStack: [],
1170
+ clipboard: null,
1171
+ selectionAnchor: null,
1172
+ });
1173
+ const result = textBufferReducer(initialState, {
1174
+ type: 'vim_change_word_end',
1175
+ payload: { count: 1 },
1176
+ });
1177
+ // Should delete "ello" (from cursor to end of word), leaving "h world test"
1178
+ expect(result.lines).toEqual(['h world test']);
1179
+ expect(result.cursorRow).toBe(0);
1180
+ expect(result.cursorCol).toBe(1);
1181
+ });
1182
+ it('should change multiple word ends with count', () => {
1183
+ const initialState = createMockTextBufferState({
1184
+ lines: ['hello world test'],
1185
+ cursorRow: 0,
1186
+ cursorCol: 1, // cursor on 'e' in "hello"
1187
+ preferredCol: null,
1188
+ undoStack: [],
1189
+ redoStack: [],
1190
+ clipboard: null,
1191
+ selectionAnchor: null,
1192
+ });
1193
+ const result = textBufferReducer(initialState, {
1194
+ type: 'vim_change_word_end',
1195
+ payload: { count: 2 },
1196
+ });
1197
+ // Should delete "ello world" (to end of second word), leaving "h test"
1198
+ expect(result.lines).toEqual(['h test']);
1199
+ expect(result.cursorRow).toBe(0);
1200
+ expect(result.cursorCol).toBe(1);
1201
+ });
1202
+ });
1203
+ describe('cb (change word backward)', () => {
1204
+ it('should change from cursor to start of previous word', () => {
1205
+ const initialState = createMockTextBufferState({
1206
+ lines: ['hello world test'],
1207
+ cursorRow: 0,
1208
+ cursorCol: 11, // cursor on 't' in "test"
1209
+ preferredCol: null,
1210
+ undoStack: [],
1211
+ redoStack: [],
1212
+ clipboard: null,
1213
+ selectionAnchor: null,
1214
+ });
1215
+ const result = textBufferReducer(initialState, {
1216
+ type: 'vim_change_word_backward',
1217
+ payload: { count: 1 },
1218
+ });
1219
+ // Should delete "world" (previous word only), leaving "hello test"
1220
+ expect(result.lines).toEqual(['hello test']);
1221
+ expect(result.cursorRow).toBe(0);
1222
+ expect(result.cursorCol).toBe(6);
1223
+ });
1224
+ });
1225
+ describe('cc (change line)', () => {
1226
+ it('should clear the line and place cursor at the start', () => {
1227
+ const initialState = createMockTextBufferState({
1228
+ lines: [' hello world'],
1229
+ cursorRow: 0,
1230
+ cursorCol: 5, // cursor on 'o'
1231
+ preferredCol: null,
1232
+ undoStack: [],
1233
+ redoStack: [],
1234
+ clipboard: null,
1235
+ selectionAnchor: null,
1236
+ });
1237
+ const result = textBufferReducer(initialState, {
1238
+ type: 'vim_change_line',
1239
+ payload: { count: 1 },
1240
+ });
1241
+ expect(result.lines).toEqual(['']);
1242
+ expect(result.cursorRow).toBe(0);
1243
+ expect(result.cursorCol).toBe(0);
1244
+ });
1245
+ });
1246
+ describe('dd (delete line)', () => {
1247
+ it('should delete the current line', () => {
1248
+ const initialState = createMockTextBufferState({
1249
+ lines: ['line1', 'line2', 'line3'],
1250
+ cursorRow: 1,
1251
+ cursorCol: 2,
1252
+ preferredCol: null,
1253
+ undoStack: [],
1254
+ redoStack: [],
1255
+ clipboard: null,
1256
+ selectionAnchor: null,
1257
+ });
1258
+ const result = textBufferReducer(initialState, {
1259
+ type: 'vim_delete_line',
1260
+ payload: { count: 1 },
1261
+ });
1262
+ expect(result.lines).toEqual(['line1', 'line3']);
1263
+ expect(result.cursorRow).toBe(1);
1264
+ expect(result.cursorCol).toBe(0);
1265
+ });
1266
+ it('should delete multiple lines with count', () => {
1267
+ const initialState = createMockTextBufferState({
1268
+ lines: ['line1', 'line2', 'line3', 'line4'],
1269
+ cursorRow: 1,
1270
+ cursorCol: 2,
1271
+ preferredCol: null,
1272
+ undoStack: [],
1273
+ redoStack: [],
1274
+ clipboard: null,
1275
+ selectionAnchor: null,
1276
+ });
1277
+ const result = textBufferReducer(initialState, {
1278
+ type: 'vim_delete_line',
1279
+ payload: { count: 2 },
1280
+ });
1281
+ // Should delete lines 1 and 2
1282
+ expect(result.lines).toEqual(['line1', 'line4']);
1283
+ expect(result.cursorRow).toBe(1);
1284
+ expect(result.cursorCol).toBe(0);
1285
+ });
1286
+ it('should handle deleting last line', () => {
1287
+ const initialState = createMockTextBufferState({
1288
+ lines: ['only line'],
1289
+ cursorRow: 0,
1290
+ cursorCol: 3,
1291
+ preferredCol: null,
1292
+ undoStack: [],
1293
+ redoStack: [],
1294
+ clipboard: null,
1295
+ selectionAnchor: null,
1296
+ });
1297
+ const result = textBufferReducer(initialState, {
1298
+ type: 'vim_delete_line',
1299
+ payload: { count: 1 },
1300
+ });
1301
+ // Should leave an empty line when deleting the only line
1302
+ expect(result.lines).toEqual(['']);
1303
+ expect(result.cursorRow).toBe(0);
1304
+ expect(result.cursorCol).toBe(0);
1305
+ });
1306
+ });
1307
+ describe('D (delete to end of line)', () => {
1308
+ it('should delete from cursor to end of line', () => {
1309
+ const initialState = createMockTextBufferState({
1310
+ lines: ['hello world test'],
1311
+ cursorRow: 0,
1312
+ cursorCol: 6, // cursor on 'w' in "world"
1313
+ preferredCol: null,
1314
+ undoStack: [],
1315
+ redoStack: [],
1316
+ clipboard: null,
1317
+ selectionAnchor: null,
1318
+ });
1319
+ const result = textBufferReducer(initialState, {
1320
+ type: 'vim_delete_to_end_of_line',
1321
+ });
1322
+ // Should delete "world test", leaving "hello "
1323
+ expect(result.lines).toEqual(['hello ']);
1324
+ expect(result.cursorRow).toBe(0);
1325
+ expect(result.cursorCol).toBe(6);
1326
+ });
1327
+ it('should handle D at end of line', () => {
1328
+ const initialState = createMockTextBufferState({
1329
+ lines: ['hello world'],
1330
+ cursorRow: 0,
1331
+ cursorCol: 11, // cursor at end
1332
+ preferredCol: null,
1333
+ undoStack: [],
1334
+ redoStack: [],
1335
+ clipboard: null,
1336
+ selectionAnchor: null,
1337
+ });
1338
+ const result = textBufferReducer(initialState, {
1339
+ type: 'vim_delete_to_end_of_line',
1340
+ });
1341
+ // Should not change anything when at end of line
1342
+ expect(result.lines).toEqual(['hello world']);
1343
+ expect(result.cursorRow).toBe(0);
1344
+ expect(result.cursorCol).toBe(11);
1345
+ });
1346
+ });
1347
+ describe('C (change to end of line)', () => {
1348
+ it('should change from cursor to end of line', () => {
1349
+ const initialState = createMockTextBufferState({
1350
+ lines: ['hello world test'],
1351
+ cursorRow: 0,
1352
+ cursorCol: 6, // cursor on 'w' in "world"
1353
+ preferredCol: null,
1354
+ undoStack: [],
1355
+ redoStack: [],
1356
+ clipboard: null,
1357
+ selectionAnchor: null,
1358
+ });
1359
+ const result = textBufferReducer(initialState, {
1360
+ type: 'vim_change_to_end_of_line',
1361
+ });
1362
+ // Should delete "world test", leaving "hello "
1363
+ expect(result.lines).toEqual(['hello ']);
1364
+ expect(result.cursorRow).toBe(0);
1365
+ expect(result.cursorCol).toBe(6);
1366
+ });
1367
+ it('should handle C at beginning of line', () => {
1368
+ const initialState = createMockTextBufferState({
1369
+ lines: ['hello world'],
1370
+ cursorRow: 0,
1371
+ cursorCol: 0,
1372
+ preferredCol: null,
1373
+ undoStack: [],
1374
+ redoStack: [],
1375
+ clipboard: null,
1376
+ selectionAnchor: null,
1377
+ });
1378
+ const result = textBufferReducer(initialState, {
1379
+ type: 'vim_change_to_end_of_line',
1380
+ });
1381
+ // Should delete entire line content
1382
+ expect(result.lines).toEqual(['']);
1383
+ expect(result.cursorRow).toBe(0);
1384
+ expect(result.cursorCol).toBe(0);
1385
+ });
1386
+ });
1387
+ });
1388
+ });
1389
+ //# sourceMappingURL=vim.test.js.map