Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

neovim/tests/drive/neovide.lua

Raw
---@diagnostic disable: need-check-nil, undefined-field, param-type-mismatch, assign-type-mismatch
-- The checks that a headless neovim can not answer: does this actually behave
-- when a real GUI is painting it.
-- Run with: just drive-neovide
--
-- Everything is driven over the RPC socket of the neovim that neovide is
-- attached to. No mouse, no synthetic keystrokes, no foreground window
-- stealing, so it does not fight the person sitting in front of the machine.

if vim.fn.executable 'neovide' == 0 then
  print 'neovide is not installed, skipping'
  os.exit( 0 )
end

local ADDRESS = '127.0.0.1:' .. tostring( 6200 + math.random( 1, 180 ) )

local fixture = vim.fn.tempname()
vim.fn.mkdir( fixture, 'p' )
fixture = fixture .. '/watched.txt'
vim.fn.writefile( vim.fn.range( 1, 40 ), fixture )

-- the real configuration on purpose: this checks what a person actually sees,
-- including the notification plugin. `-i NONE` keeps it out of the shada file.
local server = vim.system( {
  vim.v.progpath,
  '--headless',
  '-i', 'NONE',
  '--listen', ADDRESS,
  fixture,
}, { stdout = false, stderr = false, } )

local channel
local connected = vim.wait( 20000, function ()
  local ok, value = pcall( vim.fn.sockconnect, 'tcp', ADDRESS, { rpc = true, } )
  if ok and value ~= 0 then
    channel = value
    return true
  end
  return false
end, 100 )

if not connected then
  io.stderr:write( 'cannot reach the driven neovim at ' .. ADDRESS .. '\n' )
  server:kill 'sigterm'
  os.exit( 1 )
end

local remote = function ( code, arguments )
  return vim.rpcrequest( channel, 'nvim_exec_lua', code, arguments or {} )
end

remote( [[
  require 'bugabinga.file_changed'
  return package.loaded['bugabinga.file_changed'] ~= nil
]] )

-- `--no-fork` keeps the window in the process we spawned, so it can be
-- cleaned up again
local gui = vim.system( { 'neovide', '--no-fork', '--server', ADDRESS, },
  { stdout = false, stderr = false, } )

