local M = {}
---@param edit copilotlsp.InlineEdit
function M.apply_inline_edit(edit)
    local bufnr = vim.uri_to_bufnr(edit.textDocument.uri)

    ---@diagnostic disable-next-line: assign-type-mismatch
    vim.lsp.util.apply_text_edits({ edit }, bufnr, "utf-16")
end

---Debounces calls to a function, and ensures it only runs once per delay
---even if called repeatedly.
---@param fn fun(...: any)
---@param delay integer
function M.debounce(fn, delay)
    local timer = vim.uv.new_timer()
    return function(...)
        local argv = vim.F.pack_len(...)
        timer:start(delay, 0, function()
            timer:stop()
            vim.schedule_wrap(fn)(vim.F.unpack_len(argv))
        end)
    end
end

---@private
---@class Capture
---@field hl string|string[]
---range(0-based)
---@field start_row integer
---@field start_col integer
---@field end_row integer
---@field end_col integer

---@param text string
---@param lang string
---@return Capture[]?
local function parse_text(text, lang)
    local ok, trees = pcall(vim.treesitter.get_string_parser, text, lang)
    if not ok then
        return
    end
    trees:parse(true)

    local captures = {}

    trees:for_each_tree(function(tree, _ltree)
        local hl_query = vim.treesitter.query.get(lang, "highlights")
        if not hl_query then
            return
        end

        local iter = hl_query:iter_captures(tree:root(), text)
        vim.iter(iter):each(function(id, node)
            local name = hl_query.captures[id]
            local hl = "Normal"
            if not vim.startswith(name, "_") then
                hl = "@" .. name .. "." .. lang
            end
            local start_row, start_col, end_row, end_col = node:range()

            -- Ignore zero-width captures if they cause issues (sometimes happen at EOF)
            if start_row == end_row and start_col == end_col then
                return
            end

            table.insert(captures, {
                hl = hl,
                start_row = start_row,
                start_col = start_col,
                end_row = end_row,
                end_col = end_col,
            })
        end)
    end)
    return captures
end

local function merge_captures(captures)
    table.sort(captures, function(a, b)
        if a.start_row == b.start_row then
            return a.start_col < b.start_col
        end
        return a.start_row < b.start_row
    end)
    local merged_captures = {}
    for i = 2, #captures do
        local prev = captures[i - 1]
        local curr = captures[i]
        if
            prev.start_row == curr.start_row
            and prev.start_col == curr.start_col
            and prev.end_row == curr.end_row
            and prev.end_col == curr.end_col
        then
            local prev_hl = type(prev.hl) == "table" and prev.hl or { prev.hl }
            local curr_hl = type(curr.hl) == "table" and curr.hl or { curr.hl }
            ---@diagnostic disable-next-line: param-type-mismatch
            vim.list_extend(prev_hl, curr_hl)
            curr.hl = prev_hl
        else
            table.insert(merged_captures, prev)
        end
    end
    table.insert(merged_captures, captures[#captures])

    return merged_captures
end

function M.hl_text_to_virt_lines(text, lang)
    local lines = vim.split(text, "\n")
    local normal_hl = "Normal"
    local bg_hl = "CopilotLspNesAdd"

    local captures = parse_text(text, lang)
    if not captures or #captures == 0 then
        return vim.iter(lines)
            :map(function(line)
                return { hl_chunk(line) }
            end)
            :totable()
    end

    captures = merge_captures(captures)

    local virt_lines = {}

    local curr_row = 0
    local curr_col = 0
    local curr_virt_line = {}

    vim.iter(captures):each(function(cap)
        -- skip if the capture is before the current position
        if cap.end_row < curr_row or (cap.end_row == curr_row and cap.end_col <= curr_col) then
            return
        end

        if cap.start_row > curr_row then
            -- add the rest of the line
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1)
            table.insert(curr_virt_line, hl_chunk(chunk_text))
            table.insert(virt_lines, curr_virt_line)

            for i = curr_row + 1, cap.start_row - 1 do
                local line_text = lines[i + 1]
                table.insert(virt_lines, { hl_chunk(line_text) })
            end

            curr_row = cap.start_row
            curr_col = 0
            curr_virt_line = {}
        end

        assert(curr_row == cap.start_row, "Unexpected start row")

        if cap.start_col > curr_col then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1, cap.start_col)
            table.insert(curr_virt_line, hl_chunk(chunk_text))
            curr_col = cap.start_col
        end

        assert(curr_col == cap.start_col, "Unexpected start column")

        if cap.end_row > curr_row then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1)
            table.insert(curr_virt_line, hl_chunk(chunk_text, cap.hl))
            table.insert(virt_lines, curr_virt_line)

            for i = curr_row + 1, cap.end_row - 1 do
                local line_text = lines[i + 1]
                table.insert(virt_lines, { hl_chunk(line_text, cap.hl) })
            end

            curr_row = cap.end_row
            curr_col = 0
            curr_virt_line = {}
        end

        assert(curr_row == cap.end_row, "Unexpected end row")

        if cap.end_col > curr_col then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1, cap.end_col)
            table.insert(curr_virt_line, hl_chunk(chunk_text, cap.hl))
            curr_col = cap.end_col
        end
    end)

    if #curr_virt_line > 0 then
        table.insert(virt_lines, curr_virt_line)
    end

    return virt_lines
end

return M

local function hl_chunk(chunk, hl)
        if not hl then
            return { chunk, { normal_hl, bg_hl } }
        end
        if type(hl) == "string" then
            return { chunk, { hl, bg_hl } }
        end
        hl = vim.deepcopy(hl)
        table.insert(hl, bg_hl)
        return { chunk, hl }
    end

local M = {}
local config = require("copilot-lsp.config").config

---@param bufnr integer
---@param ns_id integer
local function _dismiss_suggestion(bufnr, ns_id)
    pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_id, 0, -1)
end

---@param bufnr? integer
---@param ns_id integer
function M.clear_suggestion(bufnr, ns_id)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    -- Validate buffer exists before accessing buffer-scoped variables
    if not vim.api.nvim_buf_is_valid(bufnr) then
        return
    end
    if vim.b[bufnr].nes_jump then
        vim.b[bufnr].nes_jump = false
        return
    end
    _dismiss_suggestion(bufnr, ns_id)
    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return
    end

    -- Clear buffer variables
    vim.b[bufnr].nes_state = nil
    vim.b[bufnr].copilotlsp_nes_cursor_moves = nil
    vim.b[bufnr].copilotlsp_nes_last_line = nil
    vim.b[bufnr].copilotlsp_nes_last_col = nil
end

