|
1 | 1 | local inline_input = require('opencode.ui.inline_input') |
| 2 | +local stub = require('luassert.stub') |
2 | 3 |
|
3 | 4 | describe('inline_input', function() |
4 | 5 | local anchor_buf |
@@ -323,4 +324,200 @@ describe('inline_input', function() |
323 | 324 |
|
324 | 325 | assert.equals(text, submitted) |
325 | 326 | end) |
| 327 | + |
| 328 | + describe('history navigation (<C-p> / <C-n>)', function() |
| 329 | + local histget_stub |
| 330 | + local histadd_stub |
| 331 | + |
| 332 | + -- Build a vim.fn.histget stub that mirrors vim's "input" history: negative |
| 333 | + -- indices walk from most recent backwards, and any further-out entry |
| 334 | + -- returns '' (matching the real |histget()|). |
| 335 | + local function with_history(history) |
| 336 | + histget_stub:revert() |
| 337 | + histget_stub = stub(vim.fn, 'histget') |
| 338 | + histget_stub.invokes(function(history_name, idx) |
| 339 | + assert.equals('input', history_name) |
| 340 | + local real = idx |
| 341 | + if type(real) == 'number' and real < 0 then |
| 342 | + local entry = history[-real] |
| 343 | + return entry or '' |
| 344 | + end |
| 345 | + return '' |
| 346 | + end) |
| 347 | + |
| 348 | + histadd_stub:revert() |
| 349 | + histadd_stub = stub(vim.fn, 'histadd') |
| 350 | + histadd_stub.returns(1) |
| 351 | + end |
| 352 | + |
| 353 | + -- Invoke the buffer-local insert-mode keymap callback directly. This |
| 354 | + -- avoids headless mode quirks where nvim_buf_set_lines can drop us out |
| 355 | + -- of insert mode, and lets us assert the keymap wiring + history state |
| 356 | + -- machine independently of the mode state. |
| 357 | + local function call_keymap(input, lhs) |
| 358 | + local target = lhs:gsub('^<(.-)>$', function(k) |
| 359 | + return '<' .. k:upper() .. '>' |
| 360 | + end) |
| 361 | + for _, m in ipairs(vim.api.nvim_buf_get_keymap(input.buf, 'i')) do |
| 362 | + if m.lhs == target and m.callback then |
| 363 | + m.callback() |
| 364 | + return true |
| 365 | + end |
| 366 | + end |
| 367 | + return false |
| 368 | + end |
| 369 | + |
| 370 | + local function buf_lines(input) |
| 371 | + return vim.api.nvim_buf_get_lines(input.buf, 0, -1, false) |
| 372 | + end |
| 373 | + |
| 374 | + before_each(function() |
| 375 | + histget_stub = stub(vim.fn, 'histget') |
| 376 | + histadd_stub = stub(vim.fn, 'histadd') |
| 377 | + end) |
| 378 | + |
| 379 | + after_each(function() |
| 380 | + if histget_stub then |
| 381 | + histget_stub:revert() |
| 382 | + end |
| 383 | + if histadd_stub then |
| 384 | + histadd_stub:revert() |
| 385 | + end |
| 386 | + end) |
| 387 | + |
| 388 | + it('walks forward through history with <C-p>', function() |
| 389 | + with_history({ [1] = 'newest', [2] = 'middle', [3] = 'oldest' }) |
| 390 | + local input = open_input(0, 0) |
| 391 | + |
| 392 | + assert.is_true(call_keymap(input, '<C-p>')) |
| 393 | + assert.are.same({ 'newest' }, buf_lines(input)) |
| 394 | + |
| 395 | + assert.is_true(call_keymap(input, '<C-p>')) |
| 396 | + assert.are.same({ 'middle' }, buf_lines(input)) |
| 397 | + |
| 398 | + assert.is_true(call_keymap(input, '<C-p>')) |
| 399 | + assert.are.same({ 'oldest' }, buf_lines(input)) |
| 400 | + end) |
| 401 | + |
| 402 | + it('caps <C-p> at the oldest history entry', function() |
| 403 | + with_history({ [1] = 'newest', [2] = 'middle' }) |
| 404 | + local input = open_input(0, 0) |
| 405 | + |
| 406 | + for _ = 1, 5 do |
| 407 | + call_keymap(input, '<C-p>') |
| 408 | + end |
| 409 | + assert.are.same({ 'middle' }, buf_lines(input)) |
| 410 | + end) |
| 411 | + |
| 412 | + it('restores the in-progress draft with <C-n>', function() |
| 413 | + with_history({ [1] = 'newest', [2] = 'middle' }) |
| 414 | + local input = open_input(0, 0) |
| 415 | + |
| 416 | + vim.api.nvim_buf_set_lines(input.buf, 0, 1, false, { 'draft' }) |
| 417 | + call_keymap(input, '<C-p>') |
| 418 | + assert.are.same({ 'newest' }, buf_lines(input)) |
| 419 | + |
| 420 | + call_keymap(input, '<C-n>') |
| 421 | + assert.are.same({ 'draft' }, buf_lines(input)) |
| 422 | + end) |
| 423 | + |
| 424 | + it('walks back through history with <C-n>', function() |
| 425 | + with_history({ [1] = 'newest', [2] = 'middle', [3] = 'oldest' }) |
| 426 | + local input = open_input(0, 0) |
| 427 | + |
| 428 | + call_keymap(input, '<C-p>') |
| 429 | + call_keymap(input, '<C-p>') |
| 430 | + call_keymap(input, '<C-p>') |
| 431 | + assert.are.same({ 'oldest' }, buf_lines(input)) |
| 432 | + |
| 433 | + call_keymap(input, '<C-n>') |
| 434 | + assert.are.same({ 'middle' }, buf_lines(input)) |
| 435 | + |
| 436 | + call_keymap(input, '<C-n>') |
| 437 | + assert.are.same({ 'newest' }, buf_lines(input)) |
| 438 | + end) |
| 439 | + |
| 440 | + it('is a no-op when <C-p> is pressed with empty history', function() |
| 441 | + with_history({}) |
| 442 | + local input = open_input(0, 0) |
| 443 | + |
| 444 | + vim.api.nvim_buf_set_lines(input.buf, 0, 1, false, { 'draft' }) |
| 445 | + call_keymap(input, '<C-p>') |
| 446 | + assert.are.same({ 'draft' }, buf_lines(input)) |
| 447 | + end) |
| 448 | + |
| 449 | + it('is a no-op when <C-n> is pressed without first entering history', function() |
| 450 | + with_history({ [1] = 'newest' }) |
| 451 | + local input = open_input(0, 0) |
| 452 | + |
| 453 | + vim.api.nvim_buf_set_lines(input.buf, 0, 1, false, { 'draft' }) |
| 454 | + call_keymap(input, '<C-n>') |
| 455 | + assert.are.same({ 'draft' }, buf_lines(input)) |
| 456 | + end) |
| 457 | + |
| 458 | + it('handles multi-line history entries', function() |
| 459 | + with_history({ [1] = 'line1\nline2' }) |
| 460 | + local input = open_input(0, 0) |
| 461 | + |
| 462 | + call_keymap(input, '<C-p>') |
| 463 | + assert.are.same({ 'line1', 'line2' }, buf_lines(input)) |
| 464 | + end) |
| 465 | + |
| 466 | + it('places the cursor at the end of the inserted history entry', function() |
| 467 | + with_history({ [1] = 'first', [2] = 'second' }) |
| 468 | + local input = open_input(0, 0) |
| 469 | + |
| 470 | + call_keymap(input, '<C-p>') |
| 471 | + local cursor1 = vim.api.nvim_win_get_cursor(input.win) |
| 472 | + -- Cursor lands on the last char (normal-mode clamp) or the append |
| 473 | + -- position (insert-mode). Production runs in insert mode, so this |
| 474 | + -- matches user-visible behavior either way. |
| 475 | + assert.are.equal(1, cursor1[1]) |
| 476 | + assert.is_true(cursor1[2] == #('first') or cursor1[2] == #('first') - 1) |
| 477 | + |
| 478 | + call_keymap(input, '<C-p>') |
| 479 | + local cursor2 = vim.api.nvim_win_get_cursor(input.win) |
| 480 | + assert.are.equal(1, cursor2[1]) |
| 481 | + assert.is_true(cursor2[2] == #('second') or cursor2[2] == #('second') - 1) |
| 482 | + end) |
| 483 | + |
| 484 | + it('after restoring the draft, <C-p> re-enters history from the snapshot', function() |
| 485 | + with_history({ [1] = 'alpha', [2] = 'beta' }) |
| 486 | + local input = open_input(0, 0) |
| 487 | + |
| 488 | + vim.api.nvim_buf_set_lines(input.buf, 0, 1, false, { 'typed' }) |
| 489 | + call_keymap(input, '<C-p>') |
| 490 | + call_keymap(input, '<C-n>') |
| 491 | + assert.are.same({ 'typed' }, buf_lines(input)) |
| 492 | + |
| 493 | + call_keymap(input, '<C-p>') |
| 494 | + assert.are.same({ 'alpha' }, buf_lines(input)) |
| 495 | + end) |
| 496 | + |
| 497 | + it('writes submitted text to vim\'s "input" history (shared with vim.fn.input)', function() |
| 498 | + with_history({}) |
| 499 | + |
| 500 | + local submitted |
| 501 | + open_input(0, 0, function(value) |
| 502 | + submitted = value |
| 503 | + end) |
| 504 | + vim.api.nvim_feedkeys(vim.keycode('ianswer<CR>'), 'x', false) |
| 505 | + |
| 506 | + assert.equals('answer', submitted) |
| 507 | + assert.stub(histadd_stub).was_called_with('input', 'answer') |
| 508 | + end) |
| 509 | + |
| 510 | + it('does not write to history when submission is empty', function() |
| 511 | + with_history({}) |
| 512 | + |
| 513 | + local submitted |
| 514 | + open_input(0, 0, function(value) |
| 515 | + submitted = value |
| 516 | + end) |
| 517 | + vim.api.nvim_feedkeys(vim.keycode('<CR>'), 'x', false) |
| 518 | + |
| 519 | + assert.is_nil(submitted) |
| 520 | + assert.stub(histadd_stub).was_not_called() |
| 521 | + end) |
| 522 | + end) |
326 | 523 | end) |
0 commit comments