---@diagnostic disable: duplicate-set-field, need-check-nil, param-type-mismatch, assign-type-mismatch, redundant-parameter local navigate = require 'bugabinga.pivi.navigate' local entry = require 'bugabinga.pivi.entry' local transcript = require 'bugabinga.pivi.transcript' local follow = require 'bugabinga.pivi.follow' local fake = dofile 'tests/helpers/pivi_fake.lua' local temp_file = function( lines ) local path = vim.fs.normalize( vim.fn.tempname() .. '.txt' ) vim.fn.writefile( lines, path ) return path end local block = function( bufnr, kind, meta, lines ) local id = entry.open( bufnr, kind, meta ) for _, text in ipairs( lines ) do entry.line( bufnr, id, text ) end entry.close( bufnr, id ) return id end describe( 'pivi navigate', function() local bufnr, win before_each( function() bufnr = transcript.ensure 'moving' transcript.clear( bufnr ) block( bufnr, 'prompt', { text = 'ask', }, { 'ask', } ) block( bufnr, 'tool', { tool = 'read', }, { 'read a.lua', 'one', } ) block( bufnr, 'answer', {}, { 'reply', } ) block( bufnr, 'tool', { tool = 'edit', }, { 'edit a.lua', } ) win = transcript.show( bufnr ) vim.api.nvim_set_current_win( win ) end ) after_each( function() follow.reset() transcript.forget_all() fake.close_extra_windows() end ) describe( 'locations', function() it( 'reads the place an entry names', function() local located = navigate.location_of { kind = 'tool', meta = { args = { path = 'src/main.lua', line = 12, }, cwd = '/work', }, } assert.equal( '/work/src/main.lua', located.path ) assert.equal( 12, located.line ) end ) it( 'keeps an absolute path as it is', function() local located = navigate.location_of { kind = 'tool', meta = { args = { path = '/elsewhere/a.lua', }, cwd = '/work', }, } assert.equal( '/elsewhere/a.lua', located.path ) end ) it( 'reaches the first changed line of an edit', function() local located = navigate.location_of { kind = 'tool', meta = { tool = 'edit', args = { path = 'a.lua', }, result = { details = { diff = '-7 old\n+7 new', firstChangedLine = 7, }, }, }, } assert.equal( 7, located.line ) end ) it( 'prefers the reported change over an argument line', function() local located = navigate.location_of { kind = 'tool', meta = { tool = 'edit', args = { path = 'a.lua', line = 1, }, result = { details = { firstChangedLine = 12, }, }, }, } assert.equal( 12, located.line ) end ) it( 'keeps the argument line when no change was reported', function() local located = navigate.location_of { kind = 'tool', meta = { tool = 'read', args = { path = 'a.lua', line = 4, }, }, } assert.equal( 4, located.line ) end ) it( 'lands on an edit at the line it changed', function() local path = temp_file { 'one', 'two', 'three', 'four', 'five', } block( bufnr, 'tool', { tool = 'edit', args = { path = path, }, result = { details = { diff = '-4 four\n+4 FOUR', firstChangedLine = 4, }, }, }, { 'edit', '-4 four', '+4 FOUR', } ) local listed = entry.list( bufnr ) local target = navigate.locate( bufnr, listed[#listed].first ) assert.is_number( target ) assert.equal( 4, vim.api.nvim_win_get_cursor( target )[1] ) end ) it( 'reaches the full output of a truncated result', function() local located = navigate.location_of { kind = 'tool', meta = { tool = 'bash', args = { command = 'ls -R', }, result = { details = { truncation = { truncated = true, }, fullOutputPath = '/tmp/pi-bash-abc.log', }, }, }, } assert.equal( '/tmp/pi-bash-abc.log', located.path ) end ) it( 'prefers what a tool acted on over where its output spilled', function() local located = navigate.location_of { kind = 'tool', meta = { tool = 'read', args = { path = '/work/a.lua', }, result = { details = { fullOutputPath = '/tmp/spill.log', }, }, }, } assert.equal( '/work/a.lua', located.path ) end ) it( 'reports no place for an entry that names none', function() assert.is_nil( navigate.location_of { kind = 'answer', meta = {}, } ) assert.is_nil( navigate.location_of( nil ) ) end ) it( 'reaches the place an entry names without taking the working window', function() local path = temp_file { 'one', 'two', 'three', } block( bufnr, 'tool', { tool = 'read', args = { path = path, line = 3, }, }, { 'read', } ) local listed = entry.list( bufnr ) local tool = listed[#listed] vim.api.nvim_win_set_cursor( win, { tool.first, 0, } ) local target = navigate.locate( bufnr, tool.first ) assert.is_number( target ) assert.is_not.equal( win, target ) assert.equal( win, vim.api.nvim_get_current_win() ) assert.equal( 3, vim.api.nvim_win_get_cursor( target )[1] ) end ) it( 'reaches nothing from an entry without a location', function() local listed = entry.list( bufnr ) local answer = listed[3] assert.is_nil( navigate.locate( bufnr, answer.first ) ) end ) it( 'publishes going to a location as an intent', function() local published for _, mapping in ipairs( vim.api.nvim_buf_get_keymap( bufnr, 'n' ) ) do if mapping.lhs == '(pivi-goto-location)' then published = mapping end end assert.is_table( published ) end ) end ) it( 'moves to the next entry regardless of kind', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[1].first, 0, } ) local moved = navigate.next_entry( bufnr ) assert.equal( listed[2].id, moved.id ) assert.equal( listed[2].first, vim.api.nvim_win_get_cursor( win )[1] ) end ) it( 'moves to the previous entry', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[3].first, 0, } ) local moved = navigate.previous_entry( bufnr ) assert.equal( listed[2].id, moved.id ) end ) it( 'moves between tool calls only', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[1].first, 0, } ) local first_tool = navigate.next_entry( bufnr, 'tool' ) assert.equal( 'tool', first_tool.kind ) assert.equal( 'read', first_tool.meta.tool ) local second_tool = navigate.next_entry( bufnr, 'tool' ) assert.equal( 'edit', second_tool.meta.tool ) end ) it( 'moves backwards between tool calls', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[4].first, 0, } ) local moved = navigate.previous_entry( bufnr, 'tool' ) assert.equal( 'read', moved.meta.tool ) end ) it( 'wraps around at both ends', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[#listed].first, 0, } ) assert.equal( listed[1].id, navigate.next_entry( bufnr ).id ) vim.api.nvim_win_set_cursor( win, { listed[1].first, 0, } ) assert.equal( listed[#listed].id, navigate.previous_entry( bufnr ).id ) end ) it( 'moves nowhere in a transcript without matching entries', function() local empty = transcript.ensure 'nothing' transcript.clear( empty ) assert.is_nil( navigate.after( empty, 1 ) ) assert.is_nil( navigate.before( empty, 1 ) ) end ) it( 'publishes each motion as an intent', function() local published = {} for _, mapping in ipairs( vim.api.nvim_buf_get_keymap( bufnr, 'n' ) ) do published[mapping.lhs] = true end for _, intent in ipairs { '(pivi-next-entry)', '(pivi-previous-entry)', '(pivi-next-tool)', '(pivi-previous-tool)', } do assert.is_true( published[intent], intent .. ' is not published' ) end end ) it( 'never changes the window or the buffer while moving', function() local listed = entry.list( bufnr ) vim.api.nvim_win_set_cursor( win, { listed[1].first, 0, } ) local before = transcript.lines( bufnr ) navigate.next_entry( bufnr ) assert.equal( win, vim.api.nvim_get_current_win() ) assert.equal( bufnr, vim.api.nvim_win_get_buf( win ) ) assert.same( before, transcript.lines( bufnr ) ) end ) end )