/ lua / cellmode / runtime / autocmd.lua
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