---@private
---@param bufnr integer
---@param edit lsp.TextEdit
---@return copilotlsp.nes.InlineEditPreview
function M._calculate_preview(bufnr, edit)
    local text = edit.newText
    local range = edit.range
    local start_line = range.start.line
    local start_char = range.start.character
    local end_line = range["end"].line
    local end_char = range["end"].character

    -- Split text by newline. Use plain=true to handle trailing newline correctly.
    local new_lines = vim.split(text, "\n", { plain = true })
    local num_new_lines = #new_lines

    local old_lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false)
    local num_old_lines = #old_lines

    local is_same_line = start_line == end_line
    local is_deletion = text == ""
    local lines_edit = is_same_line or (start_char == 0 and end_char == 0)
    local is_insertion = is_same_line and start_char == end_char

    if is_deletion and is_insertion then
        -- no-op
        return {}
    end

    if is_deletion and lines_edit then
        return {
            deletion = {
                range = edit.range,
            },
        }
    end

    if is_insertion and num_new_lines == 1 and text ~= "" then
        -- inline insertion
        return {
            inline_insertion = {
                text = text,
                line = start_line,
                character = start_char,
            },
        }
    end

    if is_insertion and num_new_lines > 1 then
        if start_char == #old_lines[1] and new_lines[1] == "" then
            -- insert lines after the start line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 2), "\n"),
                    line = start_line,
                },
            }
        end

        if end_char == 0 and new_lines[num_new_lines] == "" then
            -- insert lines before the end line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 1, num_new_lines - 1), "\n"),
                    line = start_line,
                    above = true,
                },
            }
        end
    end

    -- insert lines in the middle
    local prefix = old_lines[1]:sub(1, start_char)
    local suffix = old_lines[num_old_lines]:sub(end_char + 1)
    local new_lines_extend = vim.deepcopy(new_lines)
    new_lines_extend[1] = prefix .. new_lines_extend[1]
    new_lines_extend[num_new_lines] = new_lines_extend[num_new_lines] .. suffix
    local insertion = table.concat(new_lines_extend, "\n")

    return {
        deletion = {
            range = {
                start = { line = start_line, character = 0 },
                ["end"] = { line = end_line, character = #old_lines[num_old_lines] },
            },
        },
        lines_insertion = {
            text = insertion,
            line = end_line,
        },
    }
end

---@private
---@param bufnr integer
---@param ns_id integer
---@param preview copilotlsp.nes.InlineEditPreview
function M._display_preview(bufnr, ns_id, preview)
    if preview.deletion then
        local range = preview.deletion.range
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
            hl_group = "CopilotLspNesDelete",
            end_row = range["end"].line,
            end_col = range["end"].character,
        })
    end

    local inline_insertion = preview.inline_insertion
    if inline_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(inline_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, inline_insertion.line, inline_insertion.character, {
            virt_text = virt_lines[1],
            virt_text_pos = "inline",
        })
    end

    local lines_insertion = preview.lines_insertion
    if lines_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(lines_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines_insertion.line, 0, {
            virt_lines = virt_lines,
            virt_lines_above = lines_insertion.above,
        })
    end
end

---@private
---@param bufnr integer
---@param ns_id integer
---@param edits copilotlsp.InlineEdit[]
function M._display_next_suggestion(bufnr, ns_id, edits)
    M.clear_suggestion(bufnr, ns_id)
    if not edits or #edits == 0 then
        return
    end

    local suggestion = edits[1]
    local preview = M._calculate_preview(bufnr, suggestion)
    M._display_preview(bufnr, ns_id, preview)

    vim.b[bufnr].nes_state = suggestion
    vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id
    vim.b[bufnr].copilotlsp_nes_cursor_moves = 1

    vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
        buffer = bufnr,
        callback = function()
            if not vim.b[bufnr].nes_state then
                return true
            end

            -- Get cursor position
            local cursor = vim.api.nvim_win_get_cursor(0)
            local cursor_line = cursor[1] - 1 -- 0-indexed
            local cursor_col = cursor[2]
            local suggestion_line = suggestion.range.start.line

            -- Store previous position
            local last_line = vim.b[bufnr].copilotlsp_nes_last_line or cursor_line
            local last_col = vim.b[bufnr].copilotlsp_nes_last_col or cursor_col

            -- Update stored position
            vim.b[bufnr].copilotlsp_nes_last_line = cursor_line
            vim.b[bufnr].copilotlsp_nes_last_col = cursor_col

            -- Calculate distance to suggestion
            local line_distance = math.abs(cursor_line - suggestion_line)
            local last_line_distance = math.abs(last_line - suggestion_line)

            -- Check if cursor changed position on same line
            local moved_horizontally = (cursor_line == last_line) and (cursor_col ~= last_col)

            -- Get current mode
            local mode = vim.api.nvim_get_mode().mode

            -- Determine if we should count this movement
            local should_count = false
            local first_char = mode:sub(1, 1)

            -- In insert mode, only count cursor movements, not text changes
            if first_char == "i" then
                if moved_horizontally or line_distance ~= last_line_distance then
                    should_count = true
                end
            elseif first_char == "v" or first_char == "V" or mode == "\22" then
                should_count = true
            -- In normal mode with horizontal movement
            elseif moved_horizontally and config.nes.count_horizontal_moves then
                should_count = true
            -- In normal mode with line changes
            elseif line_distance > last_line_distance then
                should_count = true
            -- Moving toward suggestion in normal mode
            elseif line_distance < last_line_distance and config.nes.reset_on_approaching then
                if line_distance > 1 then -- Don't reset if 0 or 1 line away
                    vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                end
            end

            -- Update counter if needed
            if should_count then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = (vim.b[bufnr].copilotlsp_nes_cursor_moves or 0) + 1
            end

            -- Clear if counter threshold reached
            if vim.b[bufnr].copilotlsp_nes_cursor_moves >= config.nes.move_count_threshold then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                vim.schedule(function()
                    M.clear_suggestion(bufnr, ns_id)
                end)
                return true
            end

            -- Optional: Clear on large distance
            if config.nes.clear_on_large_distance and line_distance > config.nes.distance_threshold then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end

            return false -- Keep the autocmd
        end,
    })
    -- Also clear on text changes that affect the suggestion area
    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
        buffer = bufnr,
        callback = function()
            if not vim.b[bufnr].nes_state then
                return true
            end
            -- Check if the text at the suggestion position has changed
            local start_line = suggestion.range.start.line
            -- If the lines are no longer in the buffer, clear the suggestion
            if start_line >= vim.api.nvim_buf_line_count(bufnr) then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end
            return false -- Keep the autocmd
        end,
    })
end

return M

local errs = require("copilot-lsp.errors")
local nes_ui = require("copilot-lsp.nes.ui")
local utils = require("copilot-lsp.util")

local M = {}

local nes_ns = vim.api.nvim_create_namespace("copilotlsp.nes")

---@param err lsp.ResponseError?
---@param result copilotlsp.copilotInlineEditResponse
---@param ctx lsp.HandlerContext
local function handle_nes_response(err, result, ctx)
    if err then
        -- vim.notify(err.message)
        return
    end
    -- Validate buffer still exists before processing response
    if not vim.api.nvim_buf_is_valid(ctx.bufnr) then
        return
    end
    for _, edit in ipairs(result.edits) do
        --- Convert to textEdit fields
        edit.newText = edit.text
    end
    nes_ui._display_next_suggestion(ctx.bufnr, nes_ns, result.edits)