local attached = vim.wait( 30000, function ()
  return remote [[ return #vim.api.nvim_list_uis() > 0 ]] == true
end, 200 )

local finish = function ( code )
  pcall( vim.fn.chanclose, channel )
  pcall( function () server:kill 'sigterm' end )
  pcall( function () gui:kill 'sigterm' end )
  vim.fn.delete( vim.fs.dirname( fixture ), 'rf' )
  os.exit( code )
end

if not attached then
  io.stderr:write( 'neovide never attached to ' .. ADDRESS .. '\n' )
  finish( 1 )
end

local checks, failures = {}, 0

local check = function ( name, body )
  local ok, failure = pcall( body )
  checks[#checks + 1] = ( ok and 'ok   ' or 'FAIL ' ) .. name
  if not ok then
    failures = failures + 1
    checks[#checks] = checks[#checks] .. ': ' .. tostring( failure )
  end
end

local expect = function ( condition, message )
  if not condition then error( message or 'expectation failed', 2 ) end
end

local target = fixture

--- binary mode, so lua does not turn every \n into \r\n behind our back
local write_file = function ( first, last )
  local handle = assert( io.open( target, 'wb' ) )
  for number = first, last do handle:write( number .. '\n' ) end
  handle:close()
end

--- every floating window the attached UI is currently painting
local floats = function ()
  return remote [[
    local found = {}
    for _, window in ipairs( vim.api.nvim_list_wins() ) do
      local config = vim.api.nvim_win_get_config( window )
      if config.relative ~= '' then
        local buffer = vim.api.nvim_win_get_buf( window )
        table.insert( found, {
          width = config.width,
          height = config.height,
          filetype = vim.bo[buffer].filetype,
          text = table.concat( vim.api.nvim_buf_get_lines( buffer, 0, -1, false ), '\n' ),
        } )
      end
    end
    return found
  ]]
end

-- start from a clean layout, so a previous run can not poison this one
remote [[
  vim.cmd 'silent! diffoff!'
  vim.cmd 'silent! only'
  local current = vim.api.nvim_get_current_buf()
  for _, buffer in ipairs( vim.api.nvim_list_bufs() ) do
    if buffer ~= current and vim.api.nvim_buf_get_name( buffer ) == '' then
      pcall( vim.api.nvim_buf_delete, buffer, { force = true } )
    end
  end
  vim.cmd 'silent! edit!'
  return true
]]

remote [[
  _G.T = { reloads = 0, notes = {}, focus = {} }
  vim.api.nvim_create_autocmd( 'FileChangedShellPost', {
    callback = function() _G.T.reloads = _G.T.reloads + 1 end,
  } )
  vim.api.nvim_create_autocmd( { 'FocusGained', 'FocusLost' }, {
    callback = function( event ) table.insert( _G.T.focus, event.event ) end,
  } )
  -- the server outlives this script, so only ever wrap the real notify once
  _G.T_NOTIFY = _G.T_NOTIFY or vim.notify
  vim.notify = function( message, ... )
    table.insert( _G.T.notes, message )
    return _G.T_NOTIFY( message, ... )
  end
  return true
]]

local reset = function ()
  remote [[
    _G.T.reloads = 0
    _G.T.notes = {}
    pcall( vim.api.nvim_win_set_cursor, 0, { 15, 0 } )
    return true
  ]]
end

local state = function ()
  return remote [[
    return {
      reloads = _G.T.reloads,
      notes = _G.T.notes,
      focus = _G.T.focus,
      cursor = vim.api.nvim_win_get_cursor( 0 )[1],
      text = vim.fn.getline '.',
      first = vim.fn.getline( 1 ),
      lines = vim.api.nvim_buf_line_count( 0 ),
      modified = vim.bo.modified,
      mode = vim.api.nvim_get_mode().mode,
    }
  ]]
end

local await_reload = function ()
  return vim.wait( 8000, function ()
    return remote [[ return _G.T.reloads > 0 ]] == true
  end, 100 )
end

--[[ CHECKS ]]

check( 'a real neovide UI is attached', function ()
  local uis = remote [[ return vim.api.nvim_list_uis() ]]
  expect( #uis == 1, 'expected one attached UI, got ' .. tostring( #uis ) )
  expect( uis[1].width > 0 and uis[1].height > 0, 'the UI has no grid' )
end )

check( 'a change on disk reloads and the cursor stays put', function ()
  reset()
  write_file( 101, 140 )
  expect( await_reload(), 'the change was never noticed' )

  local now = state()
  expect( now.text == '115', 'expected 115 on the cursor line, got ' .. vim.inspect( now.text ) )
  expect( now.cursor == 15, 'the cursor moved to line ' .. tostring( now.cursor ) )
  expect( #now.notes == 1, 'expected one notification, got ' .. tostring( #now.notes ) )
end )

check( 'the reload notification is a real floating window in the UI', function ()
  reset()
  write_file( 151, 190 )
  expect( await_reload(), 'the change was never noticed' )

  local painted = floats()
  expect( #painted > 0, 'no floating window is being painted' )
  local found = false
  for _, float in ipairs( painted ) do
    if float.text:find( 'reloaded', 1, true ) then found = true end
  end
  expect( found, 'the reload notification is not on screen: ' .. vim.inspect( painted ) )
end )

check( 'nothing is reloaded under your fingers in insert mode', function ()
  reset()
  local before = state().text
  remote [[ vim.api.nvim_input 'i' ; return true ]]
  vim.uv.sleep( 500 )
  expect( state().mode == 'i', 'could not get into insert mode' )

  write_file( 201, 240 )
  vim.uv.sleep( 5000 )

  local now = state()
  expect( now.mode == 'i', 'insert mode was left on its own' )
  expect( now.reloads == 0, 'reloaded while typing' )
  expect( now.text == before,
    'the buffer changed under the cursor: ' .. vim.inspect( before ) .. ' -> ' .. vim.inspect( now.text ) )
end )

check( 'leaving insert mode reloads immediately', function ()
  remote [[ vim.api.nvim_input '<Esc>' ; return true ]]
  expect( await_reload(), 'no reload after leaving insert mode' )

  local now = state()
  expect( now.text == '215', 'expected 215, got ' .. vim.inspect( now.text ) )
  expect( now.cursor == 15, 'the cursor moved to line ' .. tostring( now.cursor ) )
end )

check( 'undo brings the version from before the reload back', function ()
  remote [[ vim.api.nvim_input 'u' ; return true ]]
  vim.uv.sleep( 1500 )

  local now = state()
  expect( now.first == '151',
    'undo did not restore the text from before the reload: ' .. vim.inspect( now.first ) )
  expect( now.modified, 'the buffer is not marked modified after undoing a reload' )

  remote [[ vim.cmd 'silent! edit!' ; return true ]]
  vim.uv.sleep( 1000 )
end )

check( 'unsaved edits are never overwritten', function ()
  reset()
  remote [[
    vim.api.nvim_buf_set_lines( 0, 14, 15, false, { 'MY UNSAVED EDIT' } )
    return true
  ]]
  write_file( 301, 340 )
  vim.uv.sleep( 4500 )

  local now = state()
  expect( now.reloads == 0, 'reloaded over unsaved changes' )
  expect( now.text == 'MY UNSAVED EDIT', 'lost the local edit: ' .. vim.inspect( now.text ) )
  expect( #now.notes == 1, 'expected one warning, got ' .. tostring( #now.notes ) )
  expect( now.notes[1]:find( 'DiffDisk', 1, true ) ~= nil, 'the warning offers no way out' )
end )

check( 'the conflict warning is legible in the UI', function ()
  local painted = floats()
  local warning
  for _, float in ipairs( painted ) do
    if float.text:find( 'changed on disk', 1, true ) then warning = float end
  end
  expect( warning, 'the conflict warning is not on screen: ' .. vim.inspect( painted ) )
  expect( warning.text:find( 'DiffDisk', 1, true ) ~= nil, 'the way out is cut off' )
  expect( warning.height >= 2, 'the warning is squeezed into one line' )
end )

check( 'a second conflict is reported again after :e!', function ()
  remote [[ vim.cmd 'edit!' ; return true ]]
  reset()
  remote [[
    vim.api.nvim_buf_set_lines( 0, 14, 15, false, { 'ANOTHER UNSAVED EDIT' } )
    return true
  ]]
  write_file( 401, 440 )
  vim.uv.sleep( 4500 )

  local now = state()
  expect( now.reloads == 0, 'reloaded over unsaved changes' )
  expect( #now.notes == 1, 'the warning was swallowed, got ' .. tostring( #now.notes ) )
end )

check( 'the buffer survives the file being deleted', function ()
  reset()
  assert( os.remove( target ) )
  vim.uv.sleep( 4500 )

  local now = state()
  expect( now.text == 'ANOTHER UNSAVED EDIT', 'the text died with the file' )
  expect( #now.notes == 1, 'expected one warning, got ' .. tostring( #now.notes ) )
  expect( now.notes[1]:find( 'gone from disk', 1, true ) ~= nil,
    'unexpected warning: ' .. tostring( now.notes[1] ) )
end )

check( 'DiffDisk opens a real side by side diff', function ()
  remote [[ vim.cmd 'write' ; return true ]]
  reset()
  write_file( 501, 540 )
  expect( await_reload(), 'the change was never noticed' )

  local diffed = remote [[
    vim.api.nvim_buf_set_lines( 0, 0, 1, false, { 'ONLY IN THE BUFFER' } )
    vim.cmd 'DiffDisk'
    local original = vim.api.nvim_get_current_buf()
    local in_diff, disk = 0, nil
    for _, window in ipairs( vim.api.nvim_tabpage_list_wins( 0 ) ) do
      if vim.wo[window].diff then
        in_diff = in_diff + 1
        local buffer = vim.api.nvim_win_get_buf( window )
        if buffer ~= original then
          disk = vim.api.nvim_buf_get_lines( buffer, 0, 1, false )[1]
        end
      end
    end
    vim.cmd 'redraw'
    return { in_diff = in_diff, disk = disk }
  ]]
  expect( diffed.in_diff == 2, 'expected two diff windows, got ' .. tostring( diffed.in_diff ) )
  expect( diffed.disk == '501',
    'the scratch side does not hold the disk version: ' .. vim.inspect( diffed.disk ) )

  remote [[ vim.cmd 'diffoff!' ; vim.cmd 'only' ; vim.cmd 'silent! edit!' ; return true ]]
end )

--[[ REPORT ]]

print( table.concat( checks, '\n' ) )

-- focus reporting can not be produced from inside neovim: it needs the window
-- manager to actually move focus. report what was observed instead of pretending.
local focus = state().focus
print( ( '\nfocus events seen while driving: %d %s' )
  :format( #focus, vim.inspect( focus ) ) )

print( failures == 0 and '\nall neovide checks passed'
  or ( '\n' .. tostring( failures ) .. ' check(s) failed' ) )

finish( failures == 0 and 0 or 1 )