-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathtrunk.lua
492 lines (453 loc) · 14.5 KB
/
trunk.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
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
---@diagnostic disable: need-check-nil
-- Config variables
local function is_win()
return package.config:sub(1, 1) == "\\"
end
local trunkPath = is_win() and "trunk.ps1" or "trunk"
local appendArgs = {}
local formatOnSave = true
local formatOnSaveTimeout = 10
local function executionTrunkPath()
if trunkPath:match(".ps1$") then
return { "powershell", "-ExecutionPolicy", "ByPass", trunkPath }
end
return { trunkPath }
end
-- State tracking
local errors = {}
local failures = {}
local notifications = {}
local cliVersion = nil
local logger = require("log")
local math = require("math")
logger.info("Starting")
local function isempty(s)
return s == nil or s == ""
end
local function findWorkspace()
return vim.fs.dirname(vim.fs.find({ ".trunk", ".git" }, { upward = true })[1])
end
local function findConfig()
local configDir = findWorkspace()
logger.info("Found workspace", configDir)
return configDir and configDir .. "/.trunk/trunk.yaml" or ".trunk/trunk.yaml"
end
local function trim(s)
return s:match("^%s*(.-)%s*$")
end
local function getCliVersion()
local possibleVersion = nil
if
pcall(function()
local cmd = executionTrunkPath()
table.insert(cmd, #cmd + 1, "version")
logger.info("cli version command", table.concat(cmd, " "))
local output = vim.fn.systemlist(cmd)
possibleVersion = trim(output[#output])
vim.version.parse(possibleVersion)
end)
then
return possibleVersion
else
-- version is not parsable
logger.warn("Received unparsable version string", possibleVersion)
return nil
end
end
local function checkCliVersion(requiredVersionString)
local version = vim.version
if cliVersion == nil then
logger.info("nil CLI version")
return false
end
local currentVersion = version.parse(cliVersion)
local requiredVersion = version.parse(requiredVersionString)
return not version.lt(currentVersion, requiredVersion)
end
-- Handlers for user commands
local function printFailures()
-- Empty list of a named failure signifies failures have been resolved/cleared
local failure_elements = {}
local detail_array = {}
local index = 1
for name, fails in pairs(failures) do
for _, fail in pairs(fails) do
table.insert(failure_elements, string.format("%d Failure %s: %s", index, name, fail.message))
table.insert(detail_array, fail.detailPath)
index = index + 1
end
end
if #failure_elements > 0 then
logger.info("Failures:", table.concat(failure_elements, ","))
else
logger.info("No failures")
return
end
-- TODO(Tyler): Don't unconditionally depend on telescope
local picker = require("telescope.pickers")
local finders = require("telescope.finders")
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
local attach_callback = function(prompt_bufnr, _)
actions.select_default:replace(function()
actions.close(prompt_bufnr)
local selection = action_state.get_selected_entry()
print(selection[1])
local words = {}
for word in selection[1]:gmatch("%w+") do
table.insert(words, word)
end
local failure_index = tonumber(words[1])
local fileToOpen = detail_array[failure_index]
logger.info("file to open", fileToOpen)
if is_win() then
fileToOpen = fileToOpen:gsub("^file:///", "")
else
fileToOpen = fileToOpen:gsub("^file://", "")
end
fileToOpen = fileToOpen:gsub("%%3A", ":")
logger.info("file to open", fileToOpen)
vim.cmd(":edit " .. fileToOpen)
end)
return true
end
if #failure_elements > 0 then
picker
.new({}, {
prompt_title = "Failures",
results_title = "Open failure contents",
finder = finders.new_table({
results = failure_elements,
}),
cwd = findWorkspace(),
attach_mappings = attach_callback,
})
:find()
else
print("No failures")
end
end
local function printActionNotifications()
-- TODO(Tyler): Don't unconditionally depend on telescope
local picker = require("telescope.pickers")
local finders = require("telescope.finders")
local actions = require("telescope.actions")
local action_state = require("telescope.actions.state")
for _, v in pairs(notifications) do
-- This output is replaced with the picker
print(string.format("%s:\n%s", v.title, v.message))
if #v.commands > 0 then
local commands = {}
for _, command in pairs(v.commands) do
table.insert(commands, command.run)
end
local attach_callback = function(prompt_bufnr, _)
actions.select_default:replace(function()
actions.close(prompt_bufnr)
local selection = action_state.get_selected_entry()
local command = selection[1]:gsub("^trunk", table.concat(executionTrunkPath(), " "))
-- Remove ANSI coloring from action messages
vim.cmd(":!" .. command .. [[ | sed -e 's/\x1b\[[0-9;]*m//g']])
end)
return true
end
picker
.new({}, {
prompt_title = v.title,
results_title = "Commands",
finder = finders.new_table({
results = commands,
}),
cwd = findWorkspace(),
attach_mappings = attach_callback,
})
:find()
end
end
end
local function checkQuery()
local currentPath = vim.api.nvim_buf_get_name(0)
if not isempty(currentPath) then
local workspace = findWorkspace()
if isempty(workspace) then
print("Must be inside a Trunk workspace to run this command")
else
local relativePath = string.sub(currentPath, #workspace + 2)
vim.cmd("!" .. table.concat(executionTrunkPath(), " ") .. " check query " .. relativePath)
end
end
end
-- LSP client lifetime control
local function connect()
local cmd = executionTrunkPath()
table.insert(cmd, "lsp-proxy")
for _, e in pairs(appendArgs) do
table.insert(cmd, e)
end
logger.debug("Launching " .. table.concat(cmd, " "))
local workspace = findWorkspace()
if not isempty(workspace) then
return vim.lsp.start({
name = "neovim-trunk",
cmd = cmd,
root_dir = workspace,
init_options = {
-- *** OFFICIAL VERSION OF PLUGIN IS IDENTIFIED HERE ***
version = "0.1.0",
clientType = "neovim",
-- Based on version parsing here https://github.com/neovim/neovim/issues/23863
clientVersion = vim.split(vim.fn.execute("version"), "\n")[3]:sub(6),
},
handlers = {
-- We must identify handlers for the other events we will receive but don't handle.
["$trunk/publishFileWatcherEvent"] = function(_err, _result, _ctx, _config)
-- We don't handle file watcher events in neovim
end,
["$trunk/publishNotification"] = function(_err, result, _ctx, _config)
logger.info("Action notification received")
for _, v in pairs(result.notifications) do
table.insert(notifications, v)
end
end,
["$trunk/log.Error"] = function(err, result, ctx, config)
-- TODO(Tyler): Clear and surface these in a meaningful way
logger.error(err, result, ctx, config)
table.insert(errors, ctx.params)
end,
["$trunk/publishFailures"] = function(_err, result, _ctx, _config)
logger.info("Failure received")
-- TODO(Tyler): Consider removing this print, it can sometimes be obtrusive.
print("Trunk failure occurred. Run :TrunkStatus to view")
if #result.failures > 0 then
failures[result.name] = result.failures
else
-- This empty failure list is sent when the clear button in VSCode is hit.
-- TODO(Tyler): Add in the ability to clear failures during a session.
table.remove(failures, result.name)
end
end,
["$/progress"] = function(_err, _result, _ctx, _config)
-- TODO(Tyler): Conditionally add a progress bar pane?
end,
},
-- custom callbacks for commands from code actions
commands = {
["trunk.checkEnable"] = function(command, _client_info, _command_str, _args)
-- TODO(Tyler): Use non-ANSI mode
vim.cmd(
"!"
.. table.concat(executionTrunkPath(), " ")
.. " check enable "
.. table.concat(command["arguments"])
.. [[ | sed -e 's/\x1b\[[0-9;]*m//g']]
)
end,
["trunk.checkDisable"] = function(command, _client_info, _command_str, _args)
-- TODO(Tyler): Use non-ANSI mode
vim.cmd(
"!"
.. table.concat(executionTrunkPath(), " ")
.. " check disable "
.. table.concat(command["arguments"])
.. [[ | sed -e 's/\x1b\[[0-9;]*m//g']]
)
end,
["trunk.openConfigFile"] = function(_command, _client_info, _command_str, _args)
vim.cmd(":edit " .. findConfig())
end,
},
})
end
end
-- Startup, including attaching autocmds
local function start()
cliVersion = getCliVersion()
logger.info("Running on CLI version:", cliVersion)
if cliVersion == nil then
logger.error("nil CLI version")
print(
"The Trunk Neovim extension requires Trunk CLI version >= 1.17.0 - we could not determine your Trunk CLI version."
)
print("The extension will not run until you upgrade your CLI version.")
print("Please run `trunk upgrade` to get the latest improvements and fixes for Neovim.")
return
end
if cliVersion ~= "0.0.0-rc" then
if not checkCliVersion("1.17.0") then
logger.error("Trunk CLI version must be >= 1.17.0")
print(
"The Trunk Neovim extension requires Trunk CLI version >= 1.17.0 - you currently have "
.. cliVersion
.. "."
)
print("The extension will not run until you upgrade your CLI version.")
print("Please run `trunk upgrade` to get the latest improvements and fixes for Neovim.")
return
end
if not checkCliVersion("1.17.2-beta.5") then
logger.warn("Trunk CLI version should be >= 1.17.2")
print("Detected stale Trunk CLI version " .. cliVersion .. ".")
print(" Please run `trunk upgrade` to get the latest improvements and fixes for Neovim.")
end
end
logger.info("Setting up autocmds")
local autocmd = vim.api.nvim_create_autocmd
autocmd("FileType", {
pattern = "*",
callback = function()
local bufname = vim.api.nvim_buf_get_name(0)
logger.debug("Buffer filename: " .. bufname)
local fs = vim.fs
local findResult = fs.find(fs.basename(bufname), { path = fs.dirname(bufname) })
-- Checks that the opened buffer actually exists and isn't a directory, else trunk crashes
if #findResult == 0 or vim.fn.isdirectory(bufname) ~= 0 then
return
end
logger.debug("Attaching to new buffer")
-- This attaches the existing client since it is keyed by name
local client = connect()
if client ~= nil then
vim.lsp.buf_attach_client(0, client)
end
end,
})
autocmd("BufWritePre", {
pattern = "*",
callback = function()
if formatOnSave then
logger.debug("Running fmt on save callback")
-- older LSP clients did not advertise documentFormattingProvider
if cliVersion == "0.0.0-rc" or checkCliVersion("1.21.1-beta.14") then
vim.lsp.buf.format({
async = false,
timeout_ms = formatOnSaveTimeout * 1000,
name = "neovim-trunk",
})
else
local cursor = vim.api.nvim_win_get_cursor(0)
local filename = vim.api.nvim_buf_get_name(0)
local workspace = findWorkspace()
if is_win() then
workspace = workspace:gsub("/", "\\")
end
-- if filename doesn't start with workspace
if workspace == nil or filename:sub(1, #workspace) ~= workspace then
return
end
local handle = io.popen("command -v timeout")
local timeoutResult = handle:read("*a")
handle:close()
-- Stores current buffer in a temporary file in case trunk fmt fails so we don't overwrite the original buffer with an error message.
local tmpFile = os.tmpname()
local tmpFormattedFile = os.tmpname()
local trunkFormatCmd = table.concat(executionTrunkPath(), " ") .. " format-stdin %:p"
if checkCliVersion("1.17.2-beta.5") then
logger.info("using --output-file")
trunkFormatCmd = trunkFormatCmd .. " --output-file=" .. tmpFormattedFile
if is_win() then
trunkFormatCmd = "(" .. trunkFormatCmd .. ") >$null"
else
trunkFormatCmd = trunkFormatCmd .. " 1>/dev/null 2>/dev/null"
end
else
trunkFormatCmd = trunkFormatCmd .. " > " .. tmpFormattedFile
end
local formatCommand = ""
if is_win() then
logger.debug("Formatting on Windows")
-- TODO(Tyler): Handle carriage returns correctly here.
-- NOTE(Tyler): Powershell does not have && and || so we must use cmd /c
formatCommand = (
':% ! cmd /c "tee '
.. tmpFile
.. " | "
.. trunkFormatCmd
.. " && cat "
.. tmpFormattedFile
.. " || cat "
.. tmpFile
.. '"'
)
elseif timeoutResult:len() == 0 then
logger.debug("Formatting without timeout")
formatCommand = (
":% !tee "
.. tmpFile
.. " | "
.. trunkFormatCmd
.. " && cat "
.. tmpFormattedFile
.. " || cat "
.. tmpFile
)
else
logger.debug("Formatting with timeout")
formatCommand = (
":% !tee "
.. tmpFile
.. " | timeout "
.. formatOnSaveTimeout
.. " "
.. trunkFormatCmd
.. " && cat "
.. tmpFormattedFile
.. " || cat "
.. tmpFile
)
end
logger.debug("Format command: " .. formatCommand)
vim.cmd(formatCommand)
local line_count = vim.api.nvim_buf_line_count(0)
os.remove(tmpFile)
os.remove(tmpFormattedFile)
vim.api.nvim_win_set_cursor(0, { math.min(cursor[1], line_count), cursor[2] })
end
end
end,
})
end
-- Setup config variables
local function setup(opts)
logger.info("Performing setup", opts)
if not isempty(opts.logLevel) then
logger.log_level = opts.logLevel
logger.debug("Overrode loglevel with", opts.logLevel)
end
if not isempty(opts.trunkPath) then
logger.debug("Overrode trunkPath with", opts.trunkPath)
trunkPath = opts.trunkPath
end
if not isempty(opts.lspArgs) and #opts.lspArgs > 0 then
logger.debug("Overrode lspArgs with", table.concat(opts.lspArgs, " "))
appendArgs = opts.lspArgs
end
if not isempty(opts.formatOnSave) then
logger.debug("Overrode formatOnSave with", opts.formatOnSave)
formatOnSave = opts.formatOnSave
end
if not isempty(opts.formatOnSaveTimeout) then
logger.debug("Overrode formatOnSaveTimeout with", opts.formatOnSaveTimeout)
formatOnSave = opts.formatOnSaveTimeout
end
start()
end
local function openLogs()
local out_file = require("log").path
-- make sure this file exists
if not vim.fn.filereadable(out_file) then
logger.error("Log file does not exist")
print("We cannot open logs because of an internal error. The log module is somehow busted")
return
end
vim.cmd(":e " .. out_file)
end
-- Lua handles for plugin commands and setup
return {
start = start,
findConfig = findConfig,
setup = setup,
printStatus = printFailures,
actions = printActionNotifications,
checkQuery = checkQuery,
openLogs = openLogs,
}