feat: automatic suggestion (smart tab) (#455)

This commit is contained in:
yetone 2024-09-03 14:03:59 +08:00 committed by GitHub
parent 962dd0a759
commit 65e1e178f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 592 additions and 43 deletions

View File

@ -192,6 +192,13 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_
temperature = 0,
max_tokens = 4096,
},
behaviour = {
auto_suggestions = false, -- Experimental stage
auto_set_highlight_group = true,
auto_set_keymaps = true,
auto_apply_diff_after_generation = false,
support_paste_from_clipboard = false,
},
mappings = {
--- @class AvanteConflictMappings
diff = {
@ -203,6 +210,12 @@ _See [config.lua#L9](./lua/avante/config.lua) for the full config_
next = "]x",
prev = "[x",
},
suggestion = {
accept = "<M-l>",
next = "<M-]>",
prev = "<M-[>",
dismiss = "<C-]>",
},
jump = {
next = "]]",
prev = "[[",

View File

@ -45,8 +45,14 @@ M.edit = function(question)
end
end
---@return avante.Suggestion | nil
M.get_suggestion = function()
local _, _, suggestion = require("avante").get()
return suggestion
end
M.refresh = function()
local sidebar, _ = require("avante").get()
local sidebar = require("avante").get()
if not sidebar then
return
end

View File

@ -86,6 +86,7 @@ M.defaults = {
---3. auto_set_highlight_group : Whether to automatically set the highlight group for the current line. Default to true.
---4. support_paste_from_clipboard : Whether to support pasting image from clipboard. This will be determined automatically based whether img-clip is available or not.
behaviour = {
auto_suggestions = false, -- Experimental stage
auto_set_highlight_group = true,
auto_set_keymaps = true,
auto_apply_diff_after_generation = false,
@ -116,6 +117,12 @@ M.defaults = {
next = "]x",
prev = "[x",
},
suggestion = {
accept = "<M-l>",
next = "<M-]>",
prev = "<M-[>",
dismiss = "<C-]>",
},
jump = {
next = "]]",
prev = "[[",

View File

@ -11,6 +11,8 @@ local Highlights = {
REVERSED_SUBTITLE = { name = "AvanteReversedSubtitle", fg = "#56b6c2" },
THIRD_TITLE = { name = "AvanteThirdTitle", fg = "#ABB2BF", bg = "#353B45" },
REVERSED_THIRD_TITLE = { name = "AvanteReversedThirdTitle", fg = "#353B45" },
SUGGESTION = { name = "AvanteSuggestion", link = "Comment" },
ANNOTATION = { name = "AvanteAnnotation", link = "Comment" },
}
Highlights.conflict = {
@ -48,7 +50,7 @@ M.setup = function()
end)
:each(function(_, hl)
if not has_set_colors(hl.name) then
api.nvim_set_hl(0, hl.name, { fg = hl.fg or nil, bg = hl.bg or nil })
api.nvim_set_hl(0, hl.name, { fg = hl.fg or nil, bg = hl.bg or nil, link = hl.link or nil })
end
end)

View File

@ -3,6 +3,7 @@ local api = vim.api
local Utils = require("avante.utils")
local Sidebar = require("avante.sidebar")
local Selection = require("avante.selection")
local Suggestion = require("avante.suggestion")
local Config = require("avante.config")
local Diff = require("avante.diff")
@ -12,8 +13,10 @@ local M = {
sidebars = {},
---@type avante.Selection[]
selections = {},
---@type {sidebar?: avante.Sidebar, selection?: avante.Selection}
current = { sidebar = nil, selection = nil },
---@type avante.Suggestion[]
suggestions = {},
---@type {sidebar?: avante.Sidebar, selection?: avante.Selection, suggestion?: avante.Suggestion}
current = { sidebar = nil, selection = nil, suggestion = nil },
}
M.did_setup = false
@ -185,7 +188,7 @@ H.autocmds = function()
api.nvim_create_autocmd("VimResized", {
group = H.augroup,
callback = function()
local sidebar, _ = M.get()
local sidebar = M.get()
if not sidebar then
return
end
@ -234,22 +237,25 @@ H.autocmds = function()
end
---@param current boolean? false to disable setting current, otherwise use this to track across tabs.
---@return avante.Sidebar, avante.Selection
---@return avante.Sidebar, avante.Selection, avante.Suggestion
function M.get(current)
local tab = api.nvim_get_current_tabpage()
local sidebar = M.sidebars[tab]
local selection = M.selections[tab]
local suggestion = M.suggestions[tab]
if current ~= false then
M.current.sidebar = sidebar
M.current.selection = selection
M.current.suggestion = suggestion
end
return sidebar, selection
return sidebar, selection, suggestion
end
---@param id integer
function M._init(id)
local sidebar = M.sidebars[id]
local selection = M.selections[id]
local suggestion = M.suggestions[id]
if not sidebar then
sidebar = Sidebar:new(id)
@ -259,7 +265,11 @@ function M._init(id)
selection = Selection:new(id)
M.selections[id] = selection
end
M.current = { sidebar = sidebar, selection = selection }
if not suggestion then
suggestion = Suggestion:new(id)
M.suggestions[id] = suggestion
end
M.current = { sidebar = sidebar, selection = selection, suggestion = suggestion }
return M
end
@ -288,7 +298,7 @@ M.toggle.hint = H.api(Utils.toggle_wrap({
setmetatable(M.toggle, {
__index = M.toggle,
__call = function()
local sidebar, _ = M.get()
local sidebar = M.get()
if not sidebar then
M._init(api.nvim_get_current_tabpage())
M.current.sidebar:open()
@ -326,7 +336,7 @@ M.build = H.api(function()
local os_name = Utils.get_os_name()
if vim.tbl_contains({ "linux", "darwin" }, os_name) then
cmd = { "sh", "-c", ("make -C %s"):format(build_directory) }
cmd = { "sh", "-c", string.format("make -C %s", build_directory) }
elseif os_name == "windows" then
build_directory = to_windows_path(build_directory)
cmd = {
@ -334,7 +344,7 @@ M.build = H.api(function()
"-ExecutionPolicy",
"Bypass",
"-File",
("%s\\Build.ps1"):format(build_directory),
string.format("%s\\Build.ps1", build_directory),
"-WorkingDirectory",
build_directory,
}

View File

@ -88,8 +88,36 @@ Your task is to modify the provided code according to the user's request. Follow
Remember: Your response should contain ONLY the modified code, ready to be used as a direct replacement for the original file.
]]
local suggesting_mode_user_prompt_tpl = [[
Your task is to suggest code modifications at the cursor position. Follow these instructions meticulously:
1. Carefully analyze the original code, paying close attention to its structure and the cursor position.
2. You must follow this json format when suggesting modifications:
[
{
"row": ${row},
"col": ${column},
"content": "Your suggested code here"
}
]
3. When suggesting suggested code:
- Each element in the returned list is a COMPLETE and INDEPENDENT code snippet.
- MUST be a valid json format. Don't be lazy!
- Only return the new code to be inserted.
- Your returned code should not overlap with the original code in any way. Don't be lazy!
- Please strictly check the code around the position and ensure that the complete code after insertion is correct. Don't be lazy!
- Do not return the entire file content or any surrounding code.
- Do not include any explanations, comments, or line numbers in your response.
- Ensure the suggested code fits seamlessly with the existing code structure and indentation.
- If there are no recommended modifications, return an empty list.
Remember: Return ONLY the suggested code snippet, without any additional formatting or explanation.
]]
local group = api.nvim_create_augroup("avante_llm", { clear = true })
local active_job = nil
---@class StreamOptions
---@field file_content string
@ -99,7 +127,7 @@ local active_job = nil
---@field project_context string | nil
---@field memory_context string | nil
---@field full_file_contents_context string | nil
---@field mode "planning" | "editing"
---@field mode "planning" | "editing" | "suggesting"
---@field on_chunk AvanteChunkParser
---@field on_complete AvanteCompleteParser
@ -108,7 +136,13 @@ M.stream = function(opts)
local mode = opts.mode or "planning"
local provider = Config.provider
local user_prompt_tpl = mode == "planning" and planning_mode_user_prompt_tpl or editing_mode_user_prompt_tpl
local user_prompt_tpl = planning_mode_user_prompt_tpl
if mode == "editing" then
user_prompt_tpl = editing_mode_user_prompt_tpl
elseif mode == "suggesting" then
user_prompt_tpl = suggesting_mode_user_prompt_tpl
end
-- Check if the instructions contains an image path
local image_paths = {}
@ -191,13 +225,10 @@ M.stream = function(opts)
end
end
if active_job then
active_job:shutdown()
active_job = nil
end
local completed = false
local active_job
active_job = curl.post(spec.url, {
headers = spec.headers,
proxy = spec.proxy,
@ -230,11 +261,13 @@ M.stream = function(opts)
end
end)
end,
on_error = function(err)
on_error = function()
active_job = nil
completed = true
opts.on_complete(err)
opts.on_complete(nil)
end,
callback = function(result)
active_job = nil
if result.status >= 400 then
if Provider.on_error then
Provider.on_error(result)
@ -250,16 +283,21 @@ M.stream = function(opts)
end
end)
end
active_job = nil
end,
})
api.nvim_create_autocmd("User", {
group = group,
pattern = M.CANCEL_PATTERN,
once = true,
callback = function()
-- Error: cannot resume dead coroutine
if active_job then
xpcall(function()
active_job:shutdown()
end, function(err)
return err
end)
Utils.debug("LLM request cancelled", { title = "Avante" })
active_job = nil
end
@ -269,4 +307,8 @@ M.stream = function(opts)
return active_job
end
function M.cancel_inflight_request()
api.nvim_exec_autocmds("User", { pattern = M.CANCEL_PATTERN })
end
return M

View File

@ -84,7 +84,7 @@ end
function Selection:close_editing_input()
self:close_editing_input_shortcuts_hints()
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
Llm.cancel_inflight_request()
if api.nvim_get_mode().mode == "i" then
vim.cmd([[stopinsert]])
end

View File

@ -448,7 +448,7 @@ local function insert_conflict_contents(bufnr, snippets)
local snippet_lines = vim.split(snippet.content, "\n")
for idx, line in ipairs(snippet_lines) do
line = line:gsub("^L%d+: ", "")
line = Utils.trim_line_number(line)
if idx == 1 then
local indentation = Utils.get_indentation(line)
need_prepend_indentation = indentation ~= original_start_line_indentation
@ -824,7 +824,7 @@ function Sidebar:on_mount()
self:render_input()
self:render_selected_code()
self.augroup = api.nvim_create_augroup("avante_" .. self.id .. self.result.winid, { clear = true })
self.augroup = api.nvim_create_augroup("avante_sidebar_" .. self.id .. self.result.winid, { clear = true })
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
@ -1040,17 +1040,6 @@ function Sidebar:update_content(content, opts)
return self
end
local function prepend_line_number(content, start_line)
start_line = start_line or 1
local lines = vim.split(content, "\n")
local result = {}
for i, line in ipairs(lines) do
i = i + start_line - 1
table.insert(result, "L" .. i .. ": " .. line)
end
return table.concat(result, "\n")
end
-- Function to get current timestamp
local function get_timestamp()
return os.date("%Y-%m-%d %H:%M:%S")
@ -1253,14 +1242,14 @@ function Sidebar:create_input()
self:update_content(content_prefix .. "🔄 **Generating response ...**\n")
local content = table.concat(Utils.get_buf_lines(0, -1, self.code.bufnr), "\n")
local content_with_line_numbers = prepend_line_number(content)
local content_with_line_numbers = Utils.prepend_line_number(content)
local filetype = api.nvim_get_option_value("filetype", { buf = self.code.bufnr })
local selected_code_content_with_line_numbers = nil
if self.code.selection ~= nil then
selected_code_content_with_line_numbers =
prepend_line_number(self.code.selection.content, self.code.selection.range.start.line)
Utils.prepend_line_number(self.code.selection.content, self.code.selection.range.start.line)
end
if request:sub(1, 1) == "/" then
@ -1289,7 +1278,7 @@ function Sidebar:create_input()
Utils.error("Invalid end line number", { once = true, title = "Avante" })
return
end
selected_code_content_with_line_numbers = prepend_line_number(
selected_code_content_with_line_numbers = Utils.prepend_line_number(
table.concat(api.nvim_buf_get_lines(self.code.bufnr, start_line - 1, end_line, false), "\n"),
start_line
)
@ -1640,12 +1629,12 @@ function Sidebar:render()
end)
self.result:map("n", "q", function()
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
Llm.cancel_inflight_request()
self:close()
end)
self.result:map("n", "<Esc>", function()
api.nvim_exec_autocmds("User", { pattern = Llm.CANCEL_PATTERN })
Llm.cancel_inflight_request()
self:close()
end)

404
lua/avante/suggestion.lua Normal file
View File

@ -0,0 +1,404 @@
local Utils = require("avante.utils")
local Llm = require("avante.llm")
local Highlights = require("avante.highlights")
local Config = require("avante.config")
local api = vim.api
local fn = vim.fn
local SUGGESTION_NS = api.nvim_create_namespace("avante_suggestion")
---@class avante.SuggestionItem
---@field content string
---@field row number
---@field col number
---@class avante.SuggestionContext
---@field suggestions avante.SuggestionItem[]
---@field current_suggestion_idx number
---@field prev_doc? table
---@class avante.Suggestion
---@field id number
---@field augroup integer
---@field extmark_id integer
---@field _timer? table
---@field _contexts table
local Suggestion = {}
---@param id number
---@return avante.Suggestion
function Suggestion:new(id)
local o = { id = id, suggestions = {} }
setmetatable(o, self)
self.__index = self
self.augroup = api.nvim_create_augroup("avante_suggestion_" .. id, { clear = true })
self.extmark_id = 1
self._timer = nil
self._contexts = {}
if Config.behaviour.auto_suggestions then
self:setup_mappings()
self:setup_autocmds()
end
return o
end
function Suggestion:destroy()
self:stop_timer()
self:reset()
self:delete_autocmds()
api.nvim_del_namespace(SUGGESTION_NS)
end
function Suggestion:setup_mappings()
if not Config.behaviour.auto_set_keymaps then
return
end
if Config.mappings.suggestion and Config.mappings.suggestion.accept then
vim.keymap.set("i", Config.mappings.suggestion.accept, function()
self:accept()
end, {
desc = "[avante] accept suggestion",
noremap = true,
silent = true,
})
end
if Config.mappings.suggestion and Config.mappings.suggestion.dismiss then
vim.keymap.set("i", Config.mappings.suggestion.dismiss, function()
if self:is_visible() then
self:dismiss()
end
end, {
desc = "[avante] dismiss suggestion",
noremap = true,
silent = true,
})
end
if Config.mappings.suggestion and Config.mappings.suggestion.next then
vim.keymap.set("i", Config.mappings.suggestion.next, function()
self:next()
end, {
desc = "[avante] next suggestion",
noremap = true,
silent = true,
})
end
if Config.mappings.suggestion and Config.mappings.suggestion.prev then
vim.keymap.set("i", Config.mappings.suggestion.prev, function()
self:prev()
end, {
desc = "[avante] previous suggestion",
noremap = true,
silent = true,
})
end
end
function Suggestion:suggest()
Utils.debug("suggesting")
local ctx = self:ctx()
local doc = Utils.get_doc()
ctx.prev_doc = doc
local bufnr = api.nvim_get_current_buf()
local filetype = api.nvim_get_option_value("filetype", { buf = bufnr })
local code_content =
Utils.prepend_line_number(table.concat(api.nvim_buf_get_lines(bufnr, 0, -1, false), "\n") .. "\n\n")
local full_response = ""
Llm.stream({
file_content = code_content,
code_lang = filetype,
instructions = vim.json.encode(doc),
mode = "suggesting",
on_chunk = function(chunk)
full_response = full_response .. chunk
end,
on_complete = function(err)
if err then
Utils.error("Error while suggesting: " .. vim.inspect(err), { once = true, title = "Avante" })
return
end
Utils.debug("full_response: " .. vim.inspect(full_response))
local cursor_row, cursor_col = Utils.get_cursor_pos()
if cursor_row ~= doc.position.row or cursor_col ~= doc.position.col then
return
end
local ok, suggestions = pcall(vim.json.decode, full_response)
if not ok then
Utils.error("Error while decoding suggestions: " .. full_response, { once = true, title = "Avante" })
return
end
if not suggestions then
Utils.info("No suggestions found", { once = true, title = "Avante" })
return
end
suggestions = vim
.iter(suggestions)
:map(function(s)
return { row = s.row, col = s.col, content = Utils.trim_all_line_numbers(s.content) }
end)
:totable()
ctx.suggestions = suggestions
ctx.current_suggestion_idx = 1
self:show()
end,
})
end
function Suggestion:show()
self:hide()
if not fn.mode():match("^[iR]") then
return
end
local ctx = self:ctx()
local suggestion = ctx.suggestions[ctx.current_suggestion_idx]
if not suggestion then
return
end
local cursor_row, cursor_col = Utils.get_cursor_pos()
if suggestion.row < cursor_row then
return
end
local bufnr = api.nvim_get_current_buf()
local row = suggestion.row
local col = suggestion.col
local content = suggestion.content
local lines = vim.split(content, "\n")
local extmark_col = cursor_col
if cursor_row < row then
extmark_col = 0
end
local current_lines = api.nvim_buf_get_lines(bufnr, 0, -1, false)
if cursor_row == row then
local cursor_line_col = #current_lines[cursor_row] - 1
if cursor_col ~= cursor_line_col then
local current_line = current_lines[cursor_row]
lines[1] = lines[1] .. current_line:sub(col + 1, -1)
end
end
local extmark = {
id = self.extmark_id,
virt_text_win_col = col,
virt_text = { { lines[1], Highlights.SUGGESTION } },
}
if #lines > 1 then
extmark.virt_lines = {}
for i = 2, #lines do
extmark.virt_lines[i - 1] = { { lines[i], Highlights.SUGGESTION } }
end
end
extmark.hl_mode = "combine"
local buf_lines = Utils.get_buf_lines(0, -1, bufnr)
local buf_lines_count = #buf_lines
while buf_lines_count < row do
api.nvim_buf_set_lines(bufnr, buf_lines_count, -1, false, { "" })
buf_lines_count = buf_lines_count + 1
end
api.nvim_buf_set_extmark(bufnr, SUGGESTION_NS, row - 1, extmark_col, extmark)
end
function Suggestion:is_visible()
return not not api.nvim_buf_get_extmark_by_id(0, SUGGESTION_NS, self.extmark_id, { details = false })[1]
end
function Suggestion:hide()
api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id)
end
function Suggestion:ctx()
local bufnr = api.nvim_get_current_buf()
local ctx = self._contexts[bufnr]
if not ctx then
ctx = {
suggestions = {},
current_suggestion_idx = 0,
prev_doc = {},
}
self._contexts[bufnr] = ctx
end
return ctx
end
function Suggestion:reset()
self._timer = nil
local bufnr = api.nvim_get_current_buf()
self._contexts[bufnr] = nil
end
function Suggestion:stop_timer()
if self._timer then
pcall(function()
fn.timer_stop(self._timer)
end)
self._timer = nil
end
end
function Suggestion:next()
local ctx = self:ctx()
if #ctx.suggestions == 0 then
return
end
ctx.current_suggestion_idx = (ctx.current_suggestion_idx % #ctx.suggestions) + 1
self:show()
end
function Suggestion:prev()
local ctx = self:ctx()
if #ctx.suggestions == 0 then
return
end
ctx.current_suggestion_idx = ((ctx.current_suggestion_idx - 2 + #ctx.suggestions) % #ctx.suggestions) + 1
self:show()
end
function Suggestion:dismiss()
self:stop_timer()
self:hide()
self:reset()
end
function Suggestion:accept()
-- Llm.cancel_inflight_request()
api.nvim_buf_del_extmark(0, SUGGESTION_NS, self.extmark_id)
local ctx = self:ctx()
local suggestion = ctx.suggestions and ctx.suggestions[ctx.current_suggestion_idx] or nil
if not suggestion then
if Config.mappings.suggestion and Config.mappings.suggestion.accept == "<Tab>" then
api.nvim_feedkeys(api.nvim_replace_termcodes("<Tab>", true, false, true), "n", true)
end
return
end
local bufnr = api.nvim_get_current_buf()
local current_lines = Utils.get_buf_lines(0, -1, bufnr)
local row = suggestion.row
local col = suggestion.col
local content = suggestion.content
local lines = vim.split(content, "\n")
local cursor_row, cursor_col = Utils.get_cursor_pos()
if row > cursor_row then
api.nvim_buf_set_lines(bufnr, row - 1, row - 1, false, { "" })
end
local line_count = #lines
if line_count > 0 then
if cursor_row == row then
local cursor_line_col = #current_lines[cursor_row] - 1
if cursor_col ~= cursor_line_col then
local current_line_ = current_lines[cursor_row]
lines[1] = lines[1] .. current_line_:sub(col + 1, -1)
end
end
local current_line = current_lines[row] or ""
local current_line_max_col = #current_line - 1
local start_col = col
if start_col > current_line_max_col then
lines[1] = string.rep(" ", start_col - current_line_max_col - 1) .. lines[1]
start_col = -1
end
api.nvim_buf_set_text(bufnr, row - 1, start_col, row - 1, -1, { lines[1] })
if #lines > 1 then
local insert_lines = vim.list_slice(lines, 2)
api.nvim_buf_set_lines(bufnr, row, row, true, insert_lines)
end
end
local down_count = line_count - 1
if row > cursor_row then
down_count = down_count + 1
end
local cursor_keys = string.rep("<Down>", down_count) .. "<End>"
api.nvim_feedkeys(api.nvim_replace_termcodes(cursor_keys, true, false, true), "n", false)
self:hide()
self:reset()
end
function Suggestion:setup_autocmds()
local last_cursor_pos = {}
local check_for_suggestion = Utils.debounce(function()
local current_cursor_pos = api.nvim_win_get_cursor(0)
if last_cursor_pos[1] == current_cursor_pos[1] and last_cursor_pos[2] == current_cursor_pos[2] then
self:suggest()
end
end, 77)
local function suggest_callback()
if not vim.bo.buflisted then
return
end
if vim.bo.buftype ~= "" then
return
end
local ctx = self:ctx()
if ctx.prev_doc and vim.deep_equal(ctx.prev_doc, Utils.get_doc()) then
return
end
self:hide()
last_cursor_pos = api.nvim_win_get_cursor(0)
self._timer = check_for_suggestion()
end
api.nvim_create_autocmd("InsertEnter", {
group = self.augroup,
callback = suggest_callback,
})
api.nvim_create_autocmd("BufEnter", {
group = self.augroup,
callback = function()
if fn.mode():match("^[iR]") then
suggest_callback()
end
end,
})
api.nvim_create_autocmd("CursorMovedI", {
group = self.augroup,
callback = suggest_callback,
})
api.nvim_create_autocmd("InsertLeave", {
group = self.augroup,
callback = function()
last_cursor_pos = {}
self:hide()
self:reset()
end,
})
end
function Suggestion:delete_autocmds()
if self.augroup then
api.nvim_del_augroup_by_id(self.augroup)
end
self.augroup = nil
end
return Suggestion

View File

@ -1,4 +1,6 @@
local api = vim.api
local fn = vim.fn
local lsp = vim.lsp
---@class avante.utils: LazyUtilCore
---@field tokens avante.utils.tokens
@ -338,7 +340,7 @@ function M.debug(msg, opts)
end
opts = opts or {}
if opts.title then
opts.title = "lazy.nvim: " .. opts.title
opts.title = "avante.nvim: " .. opts.title
end
if type(msg) == "string" then
M.notify(msg, opts)
@ -484,4 +486,78 @@ function M.remove_indentation(code)
return code:gsub("^%s*", "")
end
local function relative_path(absolute)
local relative = fn.fnamemodify(absolute, ":.")
if string.sub(relative, 0, 1) == "/" then
return fn.fnamemodify(absolute, ":t")
end
return relative
end
function M.get_doc()
local absolute = api.nvim_buf_get_name(0)
local params = lsp.util.make_position_params(0, "utf-8")
local position = {
row = params.position.line + 1,
col = params.position.character,
}
local doc = {
uri = params.textDocument.uri,
version = api.nvim_buf_get_var(0, "changedtick"),
relativePath = relative_path(absolute),
insertSpaces = vim.o.expandtab,
tabSize = fn.shiftwidth(),
indentSize = fn.shiftwidth(),
position = position,
}
return doc
end
function M.prepend_line_number(content, start_line)
start_line = start_line or 1
local lines = vim.split(content, "\n")
local result = {}
for i, line in ipairs(lines) do
i = i + start_line - 1
table.insert(result, "L" .. i .. ": " .. line)
end
return table.concat(result, "\n")
end
function M.trim_line_number(line)
return line:gsub("^L%d+: ", "")
end
function M.trim_all_line_numbers(content)
return vim
.iter(vim.split(content, "\n"))
:map(function(line)
local new_line = M.trim_line_number(line)
return new_line
end)
:join("\n")
end
function M.debounce(func, delay)
local timer_id = nil
return function(...)
local args = { ... }
if timer_id then
fn.timer_stop(timer_id)
end
timer_id = fn.timer_start(delay, function()
func(unpack(args))
timer_id = nil
end)
return timer_id
end
end
return M