Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,18 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
version: latest
args: --color always --check lua/
args: --color always --check lua/ tests/
test:
name: test
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
- uses: rhysd/action-setup-vim@v1
with:
neovim: true
version: v0.10.0
- run: make test
docs:
name: vimdoc
runs-on: ubuntu-latest
Expand Down
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
PLENARY_DIR ?= /tmp/plenary.nvim

.PHONY: test plenary lint

plenary:
@if [ ! -d "$(PLENARY_DIR)" ]; then \
git clone --depth 1 https://github.com/nvim-lua/plenary.nvim $(PLENARY_DIR); \
fi

test: plenary
PLENARY_DIR=$(PLENARY_DIR) nvim --headless -u tests/minimal_init.lua -c "PlenaryBustedDirectory tests/ {minimal_init = 'tests/minimal_init.lua'}"

lint:
stylua --check lua/ tests/
90 changes: 90 additions & 0 deletions tests/helpers.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
local M = {}

-- Tracked values
M.feedkeys_calls = {}
M.notifications = {}

-- Original functions
local originals = {}

function M.setup_mocks()
M.feedkeys_calls = {}
M.notifications = {}

originals.mode = vim.fn.mode
originals.expand = vim.fn.expand
originals.feedkeys = vim.api.nvim_feedkeys
originals.replace_termcodes = vim.api.nvim_replace_termcodes
originals.buf_get_mark = vim.api.nvim_buf_get_mark
originals.buf_get_lines = vim.api.nvim_buf_get_lines
originals.notify = vim.notify

-- Default mock: normal mode
vim.fn.mode = function()
return 'n'
end

vim.fn.expand = function()
return 'word'
end

vim.api.nvim_feedkeys = function(keys, mode, escape_ks)
table.insert(M.feedkeys_calls, { keys = keys, mode = mode, escape_ks = escape_ks })
end

vim.api.nvim_replace_termcodes = function(str, from_part, do_lt, special)
return str
end

vim.api.nvim_buf_get_mark = function(buf, mark)
return { 1, 0 }
end

vim.api.nvim_buf_get_lines = function(buf, start_line, end_line, strict)
return { 'some text here' }
end

vim.notify = function(msg, level)
table.insert(M.notifications, { msg = msg, level = level })
end
end

function M.set_mode(mode)
vim.fn.mode = function()
return mode
end
end

function M.set_expand(result)
vim.fn.expand = function()
return result
end
end

function M.set_visual_marks(start_pos, end_pos)
vim.api.nvim_buf_get_mark = function(buf, mark)
if mark == '<' then
return start_pos
elseif mark == '>' then
return end_pos
end
end
end

function M.set_buffer_lines(lines)
vim.api.nvim_buf_get_lines = function(buf, start_line, end_line, strict)
return lines
end
end

function M.teardown_mocks()
vim.fn.mode = originals.mode
vim.fn.expand = originals.expand
vim.api.nvim_feedkeys = originals.feedkeys
vim.api.nvim_replace_termcodes = originals.replace_termcodes
vim.api.nvim_buf_get_mark = originals.buf_get_mark
vim.api.nvim_buf_get_lines = originals.buf_get_lines
vim.notify = originals.notify
end

return M
21 changes: 21 additions & 0 deletions tests/minimal_init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
local plenary_dir = os.getenv('PLENARY_DIR') or '/tmp/plenary.nvim'

if vim.fn.isdirectory(plenary_dir) == 0 then
vim.fn.system({
'git',
'clone',
'--depth',
'1',
'https://github.com/nvim-lua/plenary.nvim',
plenary_dir,
})
end

vim.opt.rtp:append('.')
vim.opt.rtp:append(plenary_dir)

-- Allow spec files to require('helpers')
package.path = './tests/?.lua;' .. package.path

vim.cmd('runtime plugin/plenary.vim')
require('plenary.busted')
169 changes: 169 additions & 0 deletions tests/scalpel_spec.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
local helpers = require('helpers')

