Mapping for Hungry Backspace

I was discussing with a reddit user u//SH4DOW_KING how to implement IntelliJ’s “hungry backspace,” which behaves as follows, where | is the cursor:

local function foo()


    |return
end

becomes

local function foo()

    |return
end

after hitting backspace, regardless of indent level, and

local function foo()
                |return
end

becomes

local function foo()
    |return
end

Essentially, if deleting whitespace, it deletes as much as possible until the next syntactically proper indentation.

I recommend binding Shift-Backspace, as I do in the mapping, to normal backspace in instances where overriding this behavior is useful, which I’ve found to be the case particularly in Python

Implementation:

local escape_code = vim.api.nvim_replace_termcodes(
    "<Esc>",
    false, false, true
)
local backspace_code = vim.api.nvim_replace_termcodes(
    "<BS>",
    false, false, true
)
local function viml_backspace()
    -- expression from a deleted reddit user
    vim.cmd([[
        let g:exprvalue =
        \ (&indentexpr isnot '' ? &indentkeys : &cinkeys) =~? '!\^F' &&
        \ &backspace =~? '.*eol\&.*start\&.*indent\&' &&
        \ !search('\S','nbW',line('.')) ? (col('.') != 1 ? "\<C-U>" : "") .
        \ "\<bs>" . (getline(line('.')-1) =~ '\S' ? "" : "\<C-F>") : "\<bs>"
        ]])
    return vim.g.exprvalue
end
local function backspace()
    local line, col = unpack(vim.api.nvim_win_get_cursor(0))
    local before_cursor_is_whitespace = vim.api.nvim_get_current_line()
        :sub(0, col)
        :match("^%s*$")

    if not before_cursor_is_whitespace then
        return require('nvim-autopairs').autopairs_bs()
    end
    if line == 1 then
        return viml_backspace()
    end
    local correct_indent = require("nvim-treesitter.indent").get_indent(line)
    local current_indent = vim.fn.indent(line)
    local previous_line_is_whitespace = vim.api.nvim_buf_get_lines(
        0, line - 2, line - 1, false
    )[1]:match("^%s*$")
    if current_indent == correct_indent then
        if previous_line_is_whitespace then
            return viml_backspace()
        end
        return backspace_code
    elseif current_indent > correct_indent then
        return escape_code .. "==0wi"
    end
    return backspace_code
end
m_o('i', '<BS>', backspace, {
    expr = true,
    noremap = true,
    replace_keycodes = false,
})
m('i', '<S-BS>', '<BS>')

The convoluted viml expression at the beginning is a good implementation of the first feature of hungry whitespace, which I may rewrite in lua at some point, but for now is pretty convenient—the issue is, it doesn’t implement the second part of the functionality I demonstrated, and nvim-autopairs has its own backspace which automatically deletes pairs, and I would like to keep that functionality as well.

The finished mapping after a couple of iterations uses a series of checks to apply the appropriate backspace output, and I tried to include every edge case and minimize external function calls. If I get around to the lua rewrite, I will certainly update this with that code!

3 Likes