autocmd.lua
1 local controller = require("cellmode.runtime.controller") 2 local session_store = require("cellmode.runtime.session_store") 3 local overlay = require("cellmode.view.overlay") 4 local sticky_header = require("cellmode.view.sticky_header") 5 local scheduler = require("cellmode.runtime.scheduler") 6 local messages = require("cellmode.runtime.messages") 7 local auto_quote = require("cellmode.runtime.auto_quote") 8 9 local M = {} 10 11 local GROUP = "cellmode" 12 local pending_change = {} 13 14 local function flush_pending(bufnr) 15 local change = pending_change[bufnr] 16 if not change then 17 return 18 end 19 pending_change[bufnr] = nil 20 controller.on_buffer_changed(bufnr, change) 21 end 22 23 local function schedule_changed(bufnr) 24 scheduler.next_tick(bufnr, "changed", function() 25 if not vim.api.nvim_buf_is_valid(bufnr) then 26 pending_change[bufnr] = nil 27 return 28 end 29 flush_pending(bufnr) 30 end) 31 end 32 33 local function record_change(bufnr, firstline, lastline, new_lastline) 34 local prev = pending_change[bufnr] 35 if not prev then 36 pending_change[bufnr] = { 37 first_line = firstline, 38 last_line = lastline, 39 new_last_line = new_lastline, 40 } 41 return 42 end 43 prev.first_line = math.min(prev.first_line, firstline) 44 prev.last_line = math.max(prev.last_line, lastline) 45 prev.new_last_line = math.max(prev.new_last_line, new_lastline) 46 end 47 48 function M.attach_buffer_tracking(bufnr) 49 if vim.b[bufnr].cellmode_lines_attached then 50 return 51 end 52 vim.api.nvim_buf_attach(bufnr, false, { 53 on_lines = function(_, changed_bufnr, _, firstline, lastline, new_lastline) 54 if not session_store.get(changed_bufnr) then 55 return 56 end 57 if session_store.is_updating_buffer(changed_bufnr) then 58 return 59 end 60 record_change(changed_bufnr, firstline, lastline, new_lastline) 61 schedule_changed(changed_bufnr) 62 end, 63 on_detach = function(_, detached_bufnr) 64 vim.b[detached_bufnr].cellmode_lines_attached = false 65 pending_change[detached_bufnr] = nil 66 end, 67 }) 68 vim.b[bufnr].cellmode_lines_attached = true 69 end 70 71 local function should_attach(bufnr) 72 if not vim.api.nvim_buf_is_valid(bufnr) then 73 return false 74 end 75 if vim.api.nvim_buf_get_name(bufnr) == "" then 76 return false 77 end 78 if not vim.bo[bufnr].modifiable then 79 return false 80 end 81 return true 82 end 83 84 local function format_for_buffer(bufnr) 85 local ft = vim.bo[bufnr].filetype 86 if ft == "csv" or ft == "tsv" then 87 return ft 88 end 89 local path = vim.api.nvim_buf_get_name(bufnr) 90 local ext = path:match("^.+%.([^.]+)$") 91 if ext == "csv" or ext == "tsv" then 92 return ext 93 end 94 return nil 95 end 96 97 local function on_buf_read_post(args) 98 local bufnr = args.buf 99 if not should_attach(bufnr) then 100 return 101 end 102 if session_store.get(bufnr) then 103 return 104 end 105 local format = format_for_buffer(bufnr) 106 if not format then 107 return 108 end 109 local ok, err = controller.open(bufnr, { format = format }) 110 if not ok then 111 messages.error(err) 112 return 113 end 114 M.attach_buffer_tracking(bufnr) 115 end 116 117 local function on_buf_wipeout(args) 118 sticky_header.disable_for_buffer(args.buf) 119 controller.close(args.buf) 120 scheduler.clear_for_buffer(args.buf) 121 pending_change[args.buf] = nil 122 end 123 124 local function on_win_enter(args) 125 local bufnr = args.buf 126 if not session_store.get(bufnr) then 127 return 128 end 129 local winid = vim.api.nvim_get_current_win() 130 if sticky_header.is_float(winid) then 131 return 132 end 133 overlay.apply_window_options(winid) 134 sticky_header.refresh(winid) 135 end 136 137 local function on_text_changed_i(args) 138 if not session_store.get(args.buf) then 139 return 140 end 141 auto_quote.handle_text_changed(args.buf) 142 end 143 144 local function on_win_scrolled() 145 local event = vim.v.event or {} 146 local handled = false 147 for key, _ in pairs(event) do 148 local winid = tonumber(key) 149 if winid and winid > 0 and not sticky_header.is_float(winid) then 150 sticky_header.refresh(winid) 151 handled = true 152 end 153 end 154 if not handled then 155 sticky_header.refresh(vim.api.nvim_get_current_win()) 156 end 157 end 158 159 local function on_win_resized() 160 local event = vim.v.event or {} 161 local wins = event.windows 162 if type(wins) == "table" then 163 for _, winid in ipairs(wins) do 164 if not sticky_header.is_float(winid) then 165 sticky_header.refresh(winid) 166 end 167 end 168 else 169 sticky_header.refresh(vim.api.nvim_get_current_win()) 170 end 171 end 172 173 local function on_win_closed(args) 174 local winid = tonumber(args.match) 175 if not winid then 176 return 177 end 178 if sticky_header.is_float(winid) then 179 sticky_header.forget_float(winid) 180 else 181 sticky_header.disable_for_win(winid) 182 end 183 end 184 185 function M.setup() 186 local group = vim.api.nvim_create_augroup(GROUP, { clear = true }) 187 vim.api.nvim_create_autocmd("BufReadPost", { group = group, callback = on_buf_read_post }) 188 vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, { group = group, callback = on_buf_wipeout }) 189 vim.api.nvim_create_autocmd({ "BufWinEnter", "WinEnter" }, { group = group, callback = on_win_enter }) 190 vim.api.nvim_create_autocmd("TextChangedI", { group = group, callback = on_text_changed_i }) 191 vim.api.nvim_create_autocmd("WinScrolled", { group = group, callback = on_win_scrolled }) 192 vim.api.nvim_create_autocmd("WinResized", { group = group, callback = on_win_resized }) 193 vim.api.nvim_create_autocmd("WinClosed", { group = group, callback = on_win_closed }) 194 end 195 196 return M