describe('scalpel', function()
local scalpel

before_each(function()
helpers.setup_mocks()
package.loaded['scalpel'] = nil
scalpel = require('scalpel')
end)

after_each(function()
helpers.teardown_mocks()
end)

describe('setup', function()
it('does not error', function()
assert.has_no.errors(function()
scalpel.setup()
end)
end)
end)

describe('substitute', function()
describe('normal mode', function()
it('feeds substitution command with word under cursor', function()
helpers.set_mode('n')
helpers.set_expand('hello')

scalpel.substitute()

assert.equals(1, #helpers.feedkeys_calls)
local call = helpers.feedkeys_calls[1]
assert.truthy(call.keys:find('hello'))
assert.truthy(call.keys:find(':%%s/\\v'))
assert.truthy(call.keys:find('//gc'))
end)

it('uses very magic regex pattern', function()
helpers.set_mode('n')
helpers.set_expand('myVar')

scalpel.substitute()

local call = helpers.feedkeys_calls[1]
assert.truthy(call.keys:find('\\v'))
end)

it('includes cursor move keys to position in replacement field', function()
helpers.set_mode('n')
helpers.set_expand('test')

scalpel.substitute()

local call = helpers.feedkeys_calls[1]
assert.truthy(call.keys:find('<Left><Left><Left>'))
end)

it('notifies when word is blank', function()
helpers.set_mode('n')
helpers.set_expand(' ')

scalpel.substitute()

assert.equals(0, #helpers.feedkeys_calls)
assert.equals(1, #helpers.notifications)
assert.truthy(helpers.notifications[1].msg:find('blank'))
end)

it('notifies when word is empty', function()
helpers.set_mode('n')
helpers.set_expand('')

scalpel.substitute()

assert.equals(0, #helpers.feedkeys_calls)
assert.equals(1, #helpers.notifications)
assert.truthy(helpers.notifications[1].msg:find('blank'))
end)
end)

describe('visual mode', function()
-- Note: get_visual_range() calls nvim_feedkeys with <Esc> first,
-- so visual mode tests have an extra feedkeys call at index 1.

it('feeds substitution command with selected text', function()
helpers.set_mode('v')
helpers.set_visual_marks({ 1, 4 }, { 1, 8 })
helpers.set_buffer_lines({ 'the hello world line' })

scalpel.substitute()

assert.equals(2, #helpers.feedkeys_calls)
local call = helpers.feedkeys_calls[2]
assert.truthy(call.keys:find('hello'))
assert.truthy(call.keys:find(':%%s/\\v'))
assert.truthy(call.keys:find('//gc'))
end)

it('extracts correct substring from line', function()
helpers.set_mode('v')
helpers.set_visual_marks({ 1, 0 }, { 1, 2 })
helpers.set_buffer_lines({ 'foo bar baz' })

scalpel.substitute()

local call = helpers.feedkeys_calls[2]
assert.truthy(call.keys:find('foo'))
end)

it('notifies when selection spans multiple lines', function()
helpers.set_mode('v')
helpers.set_visual_marks({ 1, 0 }, { 3, 5 })

scalpel.substitute()

-- Only the <Esc> feedkeys call, no substitution
assert.equals(1, #helpers.feedkeys_calls)
assert.equals(1, #helpers.notifications)
assert.truthy(helpers.notifications[1].msg:find('Cannot substitute'))
end)

it('notifies when selected text is blank', function()
helpers.set_mode('v')
helpers.set_visual_marks({ 1, 4 }, { 1, 6 })
helpers.set_buffer_lines({ 'test end' })

scalpel.substitute()

-- Only the <Esc> feedkeys call, no substitution
assert.equals(1, #helpers.feedkeys_calls)
assert.equals(1, #helpers.notifications)
assert.truthy(helpers.notifications[1].msg:find('blank'))
end)
end)

describe('visual line mode', function()
it('feeds line-scoped substitution command without a word', function()
helpers.set_mode('V')

scalpel.substitute()

assert.equals(1, #helpers.feedkeys_calls)
local call = helpers.feedkeys_calls[1]
assert.truthy(call.keys:find(':s///gc'))
-- Should NOT contain %s (buffer-wide) or \v (very magic)
assert.falsy(call.keys:find('%%s'))
assert.falsy(call.keys:find('\\v'))
end)

it('includes cursor move keys to position in search field', function()
helpers.set_mode('V')

scalpel.substitute()

local call = helpers.feedkeys_calls[1]
assert.truthy(call.keys:find('<Left><Left><Left>'))
end)

it('does not trigger any notifications', function()
helpers.set_mode('V')

scalpel.substitute()

assert.equals(0, #helpers.notifications)
end)
end)
end)
end)