end

--- Requests the NextEditSuggestion from the current cursor position
---@param copilot_lss? vim.lsp.Client|string
function M.request_nes(copilot_lss)
    local pos_params = vim.lsp.util.make_position_params(0, "utf-16")
    local version = vim.lsp.util.buf_versions[vim.api.nvim_get_current_buf()]
    if type(copilot_lss) == "string" then
        copilot_lss = vim.lsp.get_clients({ name = copilot_lss })[1]
    end
    assert(copilot_lss, errs.ErrNotStarted)
    ---@diagnostic disable-next-line: inject-field
    pos_params.textDocument.version = version
    copilot_lss:request("textDocument/copilotInlineEdit", pos_params, handle_nes_response)
end

--- Walks the cursor to the start of the edit.
--- This function returns false if there is no edit to apply or if the cursor is already at the start position of the
--- edit.
---@param bufnr? integer
---@return boolean --if the cursor walked
function M.walk_cursor_start_edit(bufnr)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return false
    end

    local cursor_row, _ = unpack(vim.api.nvim_win_get_cursor(0))
    if cursor_row - 1 ~= state.range.start.line then
        vim.b[bufnr].nes_jump = true
        ---@type lsp.Location
        local jump_loc_before = {
            uri = state.textDocument.uri,
            range = {
                start = state.range["start"],
                ["end"] = state.range["start"],
            },
        }
        vim.schedule(function()
            vim.lsp.util.show_document(jump_loc_before, "utf-16", { focus = true })
        end)
        return true
    else
        return false
    end
end

--- Walks the cursor to the end of the edit.
--- This function returns false if there is no edit to apply or if the cursor is already at the end position of the
--- edit
---@param bufnr? integer
---@return boolean --if the cursor walked
function M.walk_cursor_end_edit(bufnr)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return false
    end

    ---@type lsp.Location
    local jump_loc_after = {
        uri = state.textDocument.uri,
        range = {
            start = state.range["end"],
            ["end"] = state.range["end"],
        },
    }
    --NOTE: If last line is deletion, then this may be outside of the buffer
    vim.schedule(function()
        pcall(vim.lsp.util.show_document, jump_loc_after, "utf-16", { focus = true })
    end)
    return true
end

--- This function applies the pending nes edit to the current buffer and then clears the marks for the pending
--- suggestion
---@param bufnr? integer
---@return boolean --if the nes was applied
function M.apply_pending_nes(bufnr)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()

    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return false
    end
    vim.schedule(function()
        utils.apply_inline_edit(state)
        vim.b[bufnr].nes_jump = false
        nes_ui.clear_suggestion(bufnr, nes_ns)
    end)
    return true
end

---@param bufnr? integer
function M.clear_suggestion(bufnr)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    nes_ui.clear_suggestion(bufnr, nes_ns)
end

--- Clear the current suggestion if it exists
---@return boolean -- true if a suggestion was cleared, false if no suggestion existed
function M.clear()
    local buf = vim.api.nvim_get_current_buf()
    if vim.b[buf].nes_state then
        local ns = vim.b[buf].copilotlsp_nes_namespace_id or nes_ns
        nes_ui.clear_suggestion(buf, ns)
        return true
    end
    return false
end

return M

---@type table<string, lsp.Handler>
local M = {}

local methods = {
    signIn = "signIn",
    didChangeStatus = "didChangeStatus",
}

-- copy from copilot.lua
local function open_signin_popup(code, url)
    local lines = {
        " [Copilot-lsp] ",
        "",
        " First copy your one-time code: ",
        "   " .. code .. " ",
        " In your browser, visit: ",
        "   " .. url .. " ",
        "",
        " ...waiting, it might take a while and ",
        " this popup will auto close once done... ",
    }
    local height, width = #lines, math.max(unpack(vim.tbl_map(function(line)
        return #line
    end, lines)))

    local bufnr = vim.api.nvim_create_buf(false, true)
    vim.bo[bufnr].buflisted = false
    vim.bo[bufnr].buftype = "nofile"
    vim.bo[bufnr].bufhidden = "wipe"
    vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)

    local winnr = vim.api.nvim_open_win(bufnr, true, {
        relative = "editor",
        style = "minimal",
        border = "single",
        row = (vim.o.lines - height) / 2,
        col = (vim.o.columns - width) / 2,
        height = height,
        width = width,
    })
    vim.wo[winnr].winhighlight = "Normal:Normal"
    vim.wo[winnr].winblend = 0

    return function()
        vim.api.nvim_win_close(winnr, true)
    end
end

local function copy_to_clipboard(s)
    vim.fn.setreg("+", s)
    vim.fn.setreg("*", s)
end

---@param res {command: lsp.Command, userCode: string, verificationUri: string}
M[methods.signIn] = function(err, res, ctx)
    if err then
        vim.notify("[copilot-lsp] failed to start signin flow: " .. vim.inspect(err), vim.log.levels.ERROR)
        return
    end

    local client = vim.lsp.get_client_by_id(ctx.client_id)
    if not client then
        return
    end

    vim.g.copilot_lsp_signin_pending = true

    local close_signin_popup = open_signin_popup(res.userCode, res.verificationUri)
    copy_to_clipboard(res.userCode)

    client:exec_cmd(
        res.command,
        { bufnr = ctx.bufnr },

        ---@param cmd_res {status: string, user: string}
        function(cmd_err, cmd_res)
            vim.g.copilot_lsp_signin_pending = nil
            close_signin_popup()

            if cmd_err then
                vim.notify("[copilot-lsp] failed to open browser: " .. vim.inspect(cmd_err), vim.log.levels.WARN)
                return
            end
            if cmd_res.status == "OK" then
                vim.notify("[copilot-lsp] successfully signed in as: " .. cmd_res.user, vim.log.levels.INFO)
            else
                vim.notify("[copilot-lsp] failed to sign in: " .. vim.inspect(cmd_res), vim.log.levels.ERROR)
            end
        end
    )
end

---@param client_id integer
---@param bufnr integer
local function sign_in(client_id, bufnr)
    if vim.g.copilot_lsp_signin_pending then
        return
    end

    local client = vim.lsp.get_client_by_id(client_id)
    if not client then
        return
    end

    for _, req in pairs(client.requests) do
        if req.method == methods.signIn and req.type == "pending" then
            return
        end
    end

    client:request(methods.signIn, vim.empty_dict(), nil, bufnr)
end

---@param res {busy: boolean, kind: 'Normal'|'Error'|'Warning'|'Inactive', message: string}
M["didChangeStatus"] = function(err, res, ctx)
    if err then
        return
    end
    -- real error message: You are not signed into GitHub. Please sign in to use Copilot.
    if res.kind == "Error" and res.message:find("not signed into") then
        sign_in(ctx.client_id, ctx.bufnr)
    end
