aboutsummaryrefslogtreecommitdiffstats
path: root/tests/indent/common.lua
blob: ff30b5a5d7b88e3926071863c74265be2e016dee (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
local M = {}

local assert = require('luassert')
local say = require('say')
local scan_dir = require('plenary.scandir').scan_dir
local Path = require('plenary.path')

local function same_indent(state, arguments)
  local before = arguments[1]
  local after = arguments[2]

  local ok = true
  local errors = { before = {}, after = {} }
  for line = 1,#before do
    if before[line] ~= after[line] then
      -- store the actual indentation length for each line
      errors.before[line] = #string.match(before[line], '^%s*')
      errors.after[line] = #string.match(after[line], '^%s*')
      ok = false
    end
  end

  -- we will always use only a single argument, passing the other one in fmtargs
  arguments.fmtargs = { { errors = errors, other = after } }
  arguments.fmtargs[2] = { errors = errors, other = after }

  return ok
end

local function format_indent(arg, fmtargs)
  -- find minimal width if any line is longer
  local width = 40
  for _, line in ipairs(fmtargs.other) do
    width = #line > width and #line or width
  end

  width = width + 3
  local header_fmt = '%8s %2s%-' .. tostring(width + 1) .. 's %s'
  local fmt = '%8s %2s |%-' .. tostring(width) .. 's |%s'

  local output = {header_fmt:format('', '', 'Found:', 'Expected:')}

  for i, line in ipairs(arg) do
    if fmtargs.errors.before[i] then
      local indents = string.format('%d vs %d', fmtargs.errors.after[i], fmtargs.errors.before[i])
      table.insert(output, fmt:format(indents, '=>', fmtargs.other[i], line))
    else
      table.insert(output, fmt:format('', '', fmtargs.other[i], line))
    end
  end

  return table.concat(output, '\n')
end

say:set_namespace('en')
say:set('assertion.same_indent.positive', 'Incorrect indentation\n%s')
say:set('assertion.same_indent.negative', 'Incorrect indentation\n%s')
assert:register('assertion', 'same_indent', same_indent,
  'assertion.same_indent.positive', 'assert.same_indent.negative')

-- Custom assertion better suited for indentation diffs
local function compare_indent(before, after)
  assert:add_formatter(format_indent)
  assert.is.same_indent(before, after)
  assert:remove_formatter(format_indent)
end

local function set_buf_indent_opts(opts)
  local optnames = {'tabstop', 'shiftwidth', 'softtabstop', 'expandtab', 'filetype'}
  for _, opt in ipairs(optnames) do
    if opts[opt] ~= nil then
      vim.bo[opt] = opts[opt]
    end
  end
end

function M.run_indent_test(file, runner, opts)
  assert.are.same(1, vim.fn.filereadable(file), string.format('File "%s" not readable', file))

  -- load reference file
  vim.cmd(string.format('edit %s', file))
  local before = vim.api.nvim_buf_get_lines(0, 0, -1, true)

  assert.are.same('nvim_treesitter#indent()', vim.bo.indentexpr)
  set_buf_indent_opts(opts)

  -- perform the test
  runner()

  -- get file content after the test
  local after = vim.api.nvim_buf_get_lines(0, 0, -1, true)

  -- clear any changes to avoid 'No write since last change (add ! to override)'
  vim.cmd 'edit!'

  return before, after
end

function M.indent_whole_file(file, opts)
  local before, after = M.run_indent_test(file, function()
    vim.cmd 'silent normal gg=G'
  end, opts)

  compare_indent(before, after)
end

-- Open a file, use `normal o` to insert a new line and compare results
-- @param file path to the initial file
-- @param spec a table with keys:
--   on_line: line on which `normal o` is executed
--   text: text inserted in the new line
--   indent: expected indent before the inserted text (string or int)
-- @param opts buffer options passed to set_buf_indent_opts
function M.indent_new_line(file, spec, opts)
  local before, after = M.run_indent_test(file, function()
    -- move to the line and input the new one
    vim.cmd(string.format('normal! %dG', spec.on_line))
    vim.cmd(string.format('normal! o%s', spec.text))
  end, opts)

  local indent = type(spec.indent) == 'string' and spec.indent or string.rep(' ', spec.indent)
  table.insert(before, spec.on_line + 1, indent .. spec.text)

  compare_indent(before, after)
end

local Runner = {}
Runner.__index = Runner

-- Helper to avoid boilerplate when defining tests
-- @param it  the "it" function that busted defines globally in spec files
-- @param base_dir  all other paths will be resolved relative to this directory
-- @param buf_opts  buffer options passed to set_buf_indent_opts
function Runner:new(it, base_dir, buf_opts)
  local runner = {}
  runner.it = it
  runner.base_dir = Path:new(base_dir)
  runner.buf_opts = buf_opts
  return setmetatable(runner, self)
end

function Runner:whole_file(dirs)
  dirs = type(dirs) == "table" and dirs or {dirs}
  dirs = vim.tbl_map(function(dir)
    dir = self.base_dir / Path:new(dir)
    assert.is.same(1, vim.fn.isdirectory(dir.filename))
    return dir.filename
  end, dirs)
  local files = vim.tbl_flatten(vim.tbl_map(scan_dir, dirs))
  for _, file in ipairs(files) do
    local relpath = Path:new(file):make_relative(self.base_dir.filename)
    self.it(relpath, function()
      M.indent_whole_file(file, self.buf_opts)
    end)
  end
end

function Runner:new_line(file, spec, title)
  title = title and title or tostring(spec.on_line)
  self.it(string.format('%s[%s]', file, title), function()
    local path = self.base_dir / file
    M.indent_new_line(path.filename, spec, self.buf_opts)
  end)
end

M.Runner = Runner

return M