-
Notifications
You must be signed in to change notification settings - Fork 321
/
init.lua
390 lines (361 loc) · 13.5 KB
/
init.lua
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
-- SPDX-FileCopyrightText: Copyright 2010-present Greg Hurrell and contributors.
-- SPDX-License-Identifier: BSD-2-Clause
local concat = require('wincent.commandt.private.concat')
local contains = require('wincent.commandt.private.contains')
local copy = require('wincent.commandt.private.copy')
local is_integer = require('wincent.commandt.private.is_integer')
local keys = require('wincent.commandt.private.keys')
local merge = require('wincent.commandt.private.merge')
local commandt = {}
local default_options = {
always_show_dot_files = false,
finders = {},
height = 15,
ignore_case = nil, -- If nil, will infer from Neovim's `'ignorecase'`.
-- Note that because of the way we merge mappings recursively, you can _add_
-- or _replace_ a mapping easily, but to _remove_ it you have to assign it to
-- `false` (`nil` won't work, because Lua will just skip over it).
mappings = {
i = {
['<C-a>'] = '<Home>',
['<C-c>'] = 'close',
['<C-e>'] = '<End>',
['<C-h>'] = '<Left>',
['<C-j>'] = 'select_next',
['<C-k>'] = 'select_previous',
['<C-l>'] = '<Right>',
['<C-n>'] = 'select_next',
['<C-p>'] = 'select_previous',
['<C-s>'] = 'open_split',
['<C-t>'] = 'open_tab',
['<C-v>'] = 'open_vsplit',
['<CR>'] = 'open',
['<Down>'] = 'select_next',
['<Up>'] = 'select_previous',
},
n = {
['<C-a>'] = '<Home>',
['<C-c>'] = 'close',
['<C-e>'] = '<End>',
['<C-h>'] = '<Left>',
['<C-j>'] = 'select_next',
['<C-k>'] = 'select_previous',
['<C-l>'] = '<Right>',
['<C-n>'] = 'select_next',
['<C-p>'] = 'select_previous',
['<C-s>'] = 'open_split',
['<C-t>'] = 'open_tab',
['<C-u>'] = 'clear', -- Not needed in insert mode.
['<C-v>'] = 'open_vsplit',
['<CR>'] = 'open',
['<Down>'] = 'select_next',
['<Esc>'] = 'close', -- Only in normal mode.
['<Up>'] = 'select_previous',
['H'] = 'select_first', -- Only in normal mode.
['M'] = 'select_middle', -- Only in normal mode.
['G'] = 'select_last', -- Only in normal mode.
['L'] = 'select_last', -- Only in normal mode.
['gg'] = 'select_first', -- Only in normal mode.
['j'] = 'select_next', -- Only in normal mode.
['k'] = 'select_previous', -- Only in normal mode.
},
},
margin = 10,
never_show_dot_files = false,
order = 'forward', -- 'forward', 'reverse'.
position = 'center', -- 'bottom', 'center', 'top'.
open = function(item, kind)
commandt.open(item, kind)
end,
scanners = {
git = {
submodules = true,
untracked = false,
},
},
selection_highlight = 'PMenuSel',
smart_case = nil, -- If nil, will infer from Neovim's `'smartcase'`.
threads = nil, -- Let heuristic apply.
}
-- Have to add some of these explicitly otherwise the ones with `nil` defaults
-- won't come through (eg. `ignore_case` etc).
local allowed_options = concat(keys(default_options), {
'ignore_case',
'smart_case',
'threads',
})
commandt.buffer_finder = function()
-- TODO: refactor to avoid duplication
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.buffer')(options)
ui.show(finder, merge(options, { name = 'buffer' }))
end
commandt.default_options = function()
return copy(default_options)
end
commandt.file_finder = function(directory)
directory = vim.trim(directory)
if directory == '' then
directory = '.'
end
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.file')(directory, options)
ui.show(finder, merge(options, { name = 'file' }))
end
commandt.find_finder = function(directory)
directory = vim.trim(directory)
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.find')(directory, options)
ui.show(finder, merge(options, { name = 'find' }))
end
commandt.finder = function(name, directory)
local options = commandt.options()
if options.finders[name] == nil then
error('commandt.finder(): no finder registered with name ' .. tostring(name))
end
directory = vim.trim(directory)
local finder = require('wincent.commandt.private.finders.command')(directory, options, name)
local ui = require('wincent.commandt.private.ui')
ui.show(finder, merge(options, { name = name }))
end
commandt.git_finder = function(directory)
directory = vim.trim(directory)
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.git')(directory, options)
ui.show(finder, merge(options, { name = 'git' }))
end
commandt.help_finder = function()
-- TODO: refactor to avoid duplication
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.help')(options)
ui.show(finder, merge(options, { name = 'help' }))
end
-- "Smart" open that will switch to an already open window containing the
-- specified `buffer`, if one exists; otherwise, it will open a new window using
-- `command` (which should be one of `edit`, `tabedit`, `split`, or `vsplit`).
commandt.open = function(buffer, command)
local is_visible = require('wincent.commandt.private.buffer_visible')(buffer)
if is_visible then
-- In order to be useful, `:sbuffer` needs `vim.o.switchbuf = 'usetab'`.
vim.cmd('sbuffer ' .. buffer)
else
vim.cmd(command .. ' ' .. buffer)
end
end
local _options = copy(default_options)
commandt.options = function()
return copy(_options)
end
commandt.rg_finder = function(directory)
directory = vim.trim(directory)
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.rg')(directory, options)
ui.show(finder, merge(options, { name = 'rg' }))
end
commandt.setup = function(options)
local errors = {}
if vim.g.command_t_loaded == 1 then
-- May not be an error if you (for whatever reason) are calling `setup()`
-- twice (ie. later on during a session), but it's presumed that if you're
-- doing that, you know enough about what you're doing not to understand
-- that this error message is nothing to worry about.
table.insert(errors, '`commandt.setup()` was called after Ruby plugin setup has already run')
elseif vim.g.CommandTPreferredImplementation == 'ruby' then
table.insert(errors, '`commandt.setup()` was called, but `g:CommandTPreferredImplementation` is set to "ruby"')
else
vim.g.CommandTPreferredImplementation = 'lua'
end
options = options or {}
_options = merge(_options, options)
-- Inferred from Neovim settings if not explicitly set.
if _options.ignore_case == nil then
_options.ignore_case = vim.o.ignorecase
end
if _options.smart_case == nil then
_options.smart_case = vim.o.smartcase
end
-- Helper functions for validating options.
local report = function(message)
table.insert(errors, message)
end
local last = function(list)
return list[#list]
end
local split_option = function(option)
local segments = {}
-- Append trailing '.' to make it easier to split.
for segment in string.gmatch(option .. '.', '([%a%d_]+)%.') do
table.insert(segments, segment)
end
return segments
end
local pick = function(option, value, pop)
pop = pop ~= nil and pop or 0
local segments = split_option(option)
local result = value
for i, segment in ipairs(segments) do
if i <= #segments - pop then
assert(type(result) == 'table')
result = result[segment]
end
end
return result
end
local reset = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
actual = pick(option, actual, 1)
actual[last(split_option(option))] = copy(pick(option, defaults))
end
local optional_function = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local value = pick(option, actual)
if value ~= nil and type(value) ~= 'function' then
report(string.format('`%s` must be a function or nil', option))
reset(option, actual, defaults)
end
end
local require_boolean = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
if type(pick(option, actual)) ~= 'boolean' then
report(string.format('`%s` must be true or false', option))
reset(option, actual, defaults)
end
end
local require_function_or_string = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local value = pick(option, actual)
if type(value) ~= 'function' and type(value) ~= 'string' then
report(string.format('`%s` must be a function or string', option))
reset(option, actual, defaults)
end
end
local require_one_of = function(option, choices, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local description = ''
local picked = pick(option, actual)
for i, choice in ipairs(choices) do
if picked == choice then
return
end
if i == 1 then
description = "'" .. choice .. "'"
elseif i == #choices then
description = description .. " or '" .. choice .. "'"
else
description = description .. ", '" .. choice .. "'"
end
end
report(string.format('`%s` must be %s', option, description))
reset(option, actual, defaults)
end
local require_positive_integer = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local picked = pick(option, actual)
if not is_integer(picked) or picked < 0 then
report(string.format('`%s` must be a non-negative integer', option))
reset(option, actual, defaults)
end
end
local require_string = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local picked = pick(option, actual)
if type(picked) ~= 'string' then
report(string.format('`%s` must be a string', option))
reset(option, actual, defaults)
end
end
local require_table = function(option, actual, defaults)
actual = actual ~= nil and actual or _options
defaults = defaults ~= nil and defaults or default_options
local picked = pick(option, actual)
if type(picked) ~= 'table' then
report(string.format('`%s` must be a table', option))
reset(option, actual, defaults)
end
end
for k, _ in pairs(options) do
-- `n` is small, so not worried about `O(n)` check.
if not contains(allowed_options, k) then
-- TODO: suggest near-matches for misspelled option names
report('unrecognized option: ' .. k)
end
end
require_boolean('always_show_dot_files')
require_boolean('never_show_dot_files')
if _options.always_show_dot_files == true and _options.never_show_dot_files == true then
report('`always_show_dot_files` and `never_show_dot_files` should not both be true')
reset('always_show_dot_files')
reset('never_show_dot_files')
end
require_boolean('ignore_case')
require_positive_integer('margin')
require_one_of('order', { 'forward', 'reverse' })
require_one_of('position', { 'bottom', 'center', 'top' })
require_table('scanners')
require_table('scanners.git')
require_boolean('scanners.git.submodules')
require_boolean('scanners.git.untracked')
if _options.scanners.git.submodules == true and _options.scanners.git.untracked == true then
report('`scanners.git.submodules` and `scanners.git.untracked` should not both be true')
reset('scanners.git.submodules')
reset('scanners.git.untracked')
end
require_string('selection_highlight')
require_boolean('smart_case')
for name, finder in pairs(options.finders or {}) do
require_function_or_string('finders.' .. name .. '.command', nil, {
finders = {
[name] = {
command = 'true',
},
},
})
optional_function('finders.' .. name .. '.open', nil, {
finders = {
[name] = {},
},
})
for k, _ in pairs(finder) do
if not contains({ 'command', 'open' }, k) then
report(string.format('unrecognized option in `finders.%s`: %s)', name, k))
end
end
end
if
not pcall(function()
local lib = require('wincent.commandt.private.lib') -- We can require it.
lib.epoch() -- We can use it.
end)
then
table.insert(errors, 'unable to load and use C library - run `:checkhealth wincent.commandt`')
end
if #errors > 0 then
table.insert(errors, 1, 'commandt.setup():')
for i, message in ipairs(errors) do
local indent = i == 1 and '' or ' '
errors[i] = { indent .. message .. '\n', 'WarningMsg' }
end
vim.api.nvim_echo(errors, true, {})
end
end
commandt.watchman_finder = function(directory)
directory = vim.trim(directory)
local ui = require('wincent.commandt.private.ui')
local options = commandt.options()
local finder = require('wincent.commandt.private.finders.watchman')(directory, options)
ui.show(finder, merge(options, { name = 'watchman' }))
end
return commandt