end

return M

local M = {}

---@param _results table<integer, { err: lsp.ResponseError, result: lsp.InlineCompletionList}>
---@param _ctx lsp.HandlerContext
---@param _config table
local function handle_inlineCompletion_response(_results, _ctx, _config)
    -- -- Filter errors from results
    -- local results1 = {} --- @type table<integer,lsp.InlineCompletionList>
    --
    -- for client_id, resp in pairs(results) do
    --     local err, result = resp.err, resp.result
    --     if err then
    --         vim.lsp.log.error(err.code, err.message)
    --     elseif result then
    --         results1[client_id] = result
    --     end
    -- end
    --
    -- for _, result in pairs(results1) do
    --     --TODO: Ghost text for completions
    --     -- This is where we show the completion results
    --     -- However, the LSP being named "copilot_ls" is enough for blink-cmp to show the completion
    -- end
end

---@param type lsp.InlineCompletionTriggerKind
function M.request_inline_completion(type)
    local params = vim.tbl_deep_extend("keep", vim.lsp.util.make_position_params(0, "utf-16"), {
        textDocument = vim.lsp.util.make_text_document_params(),
        position = vim.lsp.util.make_position_params(0, "utf-16"),
        context = {
            triggerKind = type,
        },
        formattingOptions = {
            --TODO: Grab this from editor also
            tabSize = 4,
            insertSpaces = true,
        },
    })
    vim.lsp.buf_request_all(0, "textDocument/inlineCompletion", params, handle_inlineCompletion_response)
end
return M

local version = vim.version()

---@type vim.lsp.Config
return {
    --NOTE: This name means that existing blink completion works
    name = "copilot_ls",
    cmd = {
        "copilot-language-server",
        "--stdio",
    },
    init_options = {
        editorInfo = {
            name = "neovim",
            version = string.format("%d.%d.%d", version.major, version.minor, version.patch),
        },
        editorPluginInfo = {
            name = "Github Copilot LSP for Neovim",
            version = "0.0.1",
        },
    },
    settings = {
        nextEditSuggestions = {
            enabled = true,
        },
    },
    handlers = setmetatable({}, {
        __index = function(_, method)
            return require("copilot-lsp.handlers")[method]
        end,
    }),
    root_dir = vim.uv.cwd(),
    on_init = function(client)
        local au = vim.api.nvim_create_augroup("copilotlsp.init", { clear = true })
        --NOTE: Inline Completions
        --TODO: We dont currently use this code path, so comment for now until a UI is built
        -- vim.api.nvim_create_autocmd("TextChangedI", {
        --     callback = function()
        --         inline_completion.request_inline_completion(2)
        --     end,
        --     group = au,
        -- })

        -- TODO: make this configurable for key maps, or just expose commands to map in config
        -- vim.keymap.set("i", "<c-i>", function()
        --     inline_completion.request_inline_completion(1)
        -- end)

        --NOTE: NES Completions
        local debounced_request = require("copilot-lsp.util").debounce(
            require("copilot-lsp.nes").request_nes,
            vim.g.copilot_nes_debounce or 500
        )
        vim.api.nvim_create_autocmd({ "TextChangedI", "TextChanged" }, {
            callback = function()
                debounced_request(client)
            end,
            group = au,
        })

        --NOTE: didFocus
        vim.api.nvim_create_autocmd("BufEnter", {
            callback = function()
                local td_params = vim.lsp.util.make_text_document_params()
                client:notify("textDocument/didFocus", {
                    textDocument = {
                        uri = td_params.uri,
                    },
                })
            end,
            group = au,
        })
    end,
}

---@class copilotlsp.config.nes
---@field move_count_threshold integer Number of cursor movements before clearing suggestion
---@field distance_threshold integer Maximum line distance before clearing suggestion
---@field clear_on_large_distance boolean Whether to clear suggestion when cursor is far away
---@field count_horizontal_moves boolean Whether to count horizontal cursor movements
---@field reset_on_approaching boolean Whether to reset counter when approaching suggestion

local M = {}

---@class copilotlsp.config
---@field nes copilotlsp.config.nes
M.defaults = {
    nes = {
        move_count_threshold = 3,
        distance_threshold = 40,
        clear_on_large_distance = true,
        count_horizontal_moves = true,
        reset_on_approaching = true,
    },
}

---@type copilotlsp.config
M.config = vim.deepcopy(M.defaults)

---@param opts? copilotlsp.config configuration to merge with defaults
function M.setup(opts)
    opts = opts or {}
    M.config = vim.tbl_deep_extend("force", M.defaults, opts)
end

return M

local config = require("copilot-lsp.config")

---@class copilotlsp
---@field defaults copilotlsp.config
---@field config copilotlsp.config
---@field setup fun(opts?: copilotlsp.config): nil
local M = {}

M.defaults = config.defaults
M.config = config.config

---@param opts? copilotlsp.config configuration to merge with defaults
function M.setup(opts)
    config.setup(opts)
    M.config = config.config
end

return M

vim.api.nvim_set_hl(0, "CopilotLspNesAdd", { link = "DiffAdd", default = true })
vim.api.nvim_set_hl(0, "CopilotLspNesDelete", { link = "DiffDelete", default = true })
vim.api.nvim_set_hl(0, "CopilotLspNesApply", { link = "DiffText", default = true })

---@class copilotlsp.InlineEdit : lsp.TextEdit
---@field command lsp.Command
---@field text string
---@field textDocument lsp.VersionedTextDocumentIdentifier

---@class copilotlsp.copilotInlineEditResponse
---@field edits copilotlsp.InlineEdit[]

---@class copilotlsp.nes.TextDeletion
---@field range lsp.Range

---@class copilotlsp.nes.InlineInsertion
---@field text string
---@field line integer
---@field character integer

---@class copilotlsp.nes.TextInsertion
---@field text string
---@field line integer insert lines at this line
---@field above? boolean above the line

---@class copilotlsp.nes.InlineEditPreview
---@field deletion? copilotlsp.nes.TextDeletion
---@field inline_insertion? copilotlsp.nes.InlineInsertion
---@field lines_insertion? copilotlsp.nes.TextInsertion

return {
    ErrNotStarted = "Copilot LSP not started",
}

This code segment functions to: Combine textual content with syntax highlighting metadata and background styling for display in virtual interface elements.

```lua
[TASK_BEGIN]
    local function hl_chunk(text, hl)
        return { text, { hl or normal_hl, bg_hl } }
    end
[TASK_END]
```


local M = {}

