I want to create a Neovim plugin that automatically runs a test suite whenever a file is saved. Here's an overview:
If I save a Rust file, run cargo test
If there's not a buffer opened for the test output, automatically create the buffer
Mark that buffer as readonly
If I save another file, reuse the existing buffer
If the buffer is quit (e.g., :q), create a new buffer on the next test run
However, I'm currently facing three issues:
I cannot detect if when I quit the buffer
When the test output is longer than the buffer height, the buffer will not scroll down
When I want to quit Neovim, it asks me to save all these temporary buffers (which I don't want to do)
How can I resolve these issues? For reference, here's my code:
local buffer_number = -1
local function log(_, data)
if data then
vim.api.nvim_buf_set_lines(buffer_number, -1, -1, true, data)
end
end
local function open_buffer()
if buffer_number == -1 then
vim.api.nvim_command('botright vnew')
buffer_number = vim.api.nvim_get_current_buf()
end
end
local function autotest(pattern, command)
vim.api.nvim_create_autocmd("BufWritePost", {
group = vim.api.nvim_create_augroup("autotest", { clear = true }),
pattern = pattern,
callback = function()
open_buffer()
vim.api.nvim_buf_set_lines(buffer_number, 0, -1, true, {})
vim.fn.jobstart(command, {
stdout_buffered = true,
on_stdout = log,
on_stderr = log,
})
end
})
end
autotest("*.rs", { "cargo", "test" })
P.S. I know there are several existing plugins for testing. I'm creating my own because I want to learn how to write neovim plugins.
I gave it my best shot. I believe this addresses your questions and does what you're aiming to do.
local buffer_number = -1
local function log(_, data)
if data then
-- Make it temporarily writable so we don't have warnings.
vim.api.nvim_buf_set_option(buffer_number, "readonly", false)
-- Append the data.
vim.api.nvim_buf_set_lines(buffer_number, -1, -1, true, data)
-- Make readonly again.
vim.api.nvim_buf_set_option(buffer_number, "readonly", true)
-- Mark as not modified, otherwise you'll get an error when
-- attempting to exit vim.
vim.api.nvim_buf_set_option(buffer_number, "modified", false)
-- Get the window the buffer is in and set the cursor position to the bottom.
local buffer_window = vim.api.nvim_call_function("bufwinid", { buffer_number })
local buffer_line_count = vim.api.nvim_buf_line_count(buffer_number)
vim.api.nvim_win_set_cursor(buffer_window, { buffer_line_count, 0 })
end
end
local function open_buffer()
-- Get a boolean that tells us if the buffer number is visible anymore.
--
-- :help bufwinnr
local buffer_visible = vim.api.nvim_call_function("bufwinnr", { buffer_number }) ~= -1
if buffer_number == -1 or not buffer_visible then
-- Create a new buffer with the name "AUTOTEST_OUTPUT".
-- Same name will reuse the current buffer.
vim.api.nvim_command("botright vsplit AUTOTEST_OUTPUT")
-- Collect the buffer's number.
buffer_number = vim.api.nvim_get_current_buf()
-- Mark the buffer as readonly.
vim.opt_local.readonly = true
end
end
function autotest(pattern, command)
vim.api.nvim_create_autocmd("BufWritePost", {
pattern = pattern,
callback = function()
-- Open our buffer, if we need to.
open_buffer()
-- Clear the buffer's contents incase it has been used.
vim.api.nvim_buf_set_lines(buffer_number, 0, -1, true, {})
-- Run the command.
vim.fn.jobstart(command, {
stdout_buffered = true,
on_stdout = log,
on_stderr = log,
})
end
})
end
autotest("*.rs", { "cargo", "test" })
I added some comments to help explain each step but specifically for each of your problems:
I cannot detect if when I quit the buffer
Since you already have the buffer number, you can query to see if it's still visible. Just by using the same name when creating the new buffer we an reuse any existing ones.
When the test output is longer than the buffer height, the buffer will not scroll down
This was tricky, and I'm not sure if it's the best way, but I was able to query how many lines the buffer had and set the cursor of the buffer's window to the bottom.
When I want to quit Neovim, it asks me to save all these temporary buffers (which I don't want to do)
This is because the buffers are modified. Neovim by default doesn't want to make it easy to accidentally lose your work if you have active modifications. To get around that we can easily mark that it isn't modified!
Hope that helps.
Related
I have an auto command highlight words on cursor hold and this autocmd gets attached to buffer when the LSP server has the capability of documentFormattingProvider.
After updating neovim to 0.81 and all the plugins and language servers. The behaviour of this autocmd is buggy.
on svelte files, it does not work and actually outputs error: method textDocument/documentHighlight is not supported by any of the servers registered for the current buffer (lsp:svelte)
on python, no highlighting, no errors, but running lua vim.lsp.buf.document_highlight() on cmd line while cursor on repetitive word, it does highlight (lsp:pyright,black)
on go, it is working! (lsp:gopls)
on typescript files, it is working (lsp:tsserver)
on dart it is working (lsp:dartls)
Here is my dotfiles
local function lsp_highlight_document(client)
-- Set autocommands conditional on server_capabilities
if client.server_capabilities.documentFormattingProvider then
vim.api.nvim_exec(
[[
augroup lsp_document_highlight
autocmd! * <buffer>
autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight()
autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references()
augroup END
]],
false
)
end
end
I also tried (also not working):
local function lsp_highlight_document(client)
-- Set autocommands conditional on server_capabilities
if client.server_capabilities.documentFormattingProvider then
vim.api.nvim_create_augroup("lsp_document_highlight", { clear = true })
vim.api.nvim_clear_autocmds({ buffer = bufnr, group = "lsp_document_highlight" })
vim.api.nvim_create_autocmd("CursorHold", {
callback = vim.lsp.buf.document_highlight,
buffer = bufnr,
group = "lsp_document_highlight",
desc = "Document Highlight",
})
vim.api.nvim_create_autocmd("CursorMoved", {
callback = vim.lsp.buf.clear_references,
buffer = bufnr,
group = "lsp_document_highlight",
desc = "Clear All the References",
})
end
end
Both of these functions gets called here
options.on_attach = function(client, bufnr)
if client.name == "tsserver" then
client.server_capabilities.documentFormattingProvider = true
end
if client.name == "sumneko_lua" then
client.server_capabilities.documentFormattingProvider = false
end
lsp_highlight_document(client)
lsp_keymaps(bufnr)
end
local capabilities = vim.lsp.protocol.make_client_capabilities()
local status_ok, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp")
if not status_ok then
return
end
options.capabilities = cmp_nvim_lsp.default_capabilities(capabilities)
return options
Prior to this update, it was working fine in every language. Not sure what is missing.
Here is my config, which works for pyright, pylsp, lua-language-server etc.:
local api = vim.api
local lsp = vim.lsp
if client.server_capabilities.documentHighlightProvider then
vim.cmd([[
hi! link LspReferenceRead Visual
hi! link LspReferenceText Visual
hi! link LspReferenceWrite Visual
]])
local gid = api.nvim_create_augroup("lsp_document_highlight", { clear = true })
api.nvim_create_autocmd("CursorHold" , {
group = gid,
buffer = bufnr,
callback = function ()
lsp.buf.document_highlight()
end
})
api.nvim_create_autocmd("CursorMoved" , {
group = gid,
buffer = bufnr,
callback = function ()
lsp.buf.clear_references()
end
})
end
Works well on nvim 0.8.1. Try it to see if it works. If it does not work, I think there is something wrong with your config, you need to dig deeper to find why.
ALE has an API for sending errors to it from other sources. I'm using this like shown below and it works for the first error. More specifically, if I make one edit that results in an LSP error, the error will be displayed by ALE in the location list. If I make any further keystrokes, the location list is emptied again.
I can also trigger this behavior if I disable LSP, load ALE, manually call ShowResults and then press any other key in insert mode.
My hypothesis is that ALEs linting in insert mode (per default) kicks in. If LSP is disabled and there are no linters registered for the current file type, it doesn't find any errors (obviously, there's nothing that could report any) and so it empties my location list again. So the steps are: open buffer without LSP, call ShowResults, location list opens, press i, press any key, location list is empty
Now I thought that it was because I wasn't implementing the full ALE API. So I added a hook (2nd snippet). I can verify that this hook is called and that it generates valid messages. If I keep the location list open I can even see all the expected errors flickering across the loclist. I can navigate to those errors with :lolder, but I don't know why ALE always adds another, empty location list, after the LSP is done doing its job.
Or maybe I'm doing something wrong here.
vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, _, params, client_id, _, config)
local uri = params.uri
local bufnr = vim.uri_to_bufnr(uri)
if not bufnr then
return
end
local diagnostics = params.diagnostics
if not diagnostics or vim.tbl_isempty(diagnostics) then
vim.fn['ale#other_source#ShowResults'](bufnr, "nvim-lsp", {})
return
end
-- Important so we can pull diagnostics from this table when ALE asks for
-- them
vim.lsp.diagnostic.save(diagnostics, bufnr, client_id)
local messages = {}
for _, event in ipairs(diagnostics) do
-- :h ale-localist-format
local msg = {}
msg.text = event.message
msg.lnum = event.range.start.line
msg.end_lnum = event.range["end"].line
msg.col = event.range.start.character
msg.end_col = event.range["end"].character
msg.bufnr = bufnr
msg.nr = event.severity
table.insert(messages, msg)
end
vim.fn['ale#other_source#ShowResults'](bufnr, "nvim-lsp", messages)
end
Second snippet which is called from the 'on_attach' function of the Neovim LSP
function ALEHook(bufnr)
vim.fn['ale#other_source#StartChecking'](bufnr, "nvim-lsp")
local diagnostics = vim.lsp.diagnostic.get(bufnr, client.id)
if not diagnostics or vim.tbl_isempty(diagnostics) then
vim.fn['ale#other_source#ShowResults'](bufnr, "nvim-lsp", {})
return
end
local messages = {}
for _, event in ipairs(diagnostics) do
local msg = {}
msg.text = event.message
msg.lnum = event.range.start.line
msg.end_lnum = event.range["end"].line
msg.col = event.range.start.character
msg.end_col = event.range["end"].character
msg.bufnr = bufnr
msg.nr = event.severity
table.insert(messages, msg)
end
vim.fn['ale#other_source#ShowResults'](bufnr, "nvim-lsp", messages)
end
api.nvim_command('augroup ALEHookLSP')
api.nvim_command('autocmd!')
api.nvim_command('autocmd User ALEWantResults call v:lua.ALEHook(g:ale_want_results_buffer)')
api.nvim_command('augroup END')
I'm trying to write a program to shift the key of a midi file. Basically, I just need to shift every note event by a given amount and live the rest unchanged. I found it easy to use MIKMIDI to read, parse, modify and write back the stream.
Unfortunately, I have a problem that I'm unable to solve. I've a loop in which I select the note events and add/subtract the desired shift value, but when I append the event in the output track I get a message from the MIKMIDI library:
"Warning: attempted to insert a NULL event".
The code I wrote is the following:
for event in inputTrack.events {
if event.eventType == .midiNoteMessage {
var tmpData = event.data
if (event.data[0] != 9) { // skip percussion channel
tmpData[1] = event.data[1] - shift
}
let outEvent = MIKMIDIEvent(timeStamp: event.timeStamp, midiEventType: .midiNoteMessage, data: tmpData)!
outputSeq.tracks[i].events.append(outEvent)
}
else {
outSeq.tracks[i].events.append(event)
}
}
BTW, the code works perfectly (the midi file is plays as expected), it is just that it takes minutes to execute in debugging mode due to the infinite sequence of warning messages printed in the debug screen.
Thanks!
In a mongo shell window, I'd like to periodically run a script that will display various stats on the database activity, before displaying the stats, I'd like to clear the screen. There is a "cls" command in the mongo shell, but I am not able to execute it from within the javascript.
function stats () {
while(1) {
cls;
print("display stats");
sleep(5000);
}}
The line with the "cls" is not recognized.
Thank you for any suggestions,
Gary
At the first glance it seemed that you won't be able to do it. According to the docs here: "You cannot use any shell helper (e.g. use , show dbs, etc.) inside the JavaScript file because they are not valid JavaScript.".
One option was to fill the screen with empty lines:
function clearIt () { for(var i = 0; i < 100; i++) { print() } }
clearIt()
However, thanks to #NeilLunn pointing it out there seems to be a solution:
function clearIt () { run('clear') }
clearIt()
This would execute system command which will clear your terminal screen. I don't know how reliable it is (see man clear -> depends if it can figure out how to clear screen) and this works only on POSIX systems. On Windows you would have to replace clear with cls:
function clearIt () { run('cls') }
Additional:
I looked up the source code of mongo shell (src/mongo/shell/linenoise.cpp). Here is how it clears the screen:
void linenoiseClearScreen( void ) {
#ifdef _WIN32
COORD coord = {0, 0};
CONSOLE_SCREEN_BUFFER_INFO inf;
HANDLE screenHandle = GetStdHandle( STD_OUTPUT_HANDLE );
GetConsoleScreenBufferInfo( screenHandle, &inf );
SetConsoleCursorPosition( screenHandle, coord );
DWORD count;
FillConsoleOutputCharacterA( screenHandle, ' ', inf.dwSize.X * inf.dwSize.Y, coord, &count );
#else
if ( write( 1, "\x1b[H\x1b[2J", 7 ) <= 0 ) return;
#endif
}
In case you feel like trying to implement your own screen cleaning function by filling screen with chars.
> help admin
ls([path]) list files
pwd() returns current directory
listFiles([path]) returns file list
hostname() returns name of this host
cat(fname) returns contents of text file as a string
removeFile(f) delete a file or directory
load(jsfilename) load and execute a .js file
run(program[, args...]) spawn a program and wait for its completion
runProgram(program[, args...]) same as run(), above
sleep(m) sleep m milliseconds
getMemInfo() diagnostic
This shows the run and runProgram commands along with some other helpers. The program argument is a string.
I'm trying to write a very simple program to replace an existing executable. It should munge its arguments slightly and exec the original program with the new arguments. It's supposed to be invoked automatically and silently by a third-party library.
It runs fine, but it pops up a console window to show the output of the invoked program. I need that console window to not be there. I do not care about the program's output.
My original attempt was set up as a console application, so I thought I could fix this by writing a new Windows GUI app that did the same thing. But it still pops up the console. I assume that the original command is marked as a console application, and so Windows automatically gives it a console window to run in. I also tried replacing my original call to _exec() with a call to system(), just in case. No help.
Does anyone know how I can make this console window go away?
Here's my code:
int APIENTRY _tWinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
char* lpCmdLine,
int nCmdShow)
{
char *argString, *executable;
// argString and executable are retrieved here
std::vector< std::string > newArgs;
// newArgs gets set up with the intended arguments here
char const ** newArgsP = new char const*[newArgs.size() + 1];
for (unsigned int i = 0; i < newArgs.size(); ++i)
{
newArgsP[i] = newArgs[i].c_str();
}
newArgsP[newArgs.size()] = NULL;
int rv = _execv(executable, newArgsP);
if (rv)
{
return -1;
}
}
Use the CreateProcess function instead of execve. For the dwCreationFlags paramter pass the CREATE_NO_WINDOW flag. You will also need to pass the command line as a string as well.
e.g.
STARTUPINFO startInfo = {0};
PROCESS_INFORMATION procInfo;
TCHAR cmdline[] = _T("\"path\\to\\app.exe\" \"arg1\" \"arg2\"");
startInfo.cb = sizeof(startInfo);
if(CreateProcess(_T("path\\to\\app.exe"), cmdline, NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, NULL, &startInfo, &procInfo))
{
CloseHandle(procInfo.hProcess);
CloseHandle(procInfo.hThread);
}
Aha, I think I found the answer on MSDN, at least if I'm prepared to use .NET. (I don't think I'm really supposed to, but I'll ignore that for now.)
System::String^ command = gcnew System::String(executable);
System::Diagnostics::Process^ myProcess = gcnew Process;
myProcess->StartInfor->FileName = command;
myProcess->StartInfo->UseShellExecute = false; //1
myProcess->StartInfo->CreateNowindow = true; //2
myProcess->Start();
It's those two lines marked //1 and //2 that are important. Both need to be present.
I really don't understand what's going on here, but it seems to work.
You need to create a non-console application (i.e. a Windows GUI app). If all this app does is some processing of files or whatever, you won't need to have a WinMain, register any windows or have a message loop - just write your code as for a console app. Of course, you won't be able to use printf et al. And when you come to execute it, use the exec() family of functions, not system().