feat: run an attempt based on visual selection
having enjoyed attempt.nvim for quite a while i wanted to upgrade my workflow a little. i ended up writing some code using attempt.nvim's api to run a visual selection of text as code in a selected language. now i'm enjoying attempt.nvim even more because this was deceptively simple to implement, but still had a few caveats to look out for.
the feature request is to add this to the plugin itself for others to enjoy. making this issue to see if its within scope for attempt.nvim. i could probably make a PR of this implementation but it might be smart to reconsider how to do this exactly.
the basic idea of what i'm doing here is:
- get the currently highlighted text
- create a new attempt buffer and put that text into that buffer
- run the attempt based on the normal attempt config
- delete the buffer after running (by delaying for 3 seconds)
this is a dump of my implementation. keep in mind its just code which makes it work for me and what i consider to be good enough. there are a few hitches to consider and i'm willing to elaborate in the comments if needed.
local attempt = require("attempt")
local function get_visual_selection(start_pos, end_pos)
local lines = vim.fn.getline(start_pos[2], end_pos[2])
if not lines then
return {}
end
-- Adjust for partial lines
lines[#lines] = string.sub(lines[#lines], 1, end_pos[3])
lines[1] = string.sub(lines[1], start_pos[3], -1)
return lines
end
local function trim_left_based_on_first_line(lines)
local first_line = lines[1]
local leading_whitespace = first_line:match("^(%s*)")
local trim_len = #leading_whitespace
local trimmed = {}
for _, line in ipairs(lines) do
local trimmed_line = line
if #line >= trim_len then
trimmed_line = line:sub(trim_len + 1)
end
table.insert(trimmed, trimmed_line)
end
return trimmed
end
local attempt_config = require("attempt.config").opts
local function run_inline_attempt()
if not attempt_config then
attempt_config = require("attempt.config").opts
if not attempt_config then
vim.notify("Attempt configuration not found", vim.log.levels.ERROR)
return
end
end
local current_buf = vim.api.nvim_get_current_buf()
vim.cmd("normal! gv")
local start_pos = vim.fn.getpos("'<")
local end_pos = vim.fn.getpos("'>")
if start_pos[2] == 0 or end_pos[2] == 0 then
vim.notify("No visual selection found", vim.log.levels.WARN)
return
end
local lines = get_visual_selection(start_pos, end_pos)
if #lines == 0 then
vim.notify("No lines selected", vim.log.levels.WARN)
return
end
lines = trim_left_based_on_first_line(lines)
local options = attempt_config.ext_options
vim.ui.select(options, {}, function(choice)
if not choice then
return
end
local autoformat_disabled = vim.g.disable_autoformat or false
vim.g.disable_autoformat = true
attempt.new({
ext = choice,
}, function(file_entry)
local path = file_entry.path
local bufnr = vim.fn.bufnr(path, false)
vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
vim.api.nvim_buf_call(bufnr, function()
vim.cmd("write")
end)
attempt.run(bufnr)
vim.api.nvim_set_current_buf(current_buf)
-- some commands takes longer to execute so we can't delete the file immediately
vim.defer_fn(function()
attempt.delete_buf(true, bufnr)
end, 3000)
vim.g.disable_autoformat = autoformat_disabled
end)
end)
end
vim.keymap.set("x", "<leader>AR", run_inline_attempt, { desc = "Run Selection as Attempt" })
I'm glad you're enjoying the plugin!
I really like the idea. I'm not sure I'd include all of this in the plugin (feels a bit too custom, and this plugin to me is more about providing a basic API that people can adapt to their workflow).
However, I think it'd be great to add some function that makes this usecase a bit more ergonomic (something like function run_string(input_string, extension, finish_callback) maybe, that handles the creation/running/deletion of the file, and maybe even a function to easily get the trimmed visual selection), and provide documentation on how to do the rest of it
This way, people could also use it to run code from other sources (clipboard maybe?) and also allows them to define their own logic for selecting the file extension (maybe vim.ui.select can be avoided in some cases)
What do you think?
i totally agree with you, my code here was certainly more as a representation of what could be done. it is very tailored to the flow i wanted and providing a more ergonomic and flexible api here sounds like a good choice. the trimmed visual selection is something i noticed i needed when trying to V-LINE select python code and run it. if it has any indents to begin with it doesn't run properly so that trim_left_based_on_first_line was my solution to that xD.
now this one: run_string(input_string, extension, finish_callback) is interesting because here you pass in the extension which i'm not sure happens on any of the other API, i can see why you'd want that though and if this is absolutely what you want then adding a function for select_extension might be a nice utility function to throw in so people don't have to do what i've done with extracting the keys from the config.
lets say we go for:
-
run_string(input_string, extension, finish_callback)- to run text as code. - optionally
select_extension()- to, well, select the extension fromext_options.- do you prefer it to return the extension or use a callback?
-
trim_input_string(input_string)- to fix stuff like i mentioned for python code or other significant whitespace languages.- this way you can also transform code in registers, clipboard, selection, or any other sources.
- should this be left up to the user?
- should this be run automatically in the
run_string(...)function instead? no need for the user to call it themselves.
with this there is no getting the visual selection in the api, but with the addition of an example in the docs that function can be copy/pasted into their own config. no need to provide it in the api nessesarily.
if i were to rewrite my own stuff with these new apis:
local function run_inline_attempt()
local lines = get_visual_selection()
if lines == nil or #lines == 0 then
vim.notify("No lines selected", vim.log.levels.WARN)
return
end
-- this is where we start using the new api
local ext = attempt.select_extension()
-- selection might have been cancelled
if ext == nil then
vim.notify("No extension selected", vim.log.levels.WARN)
return
end
-- turn the lines into a string
local input_string = ""
for line in lines do
input_string = input_string .. line .. "\n"
end
-- if this isn't automatically handled by run_string
-- input_string = attempt.trim_input_string(input_string)
-- the existing attempt.nvim config should
-- handle the creation of the temporary attempt buffer, running of the code,
-- and deletion of the temporary buffer after execution is complete.
-- preferably not by waiting 3 seconds.
-- what should happen if no callback is provided? attempt has a default
-- callback if i remember correctly where it just prints the output.
attempt.run_string(input_string, ext)
end
this is much more concice for sure, is this what you are aiming for?
it would be interesting to see an example for how this would look when using the clipboard as input instead as a second example we can evaluate towards. i imagine its just a difference of using a get_clipboard_string() instead of get_visual_selection and then not having to concat all the lines into a single string.
another thing to consider here is if you want the input_string to be a single string or if it should be a list of lines. neovim workes with lines almost everywhere as you probably know so it might be easier to just do that instead of transforming between the two back and forth and having to deal with different line endings and what-not.
Hi! Sorry for the delay, I've been super busy as of late
the trimmed visual selection is something i noticed i needed when trying to V-LINE select python code and run it. if it has any indents to begin with it doesn't run properly so that trim_left_based_on_first_line was my solution to that xD.
Yeah hahah I figured that was the reason why. I don't mind including it in the plugin, but it could also be left to the user, no strong feelings regarding that
now this one: run_string(input_string, extension, finish_callback) is interesting because here you pass in the extension which i'm not sure happens on any of the other API, i can see why you'd want that though and if this is absolutely what you want then adding a function for select_extension might be a nice utility function to throw in so people don't have to do what i've done with extracting the keys from the config.
It's only done in :h attempt.new(). I think it could be optional, and if it's not provided it takes the extension from the current file. However, an option would definetly would be nice, because you don't always want to use the same file extension as the source file (think if you have some JS inside an HTML file, you want to run that with the JS runner). Providing a picker using vim.ui.select() sounds good to me
lets say we go for:
run_string(input_string, extension, finish_callback)- to run text as code.- optionally
select_extension()- to, well, select the extension fromext_options.
- do you prefer it to return the extension or use a callback?
trim_input_string(input_string)- to fix stuff like i mentioned for python code or other significant whitespace languages.
- this way you can also transform code in registers, clipboard, selection, or any other sources.
- should this be left up to the user?
- should this be run automatically in the
run_string(...)function instead? no need for the user to call it themselves.
select_extension() will have to take a callback, since vim.ui.select() is async. Regarding trim_input_string, I think I'd leave it to the user just in case? I don't think it's to likely to bother anyone really if it's done automatically inside run_string either, as you prefer really
this is much more concice for sure, is this what you are aiming for?
Yes!
another thing to consider here is if you want the
input_stringto be a single string or if it should be a list oflines. neovim workes withlinesalmost everywhere as you probably know so it might be easier to just do that instead of transforming between the two back and forth and having to deal with different line endings and what-not.
Yeah, I guess it makes sense to try to keep the APIs as similar to nvim's as posible
thanks for getting back to me, i think most question are answered at this point. I'll take a stab at a PR soon :)