---@param _results table<integer, { err: lsp.ResponseError, result: lsp.InlineCompletionList}>
---@param _ctx lsp.HandlerContext
---@param _config table
local function handle_inlineCompletion_response(_results, _ctx, _config)
    -- -- Filter errors from results
    -- local results1 = {} --- @type table<integer,lsp.InlineCompletionList>
    --
    -- for client_id, resp in pairs(results) do
    --     local err, result = resp.err, resp.result
    --     if err then
    --         vim.lsp.log.error(err.code, err.message)
    --     elseif result then
    --         results1[client_id] = result
    --     end
    -- end
    --
    -- for _, result in pairs(results1) do
    --     --TODO: Ghost text for completions
    --     -- This is where we show the completion results
    --     -- However, the LSP being named "copilot_ls" is enough for blink-cmp to show the completion
    -- end
end

---@param type lsp.InlineCompletionTriggerKind

function M.request_inline_completion(type)
    local params = vim.tbl_deep_extend("keep", vim.lsp.util.make_position_params(0, "utf-16"), {
        textDocument = vim.lsp.util.make_text_document_params(),
        position = vim.lsp.util.make_position_params(0, "utf-16"),
        context = {
            triggerKind = type,
        },
        formattingOptions = {
            tabSize = 4,
            insertSpaces = true,
        },
    })
    vim.lsp.buf_request_all(0, "textDocument/inlineCompletion", params, handle_inlineCompletion_response)
end

local M = {}
---@param edit copilotlsp.InlineEdit
function M.apply_inline_edit(edit)
    local bufnr = vim.uri_to_bufnr(edit.textDocument.uri)

    ---@diagnostic disable-next-line: assign-type-mismatch
    vim.lsp.util.apply_text_edits({ edit }, bufnr, "utf-16")
end

---Debounces calls to a function, and ensures it only runs once per delay
---even if called repeatedly.
---@param fn fun(...: any)
---@param delay integer
function M.debounce(fn, delay)
    local timer = vim.uv.new_timer()
    return function(...)
        local argv = vim.F.pack_len(...)
        timer:start(delay, 0, function()
            timer:stop()
            vim.schedule_wrap(fn)(vim.F.unpack_len(argv))
        end)
    end
end

---@private
---@class Capture
---@field hl string|string[]
---range(0-based)
---@field start_row integer
---@field start_col integer
---@field end_row integer
---@field end_col integer

---@param text string
---@param lang string
---@return Capture[]?
local function parse_text(text, lang)
    local ok, trees = pcall(vim.treesitter.get_string_parser, text, lang)
    if not ok then
        return
    end
    trees:parse(true)

    local captures = {}

    trees:for_each_tree(function(tree, _ltree)
        local hl_query = vim.treesitter.query.get(lang, "highlights")
        if not hl_query then
            return
        end

        local iter = hl_query:iter_captures(tree:root(), text)
        vim.iter(iter):each(function(id, node)
            local name = hl_query.captures[id]
            local hl = "Normal"
            if not vim.startswith(name, "_") then
                hl = "@" .. name .. "." .. lang
            end
            local start_row, start_col, end_row, end_col = node:range()

            -- Ignore zero-width captures if they cause issues (sometimes happen at EOF)
            if start_row == end_row and start_col == end_col then
                return
            end

            table.insert(captures, {
                hl = hl,
                start_row = start_row,
                start_col = start_col,
                end_row = end_row,
                end_col = end_col,
            })
        end)
    end)
    return captures
end

local function merge_captures(captures)
    table.sort(captures, function(a, b)
        if a.start_row == b.start_row then
            return a.start_col < b.start_col
        end
        return a.start_row < b.start_row
    end)
    local merged_captures = {}
    for i = 2, #captures do
        local prev = captures[i - 1]
        local curr = captures[i]
        if
            prev.start_row == curr.start_row
            and prev.start_col == curr.start_col
            and prev.end_row == curr.end_row
            and prev.end_col == curr.end_col
        then
            local prev_hl = type(prev.hl) == "table" and prev.hl or { prev.hl }
            local curr_hl = type(curr.hl) == "table" and curr.hl or { curr.hl }
            ---@diagnostic disable-next-line: param-type-mismatch
            vim.list_extend(prev_hl, curr_hl)
            curr.hl = prev_hl
        else
            table.insert(merged_captures, prev)
        end
    end
    table.insert(merged_captures, captures[#captures])

    return merged_captures
end

function M.hl_text_to_virt_lines(text, lang)
    local lines = vim.split(text, "\n")
    local normal_hl = "Normal"
    local bg_hl = "CopilotLspNesAdd"

    local function hl_chunk(chunk, hl)
        if not hl then
            return { chunk, { normal_hl, bg_hl } }
        end
        if type(hl) == "string" then
            return { chunk, { hl, bg_hl } }
        end
        hl = vim.deepcopy(hl)
        table.insert(hl, bg_hl)
        return { chunk, hl }
    end

    local captures = parse_text(text, lang)
    if not captures or #captures == 0 then
        return vim.iter(lines)
            :map(function(line)
                return { hl_chunk(line) }
            end)
            :totable()
    end

    captures = merge_captures(captures)

    local virt_lines = {}

    local curr_row = 0
    local curr_col = 0
    local curr_virt_line = {}

    vim.iter(captures):each(function(cap)
        -- skip if the capture is before the current position
        if cap.end_row < curr_row or (cap.end_row == curr_row and cap.end_col <= curr_col) then
            return
        end

        if cap.start_row > curr_row then
            -- add the rest of the line
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1)
            table.insert(curr_virt_line, hl_chunk(chunk_text))
            table.insert(virt_lines, curr_virt_line)

            for i = curr_row + 1, cap.start_row - 1 do
                local line_text = lines[i + 1]
                table.insert(virt_lines, { hl_chunk(line_text) })
            end

            curr_row = cap.start_row
            curr_col = 0
            curr_virt_line = {}
        end

        assert(curr_row == cap.start_row, "Unexpected start row")

        if cap.start_col > curr_col then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1, cap.start_col)
            table.insert(curr_virt_line, hl_chunk(chunk_text))
            curr_col = cap.start_col
        end

        assert(curr_col == cap.start_col, "Unexpected start column")

        if cap.end_row > curr_row then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1)
            table.insert(curr_virt_line, hl_chunk(chunk_text, cap.hl))
            table.insert(virt_lines, curr_virt_line)

            for i = curr_row + 1, cap.end_row - 1 do
                local line_text = lines[i + 1]
                table.insert(virt_lines, { hl_chunk(line_text, cap.hl) })
            end

            curr_row = cap.end_row
            curr_col = 0
            curr_virt_line = {}
        end

        assert(curr_row == cap.end_row, "Unexpected end row")

        if cap.end_col > curr_col then
            local chunk_text = string.sub(lines[curr_row + 1], curr_col + 1, cap.end_col)
            table.insert(curr_virt_line, hl_chunk(chunk_text, cap.hl))
            curr_col = cap.end_col
        end
    end)

    if #curr_virt_line > 0 then
        table.insert(virt_lines, curr_virt_line)
    end

    return virt_lines
end

return M

This code segment functions to: Construct a structured request for inline code suggestions based on the current document state and transmit it to a language server, incorporating contextual trigger details and formatting preferences.

