fix(view): prevent backspace from deleting into prefix area (#135)
Problem: in insert mode, `<BS>`, `<C-h>`, `<C-w>`, and `<C-u>` could delete backwards past the name column boundary into the icon/permissions/ID prefix, corrupting the line and breaking the parser. Solution: add buffer-local insert-mode expr keymaps that compute the name column boundary (cached per line) and return no-op when the cursor is already at or before it. `<C-u>` emits exactly enough `<BS>` keys to delete back to the boundary, never past it. Closes #133
This commit is contained in:
parent
047bc0a94d
commit
e016651abe
2 changed files with 141 additions and 0 deletions
|
|
@ -145,6 +145,9 @@ end
|
||||||
---@type table<integer, oil.ViewData>
|
---@type table<integer, oil.ViewData>
|
||||||
local session = {}
|
local session = {}
|
||||||
|
|
||||||
|
---@type table<integer, { lnum: integer, min_col: integer }>
|
||||||
|
local insert_boundary = {}
|
||||||
|
|
||||||
---@return integer[]
|
---@return integer[]
|
||||||
M.get_all_buffers = function()
|
M.get_all_buffers = function()
|
||||||
return vim.tbl_filter(vim.api.nvim_buf_is_loaded, vim.tbl_keys(session))
|
return vim.tbl_filter(vim.api.nvim_buf_is_loaded, vim.tbl_keys(session))
|
||||||
|
|
@ -418,6 +421,79 @@ local function show_insert_guide(bufnr)
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
---@return integer
|
||||||
|
local function update_insert_boundary(bufnr)
|
||||||
|
local cur = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local cached = insert_boundary[bufnr]
|
||||||
|
if cached and cached.lnum == cur[1] then
|
||||||
|
return cached.min_col
|
||||||
|
end
|
||||||
|
|
||||||
|
local adapter = util.get_adapter(bufnr, true)
|
||||||
|
if not adapter then
|
||||||
|
return 0
|
||||||
|
end
|
||||||
|
|
||||||
|
local parser = require('oil.mutator.parser')
|
||||||
|
local line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1]
|
||||||
|
local column_defs = columns.get_supported_columns(adapter)
|
||||||
|
local result = parser.parse_line(adapter, line, column_defs)
|
||||||
|
local min_col = 0
|
||||||
|
if result and result.ranges then
|
||||||
|
min_col = result.ranges.name[1]
|
||||||
|
end
|
||||||
|
insert_boundary[bufnr] = { lnum = cur[1], min_col = min_col }
|
||||||
|
return min_col
|
||||||
|
end
|
||||||
|
|
||||||
|
---@param bufnr integer
|
||||||
|
local function setup_insert_constraints(bufnr)
|
||||||
|
if not config.constrain_cursor then
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_bs_rhs(bufnr_inner)
|
||||||
|
return function()
|
||||||
|
local min_col = update_insert_boundary(bufnr_inner)
|
||||||
|
local col = vim.fn.col('.')
|
||||||
|
if col <= min_col + 1 then
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
return '<BS>'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_cu_rhs(bufnr_inner)
|
||||||
|
return function()
|
||||||
|
local min_col = update_insert_boundary(bufnr_inner)
|
||||||
|
local col = vim.fn.col('.')
|
||||||
|
if col <= min_col + 1 then
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
local count = col - min_col - 1
|
||||||
|
return string.rep('<BS>', count)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function make_cw_rhs(bufnr_inner)
|
||||||
|
return function()
|
||||||
|
local min_col = update_insert_boundary(bufnr_inner)
|
||||||
|
local col = vim.fn.col('.')
|
||||||
|
if col <= min_col + 1 then
|
||||||
|
return ''
|
||||||
|
end
|
||||||
|
return '<C-w>'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local opts = { buffer = bufnr, expr = true, nowait = true, silent = true }
|
||||||
|
vim.keymap.set('i', '<BS>', make_bs_rhs(bufnr), opts)
|
||||||
|
vim.keymap.set('i', '<C-h>', make_bs_rhs(bufnr), opts)
|
||||||
|
vim.keymap.set('i', '<C-u>', make_cu_rhs(bufnr), opts)
|
||||||
|
vim.keymap.set('i', '<C-w>', make_cw_rhs(bufnr), opts)
|
||||||
|
end
|
||||||
|
|
||||||
---Redraw original path virtual text for trash buffer
|
---Redraw original path virtual text for trash buffer
|
||||||
---@param bufnr integer
|
---@param bufnr integer
|
||||||
local function redraw_trash_virtual_text(bufnr)
|
local function redraw_trash_virtual_text(bufnr)
|
||||||
|
|
@ -512,6 +588,7 @@ M.initialize = function(bufnr)
|
||||||
callback = function()
|
callback = function()
|
||||||
local view_data = session[bufnr]
|
local view_data = session[bufnr]
|
||||||
session[bufnr] = nil
|
session[bufnr] = nil
|
||||||
|
insert_boundary[bufnr] = nil
|
||||||
if view_data and view_data.fs_event then
|
if view_data and view_data.fs_event then
|
||||||
view_data.fs_event:stop()
|
view_data.fs_event:stop()
|
||||||
end
|
end
|
||||||
|
|
@ -546,6 +623,7 @@ M.initialize = function(bufnr)
|
||||||
group = 'Oil',
|
group = 'Oil',
|
||||||
buffer = bufnr,
|
buffer = bufnr,
|
||||||
callback = function()
|
callback = function()
|
||||||
|
insert_boundary[bufnr] = nil
|
||||||
if vim.w.oil_saved_ve ~= nil then
|
if vim.w.oil_saved_ve ~= nil then
|
||||||
vim.wo.virtualedit = vim.w.oil_saved_ve
|
vim.wo.virtualedit = vim.w.oil_saved_ve
|
||||||
vim.w.oil_saved_ve = nil
|
vim.w.oil_saved_ve = nil
|
||||||
|
|
@ -686,6 +764,7 @@ M.initialize = function(bufnr)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
keymap_util.set_keymaps(config.keymaps, bufnr)
|
keymap_util.set_keymaps(config.keymaps, bufnr)
|
||||||
|
setup_insert_constraints(bufnr)
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param adapter oil.Adapter
|
---@param adapter oil.Adapter
|
||||||
|
|
|
||||||
|
|
@ -124,6 +124,68 @@ describe('regression tests', function()
|
||||||
})
|
})
|
||||||
end)
|
end)
|
||||||
|
|
||||||
|
it('BS at constraint boundary is a no-op', function()
|
||||||
|
tmpdir:create({ 'a.txt' })
|
||||||
|
oil.setup({ constrain_cursor = 'name', columns = {} })
|
||||||
|
vim.cmd.edit({ args = { 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') } })
|
||||||
|
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
|
||||||
|
test_util.actions.focus('a.txt')
|
||||||
|
local line_before = vim.api.nvim_get_current_line()
|
||||||
|
local cur = vim.api.nvim_win_get_cursor(0)
|
||||||
|
local parser = require('oil.mutator.parser')
|
||||||
|
local adapter = require('oil.util').get_adapter(0, true)
|
||||||
|
local columns_mod = require('oil.columns')
|
||||||
|
local column_defs = columns_mod.get_supported_columns(adapter)
|
||||||
|
local result = parser.parse_line(adapter, line_before, column_defs)
|
||||||
|
local name_col = result.ranges.name[1]
|
||||||
|
vim.api.nvim_win_set_cursor(0, { cur[1], name_col })
|
||||||
|
vim.cmd.startinsert()
|
||||||
|
vim.wait(20)
|
||||||
|
test_util.feedkeys({ '<BS>' }, 10)
|
||||||
|
vim.cmd.stopinsert()
|
||||||
|
vim.wait(10)
|
||||||
|
local line_after = vim.api.nvim_get_current_line()
|
||||||
|
assert.equals(line_before, line_after)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('BS within name works normally', function()
|
||||||
|
tmpdir:create({ 'a.txt' })
|
||||||
|
oil.setup({ constrain_cursor = 'name', columns = {} })
|
||||||
|
vim.cmd.edit({ args = { 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') } })
|
||||||
|
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
|
||||||
|
test_util.actions.focus('a.txt')
|
||||||
|
local line_before = vim.api.nvim_get_current_line()
|
||||||
|
test_util.feedkeys({ 'A', 'x', '<BS>' }, 10)
|
||||||
|
vim.cmd.stopinsert()
|
||||||
|
vim.wait(10)
|
||||||
|
local line_after = vim.api.nvim_get_current_line()
|
||||||
|
assert.equals(line_before, line_after)
|
||||||
|
end)
|
||||||
|
|
||||||
|
it('BS does not cross into prefix area', function()
|
||||||
|
tmpdir:create({ 'a.txt' })
|
||||||
|
oil.setup({ constrain_cursor = 'name', columns = {} })
|
||||||
|
vim.cmd.edit({ args = { 'oil://' .. vim.fn.fnamemodify(tmpdir.path, ':p') } })
|
||||||
|
test_util.wait_for_autocmd({ 'User', pattern = 'OilEnter' })
|
||||||
|
test_util.actions.focus('a.txt')
|
||||||
|
local line_before = vim.api.nvim_get_current_line()
|
||||||
|
local parser = require('oil.mutator.parser')
|
||||||
|
local adapter = require('oil.util').get_adapter(0, true)
|
||||||
|
local columns_mod = require('oil.columns')
|
||||||
|
local column_defs = columns_mod.get_supported_columns(adapter)
|
||||||
|
local result = parser.parse_line(adapter, line_before, column_defs)
|
||||||
|
local name_col = result.ranges.name[1]
|
||||||
|
vim.api.nvim_win_set_cursor(0, { vim.api.nvim_win_get_cursor(0)[1], name_col + 1 })
|
||||||
|
vim.cmd.startinsert()
|
||||||
|
vim.wait(20)
|
||||||
|
test_util.feedkeys({ '<BS>', '<BS>', '<BS>' }, 10)
|
||||||
|
vim.cmd.stopinsert()
|
||||||
|
vim.wait(10)
|
||||||
|
local line_after = vim.api.nvim_get_current_line()
|
||||||
|
local id_prefix = line_before:match('^(/%d+ )')
|
||||||
|
assert.truthy(line_after:find(id_prefix, 1, true))
|
||||||
|
end)
|
||||||
|
|
||||||
it('can open files from floating window', function()
|
it('can open files from floating window', function()
|
||||||
tmpdir:create({ 'a.txt' })
|
tmpdir:create({ 'a.txt' })
|
||||||
oil.open_float(tmpdir.path)
|
oil.open_float(tmpdir.path)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue