From 022e92c4b4c9cde0bebf005fcf340a3f8179e135 Mon Sep 17 00:00:00 2001 From: Barrett Ruth Date: Fri, 6 Mar 2026 14:35:10 -0500 Subject: [PATCH] feat(view): position cursor at name column on new empty lines Problem: pressing `o`/`O` in a canola buffer placed the cursor at column 0, requiring manual navigation past concealed ID prefixes and column text (icons, permissions) to reach the name column. Solution: add `show_insert_guide()` which temporarily sets `virtualedit=all` on empty lines and positions the cursor at the name column. Computes the correct virtual column by measuring the visible column prefix width via `nvim_strwidth`, adjusting for `conceallevel` (0=full ID width, 1=replacement char, 2/3=hidden). Restores `virtualedit` on `TextChangedI` or `InsertLeave`. --- lua/canola/view.lua | 93 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/lua/canola/view.lua b/lua/canola/view.lua index faad884..0f75faf 100644 --- a/lua/canola/view.lua +++ b/lua/canola/view.lua @@ -340,6 +340,84 @@ local function constrain_cursor(bufnr, mode) end end +---@param bufnr integer +local function show_insert_guide(bufnr) + if not config.constrain_cursor then + return + end + if bufnr ~= vim.api.nvim_get_current_buf() then + return + end + local adapter = util.get_adapter(bufnr, true) + if not adapter then + return + end + + local cur = vim.api.nvim_win_get_cursor(0) + local current_line = vim.api.nvim_buf_get_lines(bufnr, cur[1] - 1, cur[1], true)[1] + if current_line ~= '' then + return + end + + local all_lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, true) + local ref_line + if cur[1] > 1 and all_lines[cur[1] - 1] ~= '' then + ref_line = all_lines[cur[1] - 1] + elseif cur[1] < #all_lines and all_lines[cur[1] + 1] ~= '' then + ref_line = all_lines[cur[1] + 1] + else + for i, line in ipairs(all_lines) do + if line ~= '' and i ~= cur[1] then + ref_line = line + break + end + end + end + if not ref_line then + return + end + + local parser = require('canola.mutator.parser') + local column_defs = columns.get_supported_columns(adapter) + local result = parser.parse_line(adapter, ref_line, column_defs) + if not result or not result.ranges then + return + end + + local id_end = result.ranges.id[2] + 1 + local col_prefix = ref_line:sub(id_end + 1, result.ranges.name[1]) + local col_width = vim.api.nvim_strwidth(col_prefix) + local id_width + local cole = vim.wo.conceallevel + if cole >= 2 then + id_width = 0 + elseif cole == 1 then + id_width = 1 + else + id_width = vim.api.nvim_strwidth(ref_line:sub(1, id_end)) + end + local virtual_col = id_width + col_width + if virtual_col <= 0 then + return + end + + vim.w.canola_saved_ve = vim.wo.virtualedit + vim.wo.virtualedit = 'all' + vim.api.nvim_win_set_cursor(0, { cur[1], virtual_col }) + + vim.api.nvim_create_autocmd('TextChangedI', { + group = 'Canola', + buffer = bufnr, + once = true, + callback = function() + if vim.w.canola_saved_ve ~= nil then + vim.wo.virtualedit = vim.w.canola_saved_ve + vim.w.canola_saved_ve = nil + end + end, + }) +end + ---Redraw original path virtual text for trash buffer ---@param bufnr integer local function redraw_trash_virtual_text(bufnr) @@ -458,7 +536,20 @@ M.initialize = function(bufnr) callback = function() -- For some reason the cursor bounces back to its original position, -- so we have to defer the call - vim.schedule_wrap(constrain_cursor)(bufnr, config.constrain_cursor) + vim.schedule(function() + constrain_cursor(bufnr, config.constrain_cursor) + show_insert_guide(bufnr) + end) + end, + }) + vim.api.nvim_create_autocmd('InsertLeave', { + group = 'Canola', + buffer = bufnr, + callback = function() + if vim.w.canola_saved_ve ~= nil then + vim.wo.virtualedit = vim.w.canola_saved_ve + vim.w.canola_saved_ve = nil + end end, }) vim.api.nvim_create_autocmd({ 'CursorMoved', 'ModeChanged' }, {