```lua
[TASK_BEGIN]
---@param results table<integer, { err: lsp.ResponseError, result: lsp.InlineCompletionList}>
---@param ctx lsp.HandlerContext
---@param config table
function M.inline_completion(results, ctx, config)
    local filtered_results = {} --- @type table<integer,lsp.InlineCompletionList>

    for client_id, resp in pairs(results) do
        local err, result = resp.err, resp.result
        if err then
            vim.lsp.log.error("Client "..client_id.." error: "..err.message)
        elseif result then
            filtered_results[client_id] = result
        end
    end

    for client_id, result in pairs(filtered_results) do
        local items = result.items
        if items and #items > 0 then
            local completion = items[1]
            vim.api.nvim_buf_set_extmark(
                ctx.bufnr,
                config.ns_id,
                ctx.pos[1],
                ctx.pos[2],
                {
                    virt_text = {{completion.insertText, "Comment"}},
                    virt_text_pos = "overlay"
                }
            )
        end
    end
end
[TASK_END]
```

function M.inline_completion(results, ctx, config)
    local filtered_results = {}
    for client_id, resp in pairs(results) do
        local err, result = resp.err, resp.result
        if err then
            vim.lsp.log.error("Client "..client_id.." error: "..err.message)
        elseif result then
            filtered_results[client_id] = result
        end
    end
    for client_id, result in pairs(filtered_results) do
        local items = result.items
        if items and #items > 0 then
            local completion = items[1]
            vim.api.nvim_buf_set_extmark(
                ctx.bufnr,
                config.ns_id,
                ctx.pos[1],
                ctx.pos[2],
                {
                    virt_text = {{completion.insertText, "Comment"}},
                    virt_text_pos = "overlay"
                }
            )
        end
    end
end


local M = {}
local config = require("copilot-lsp.config").config

---@param bufnr integer
---@param ns_id integer
local function _dismiss_suggestion(bufnr, ns_id)
    pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_id, 0, -1)
end

---@param bufnr? integer
---@param ns_id integer
function M.clear_suggestion(bufnr, ns_id)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    -- Validate buffer exists before accessing buffer-scoped variables
    if not vim.api.nvim_buf_is_valid(bufnr) then
        return
    end
    if vim.b[bufnr].nes_jump then
        vim.b[bufnr].nes_jump = false
        return
    end
    _dismiss_suggestion(bufnr, ns_id)
    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return
    end

    -- Clear buffer variables
    vim.b[bufnr].nes_state = nil
    vim.b[bufnr].copilotlsp_nes_cursor_moves = nil
    vim.b[bufnr].copilotlsp_nes_last_line = nil
    vim.b[bufnr].copilotlsp_nes_last_col = nil
end

---@private
---@param bufnr integer
---@param edit lsp.TextEdit
---@return copilotlsp.nes.InlineEditPreview
function M._calculate_preview(bufnr, edit)
    local text = edit.newText
    local range = edit.range
    local start_line = range.start.line
    local start_char = range.start.character
    local end_line = range["end"].line
    local end_char = range["end"].character

    -- Split text by newline. Use plain=true to handle trailing newline correctly.
    local new_lines = vim.split(text, "\n", { plain = true })
    local num_new_lines = #new_lines

    local old_lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false)
    local num_old_lines = #old_lines

    local is_same_line = start_line == end_line
    local is_deletion = text == ""
    local lines_edit = is_same_line or (start_char == 0 and end_char == 0)
    local is_insertion = is_same_line and start_char == end_char

    if is_deletion and is_insertion then
        -- no-op
        return {}
    end

    if is_deletion and lines_edit then
        return {
            deletion = {
                range = edit.range,
            },
        }
    end

    if is_insertion and num_new_lines == 1 and text ~= "" then
        -- inline insertion
        return {
            inline_insertion = {
                text = text,
                line = start_line,
                character = start_char,
            },
        }
    end

    if is_insertion and num_new_lines > 1 then
        if start_char == #old_lines[1] and new_lines[1] == "" then
            -- insert lines after the start line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 2), "\n"),
                    line = start_line,
                },
            }
        end

        if end_char == 0 and new_lines[num_new_lines] == "" then
            -- insert lines before the end line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 1, num_new_lines - 1), "\n"),
                    line = start_line,
                    above = true,
                },
            }
        end
    end

    -- insert lines in the middle
    local prefix = old_lines[1]:sub(1, start_char)
    local suffix = old_lines[num_old_lines]:sub(end_char + 1)
    local new_lines_extend = vim.deepcopy(new_lines)
    new_lines_extend[1] = prefix .. new_lines_extend[1]
    new_lines_extend[num_new_lines] = new_lines_extend[num_new_lines] .. suffix
    local insertion = table.concat(new_lines_extend, "\n")

    return {
        deletion = {
            range = {
                start = { line = start_line, character = 0 },
                ["end"] = { line = end_line, character = #old_lines[num_old_lines] },
            },
        },
        lines_insertion = {
            text = insertion,
            line = end_line,
        },
    }
end

---@private
---@param bufnr integer
---@param ns_id integer
---@param preview copilotlsp.nes.InlineEditPreview
function M._display_preview(bufnr, ns_id, preview)
    if preview.deletion then
        local range = preview.deletion.range
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
            hl_group = "CopilotLspNesDelete",
            end_row = range["end"].line,
            end_col = range["end"].character,
        })
    end

    local inline_insertion = preview.inline_insertion
    if inline_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(inline_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, inline_insertion.line, inline_insertion.character, {
            virt_text = virt_lines[1],
            virt_text_pos = "inline",
        })
    end

    local lines_insertion = preview.lines_insertion
    if lines_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(lines_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines_insertion.line, 0, {
            virt_lines = virt_lines,
            virt_lines_above = lines_insertion.above,
        })
    end
end

---@private
---@param bufnr integer
---@param ns_id integer
---@param edits copilotlsp.InlineEdit[]
function M._display_next_suggestion(bufnr, ns_id, edits)
    M.clear_suggestion(bufnr, ns_id)
    if not edits or #edits == 0 then
        return
    end

    local suggestion = edits[1]
    local preview = M._calculate_preview(bufnr, suggestion)
    M._display_preview(bufnr, ns_id, preview)

    vim.b[bufnr].nes_state = suggestion
    vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id
    vim.b[bufnr].copilotlsp_nes_cursor_moves = 1

    vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
        buffer = bufnr,
        callback =

,
    })
    -- Also clear on text changes that affect the suggestion area
    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
        buffer = bufnr,
        callback = function()
            if not vim.b[bufnr].nes_state then
                return true
            end
            -- Check if the text at the suggestion position has changed
            local start_line = suggestion.range.start.line
            -- If the lines are no longer in the buffer, clear the suggestion
            if start_line >= vim.api.nvim_buf_line_count(bufnr) then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end
            return false -- Keep the autocmd
        end,
    })
end

return M

function()
            if not vim.b[bufnr].nes_state then
                return true
            end
            local cursor = vim.api.nvim_win_get_cursor(0)
            local cursor_line = cursor[1] - 1
            local cursor_col = cursor[2]
            local suggestion_line = suggestion.range.start.line
            local last_line = vim.b[bufnr].copilotlsp_nes_last_line or cursor_line
            local last_col = vim.b[bufnr].copilotlsp_nes_last_col or cursor_col
            vim.b[bufnr].copilotlsp_nes_last_line = cursor_line
            vim.b[bufnr].copilotlsp_nes_last_col = cursor_col
            local line_distance = math.abs(cursor_line - suggestion_line)
            local last_line_distance = math.abs(last_line - suggestion_line)
            local moved_horizontally = (cursor_line == last_line) and (cursor_col ~= last_col)
            local mode = vim.api.nvim_get_mode().mode
            local should_count = false
            local first_char = mode:sub(1, 1)
            if first_char == "i" then
                if moved_horizontally or line_distance ~= last_line_distance then
                    should_count = true
                end
            elseif first_char == "v" or first_char == "V" or mode == "\22" then
                should_count = true
            elseif moved_horizontally and config.nes.count_horizontal_moves then
                should_count = true
            elseif line_distance > last_line_distance then
                should_count = true
            elseif line_distance < last_line_distance and config.nes.reset_on_approaching then
                if line_distance > 1 then
                    vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                end
            end
            if should_count then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = (vim.b[bufnr].copilotlsp_nes_cursor_moves or 0) + 1
            end
            if vim.b[bufnr].copilotlsp_nes_cursor_moves >= config.nes.move_count_threshold then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                vim.schedule(function()
                    M.clear_suggestion(bufnr, ns_id)
                end)
                return true
            end
            if config.nes.clear_on_large_distance and line_distance > config.nes.distance_threshold then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end
            return false
        end

This code segment functions to: Monitor cursor movement patterns and user interaction modes to determine when to automatically dismiss displayed suggestions based on configurable distance thresholds and movement count limits.

```lua
[TASK_BEGIN]
function()
    local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1 -- convert to 0-based
    local current_col = vim.api.nvim_win_get_cursor(0)[2]
    if current_line ~= vim.b[bufnr].copilotlsp_nes_last_line or current_col ~= vim.b[bufnr].copilotlsp_nes_last_col then
        M.clear_suggestion(bufnr, ns_id)
        vim.b[bufnr].copilotlsp_nes_last_line = current_line
        vim.b[bufnr].copilotlsp_nes_last_col = current_col
    end
end
[TASK_END]
```

function()
    local current_line = vim.api.nvim_win_get_cursor(0)[1] - 1
    local current_col = vim.api.nvim_win_get_cursor(0)[2]
    if current_line ~= vim.b[bufnr].copilotlsp_nes_last_line or current_col ~= vim.b[bufnr].copilotlsp_nes_last_col then
        M.clear_suggestion(bufnr, ns_id)
        vim.b[bufnr].copilotlsp_nes_last_line = current_line
        vim.b[bufnr].copilotlsp_nes_last_col = current_col
    end
end


local M = {}
local config = require("copilot-lsp.config").config

---@param bufnr integer
---@param ns_id integer
local function _dismiss_suggestion(bufnr, ns_id)
    pcall(vim.api.nvim_buf_clear_namespace, bufnr, ns_id, 0, -1)
end

---@param bufnr? integer
---@param ns_id integer
function M.clear_suggestion(bufnr, ns_id)
    bufnr = bufnr and bufnr > 0 and bufnr or vim.api.nvim_get_current_buf()
    -- Validate buffer exists before accessing buffer-scoped variables
    if not vim.api.nvim_buf_is_valid(bufnr) then
        return
    end
    if vim.b[bufnr].nes_jump then
        vim.b[bufnr].nes_jump = false
        return
    end
    _dismiss_suggestion(bufnr, ns_id)
    ---@type copilotlsp.InlineEdit
    local state = vim.b[bufnr].nes_state
    if not state then
        return
    end

    -- Clear buffer variables
    vim.b[bufnr].nes_state = nil
    vim.b[bufnr].copilotlsp_nes_cursor_moves = nil
    vim.b[bufnr].copilotlsp_nes_last_line = nil
    vim.b[bufnr].copilotlsp_nes_last_col = nil
end

