keymap.test.tsx 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. /** @jsxImportSource @opentui/solid */
  2. import { createDefaultOpenTuiKeymap } from "@opentui/keymap/opentui"
  3. import { createBindingLookup } from "@opentui/keymap/extras"
  4. import { testRender, useRenderer } from "@opentui/solid"
  5. import { expect, test } from "bun:test"
  6. import { onCleanup } from "solid-js"
  7. import { TuiKeybind } from "../src/config/keybind"
  8. import { getOpencodeModeStack, OPENCODE_BASE_MODE, OpencodeKeymapProvider, registerOpencodeKeymap } from "../src/keymap"
  9. function createResolvedKeymapConfig(input: TuiKeybind.KeybindOverrides = {}) {
  10. const keybinds = TuiKeybind.parse(input)
  11. return {
  12. keybinds: createBindingLookup(TuiKeybind.toBindingConfig(keybinds), {
  13. commandMap: TuiKeybind.CommandMap,
  14. bindingDefaults: TuiKeybind.bindingDefaults(),
  15. }),
  16. leader_timeout: 2000,
  17. }
  18. }
  19. test("legacy page key aliases compile as page keys", async () => {
  20. const sequences: Record<string, string[][]> = {}
  21. function Harness() {
  22. const renderer = useRenderer()
  23. const keymap = createDefaultOpenTuiKeymap(renderer)
  24. const config = createResolvedKeymapConfig({
  25. messages_page_up: "pgup",
  26. messages_page_down: "pgdown",
  27. })
  28. const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
  29. const offLayer = keymap.registerLayer({
  30. bindings: config.keybinds.gather("session", ["session.page.up", "session.page.down"]),
  31. })
  32. const bindings = keymap.getCommandBindings({
  33. visibility: "registered",
  34. commands: ["session.page.up", "session.page.down"],
  35. })
  36. sequences.up =
  37. bindings.get("session.page.up")?.map((binding) => binding.sequence.map((part) => part.stroke.name)) ?? []
  38. sequences.down =
  39. bindings.get("session.page.down")?.map((binding) => binding.sequence.map((part) => part.stroke.name)) ?? []
  40. onCleanup(() => {
  41. offLayer()
  42. offKeymap()
  43. })
  44. return (
  45. <OpencodeKeymapProvider keymap={keymap}>
  46. <box />
  47. </OpencodeKeymapProvider>
  48. )
  49. }
  50. const app = await testRender(() => <Harness />)
  51. try {
  52. expect(sequences).toEqual({
  53. up: [["pageup"]],
  54. down: [["pagedown"]],
  55. })
  56. } finally {
  57. app.renderer.destroy()
  58. }
  59. })
  60. test("mode-less bindings stay active when opencode mode changes", async () => {
  61. const counts: Record<string, Record<string, number>> = {}
  62. function Harness() {
  63. const renderer = useRenderer()
  64. const keymap = createDefaultOpenTuiKeymap(renderer)
  65. const config = createResolvedKeymapConfig()
  66. const offKeymap = registerOpencodeKeymap(keymap, renderer, config)
  67. const offGlobal = keymap.registerLayer({
  68. commands: [
  69. { name: "session.list", run() {} },
  70. { name: "session.new", run() {} },
  71. { name: "session.page.up", run() {} },
  72. { name: "session.first", run() {} },
  73. ],
  74. bindings: config.keybinds.gather("test.global", [
  75. "session.list",
  76. "session.new",
  77. "session.page.up",
  78. "session.first",
  79. ]),
  80. })
  81. const offBase = keymap.registerLayer({
  82. mode: OPENCODE_BASE_MODE,
  83. commands: [{ name: "model.list", run() {} }],
  84. bindings: config.keybinds.gather("test.base", ["model.list"]),
  85. })
  86. const activeCounts = () =>
  87. Object.fromEntries(
  88. Array.from(
  89. keymap.getCommandBindings({
  90. visibility: "active",
  91. commands: ["session.list", "session.new", "session.page.up", "session.first", "model.list"],
  92. }),
  93. ([command, bindings]) => [command, bindings.length],
  94. ),
  95. )
  96. counts.base = activeCounts()
  97. const popQuestion = getOpencodeModeStack(keymap).push("question")
  98. counts.question = activeCounts()
  99. popQuestion()
  100. const popAutocomplete = getOpencodeModeStack(keymap).push("autocomplete")
  101. counts.autocomplete = activeCounts()
  102. popAutocomplete()
  103. onCleanup(() => {
  104. offBase()
  105. offGlobal()
  106. offKeymap()
  107. })
  108. return (
  109. <OpencodeKeymapProvider keymap={keymap}>
  110. <box />
  111. </OpencodeKeymapProvider>
  112. )
  113. }
  114. const app = await testRender(() => <Harness />)
  115. try {
  116. expect(counts).toEqual({
  117. base: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 1 },
  118. question: { "session.list": 1, "session.new": 1, "session.page.up": 2, "session.first": 2, "model.list": 0 },
  119. autocomplete: {
  120. "session.list": 1,
  121. "session.new": 1,
  122. "session.page.up": 2,
  123. "session.first": 2,
  124. "model.list": 0,
  125. },
  126. })
  127. } finally {
  128. app.renderer.destroy()
  129. }
  130. })