---@private
---@param bufnr integer
---@param edit lsp.TextEdit
---@return copilotlsp.nes.InlineEditPreview
function M._calculate_preview(bufnr, edit)
    local text = edit.newText
    local range = edit.range
    local start_line = range.start.line
    local start_char = range.start.character
    local end_line = range["end"].line
    local end_char = range["end"].character

    -- Split text by newline. Use plain=true to handle trailing newline correctly.
    local new_lines = vim.split(text, "\n", { plain = true })
    local num_new_lines = #new_lines

    local old_lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line + 1, false)
    local num_old_lines = #old_lines

    local is_same_line = start_line == end_line
    local is_deletion = text == ""
    local lines_edit = is_same_line or (start_char == 0 and end_char == 0)
    local is_insertion = is_same_line and start_char == end_char

    if is_deletion and is_insertion then
        -- no-op
        return {}
    end

    if is_deletion and lines_edit then
        return {
            deletion = {
                range = edit.range,
            },
        }
    end

    if is_insertion and num_new_lines == 1 and text ~= "" then
        -- inline insertion
        return {
            inline_insertion = {
                text = text,
                line = start_line,
                character = start_char,
            },
        }
    end

    if is_insertion and num_new_lines > 1 then
        if start_char == #old_lines[1] and new_lines[1] == "" then
            -- insert lines after the start line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 2), "\n"),
                    line = start_line,
                },
            }
        end

        if end_char == 0 and new_lines[num_new_lines] == "" then
            -- insert lines before the end line
            return {
                lines_insertion = {
                    text = table.concat(vim.list_slice(new_lines, 1, num_new_lines - 1), "\n"),
                    line = start_line,
                    above = true,
                },
            }
        end
    end

    -- insert lines in the middle
    local prefix = old_lines[1]:sub(1, start_char)
    local suffix = old_lines[num_old_lines]:sub(end_char + 1)
    local new_lines_extend = vim.deepcopy(new_lines)
    new_lines_extend[1] = prefix .. new_lines_extend[1]
    new_lines_extend[num_new_lines] = new_lines_extend[num_new_lines] .. suffix
    local insertion = table.concat(new_lines_extend, "\n")

    return {
        deletion = {
            range = {
                start = { line = start_line, character = 0 },
                ["end"] = { line = end_line, character = #old_lines[num_old_lines] },
            },
        },
        lines_insertion = {
            text = insertion,
            line = end_line,
        },
    }
end

---@private
---@param bufnr integer
---@param ns_id integer
---@param preview copilotlsp.nes.InlineEditPreview

---@private
---@param bufnr integer
---@param ns_id integer
---@param edits copilotlsp.InlineEdit[]
function M._display_next_suggestion(bufnr, ns_id, edits)
    M.clear_suggestion(bufnr, ns_id)
    if not edits or #edits == 0 then
        return
    end

    local suggestion = edits[1]
    local preview = M._calculate_preview(bufnr, suggestion)
    M._display_preview(bufnr, ns_id, preview)

    vim.b[bufnr].nes_state = suggestion
    vim.b[bufnr].copilotlsp_nes_namespace_id = ns_id
    vim.b[bufnr].copilotlsp_nes_cursor_moves = 1

    vim.api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI" }, {
        buffer = bufnr,
        callback = function()
            if not vim.b[bufnr].nes_state then
                return true
            end

            -- Get cursor position
            local cursor = vim.api.nvim_win_get_cursor(0)
            local cursor_line = cursor[1] - 1 -- 0-indexed
            local cursor_col = cursor[2]
            local suggestion_line = suggestion.range.start.line

            -- Store previous position
            local last_line = vim.b[bufnr].copilotlsp_nes_last_line or cursor_line
            local last_col = vim.b[bufnr].copilotlsp_nes_last_col or cursor_col

            -- Update stored position
            vim.b[bufnr].copilotlsp_nes_last_line = cursor_line
            vim.b[bufnr].copilotlsp_nes_last_col = cursor_col

            -- Calculate distance to suggestion
            local line_distance = math.abs(cursor_line - suggestion_line)
            local last_line_distance = math.abs(last_line - suggestion_line)

            -- Check if cursor changed position on same line
            local moved_horizontally = (cursor_line == last_line) and (cursor_col ~= last_col)

            -- Get current mode
            local mode = vim.api.nvim_get_mode().mode

            -- Determine if we should count this movement
            local should_count = false
            local first_char = mode:sub(1, 1)

            -- In insert mode, only count cursor movements, not text changes
            if first_char == "i" then
                if moved_horizontally or line_distance ~= last_line_distance then
                    should_count = true
                end
            elseif first_char == "v" or first_char == "V" or mode == "\22" then
                should_count = true
            -- In normal mode with horizontal movement
            elseif moved_horizontally and config.nes.count_horizontal_moves then
                should_count = true
            -- In normal mode with line changes
            elseif line_distance > last_line_distance then
                should_count = true
            -- Moving toward suggestion in normal mode
            elseif line_distance < last_line_distance and config.nes.reset_on_approaching then
                if line_distance > 1 then -- Don't reset if 0 or 1 line away
                    vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                end
            end

            -- Update counter if needed
            if should_count then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = (vim.b[bufnr].copilotlsp_nes_cursor_moves or 0) + 1
            end

            -- Clear if counter threshold reached
            if vim.b[bufnr].copilotlsp_nes_cursor_moves >= config.nes.move_count_threshold then
                vim.b[bufnr].copilotlsp_nes_cursor_moves = 0
                vim.schedule(function()
                    M.clear_suggestion(bufnr, ns_id)
                end)
                return true
            end

            -- Optional: Clear on large distance
            if config.nes.clear_on_large_distance and line_distance > config.nes.distance_threshold then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end

            return false -- Keep the autocmd
        end,
    })
    -- Also clear on text changes that affect the suggestion area
    vim.api.nvim_create_autocmd({ "TextChanged", "TextChangedI" }, {
        buffer = bufnr,
        callback = function()
            if not vim.b[bufnr].nes_state then
                return true
            end
            -- Check if the text at the suggestion position has changed
            local start_line = suggestion.range.start.line
            -- If the lines are no longer in the buffer, clear the suggestion
            if start_line >= vim.api.nvim_buf_line_count(bufnr) then
                M.clear_suggestion(bufnr, ns_id)
                return true
            end
            return false -- Keep the autocmd
        end,
    })
end

return M

function M._display_preview(bufnr, ns_id, preview)
    if preview.deletion then
        local range = preview.deletion.range
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
            hl_group = "CopilotLspNesDelete",
            end_row = range["end"].line,
            end_col = range["end"].character,
        })
    end
    local inline_insertion = preview.inline_insertion
    if inline_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(inline_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, inline_insertion.line, inline_insertion.character, {
            virt_text = virt_lines[1],
            virt_text_pos = "inline",
        })
    end
    local lines_insertion = preview.lines_insertion
    if lines_insertion then
        local virt_lines =
            require("copilot-lsp.util").hl_text_to_virt_lines(lines_insertion.text, vim.bo[bufnr].filetype)
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, lines_insertion.line, 0, {
            virt_lines = virt_lines,
            virt_lines_above = lines_insertion.above,
        })
    end
end

This code segment functions to: Render visual indicators for proposed text modifications by applying syntax-highlighted virtual text overlays and highlighting ranges for content removal within a buffer.

```lua
[TASK_BEGIN]
function M._display_preview(bufnr, ns_id, preview)
    -- Handle deletion preview
    if preview.deletion then
        local range = preview.deletion.range
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
            end_line = range["end"].line,
            end_col = range["end"].character,
            hl_group = config.nes.hl_group.delete,
            hl_eol = true,
        })
    end

    -- Handle inline insertion preview
    if preview.inline_insertion then
        local ins = preview.inline_insertion
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, ins.line, ins.character, {
            virt_text = {{ins.text, config.nes.hl_group.insert}},
            virt_text_pos = "inline",
        })
    end

    -- Handle multi-line insertion preview
    if preview.lines_insertion then
        local ins = preview.lines_insertion
        local lines = vim.split(ins.text, "\n", { plain = true })
        for i, line in ipairs(lines) do
            local lnum = ins.line + (ins.above and -1 or 0) + (i - 1)
            vim.api.nvim_buf_set_extmark(bufnr, ns_id, lnum, 0, {
                virt_text = {{"│ " .. line, config.nes.hl_group.insert}},
                virt_text_pos = "eol",
            })
        end
    end
end
[TASK_END]
```

function M._display_preview(bufnr, ns_id, preview)
    if preview.deletion then
        local range = preview.deletion.range
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, range.start.line, range.start.character, {
            end_line = range["end"].line,
            end_col = range["end"].character,
            hl_group = config.nes.hl_group.delete,
            hl_eol = true,
        })
    end
    if preview.inline_insertion then
        local ins = preview.inline_insertion
        vim.api.nvim_buf_set_extmark(bufnr, ns_id, ins.line, ins.character, {
            virt_text = {{ins.text, config.nes.hl_group.insert}},
            virt_text_pos = "inline",
        })
    end
    if preview.lines_insertion then
        local ins = preview.lines_insertion
        local lines = vim.split(ins.text, "\n", { plain = true })
        for i, line in ipairs(lines) do
            local lnum = ins.line + (ins.above and -1 or 0) + (i - 1)
            vim.api.nvim_buf_set_extmark(bufnr, ns_id, lnum, 0, {
                virt_text = {{"│ " .. line, config.nes.hl_group.insert}},
                virt_text_pos = "eol",
            })
        end
    end
end