controller.lua
1 local session_store = require("cellmode.runtime.session_store") 2 local cell_layout = require("cellmode.view.cell_layout") 3 local overlay = require("cellmode.view.overlay") 4 local sticky_header = require("cellmode.view.sticky_header") 5 local csv_parser = require("cellmode.codec.csv_parser") 6 local keymaps = require("cellmode.runtime.keymaps") 7 8 local M = {} 9 10 local function detect_format(bufnr, override) 11 if override == "csv" or override == "tsv" then 12 return override 13 end 14 local ft = vim.bo[bufnr].filetype 15 if ft == "csv" or ft == "tsv" then 16 return ft 17 end 18 local path = vim.api.nvim_buf_get_name(bufnr) 19 local ext = path:match("^.+%.([^.]+)$") 20 if ext == "tsv" then 21 return "tsv" 22 end 23 return "csv" 24 end 25 26 M.detect_format = detect_format 27 28 local function apply_window_options_for_buffer(bufnr) 29 for _, winid in ipairs(vim.fn.win_findbuf(bufnr)) do 30 overlay.apply_window_options(winid) 31 end 32 end 33 34 function M.open(bufnr, opts) 35 opts = opts or {} 36 if not vim.api.nvim_buf_is_valid(bufnr) then 37 return false, "invalid buffer" 38 end 39 local format = detect_format(bufnr, opts.format) 40 cell_layout.build(bufnr, format) 41 session_store.open(bufnr, { 42 format = format, 43 overlay_visible = true, 44 }) 45 apply_window_options_for_buffer(bufnr) 46 overlay.redraw(bufnr) 47 sticky_header.refresh_buffer(bufnr) 48 keymaps.attach(bufnr) 49 return true 50 end 51 52 function M.close(bufnr) 53 keymaps.detach(bufnr) 54 sticky_header.disable_for_buffer(bufnr) 55 session_store.close(bufnr) 56 cell_layout.clear(bufnr) 57 overlay.forget(bufnr) 58 end 59 60 function M.on_buffer_changed(bufnr, change) 61 local session = session_store.get(bufnr) 62 if not session then 63 return 64 end 65 if session.updating_buffer then 66 return 67 end 68 local result = cell_layout.apply_edit(bufnr, change) 69 if not result then 70 return 71 end 72 if result.full or result.widths_changed then 73 overlay.redraw(bufnr) 74 else 75 overlay.redraw_range(bufnr, result.first_record, result.last_record, result.widths_changed) 76 end 77 sticky_header.refresh_buffer(bufnr) 78 end 79 80 function M.toggle_overlay(bufnr) 81 local session = session_store.get(bufnr) 82 if not session then 83 return false, "session not found" 84 end 85 local visible = session.overlay_visible == false 86 session_store.set_overlay_visible(bufnr, visible) 87 overlay.set_visible(bufnr, visible) 88 if visible then 89 sticky_header.refresh_buffer(bufnr) 90 else 91 sticky_header.disable_for_buffer(bufnr) 92 end 93 return true 94 end 95 96 local function delim_for(format) 97 return csv_parser.delimiter_for_format(format) 98 end 99 100 local function encode_cell(value, delim) 101 local text = tostring(value or "") 102 local needs = text:find('"', 1, true) 103 or text:find(delim, 1, true) 104 or text:find("\n", 1, true) 105 or text:find("\r", 1, true) 106 if not needs then 107 return text 108 end 109 return '"' .. text:gsub('"', '""') .. '"' 110 end 111 112 M.encode_cell = encode_cell 113 114 local function set_lines_unmanaged(bufnr, start_row0, end_row0, lines) 115 session_store.set_updating_buffer(bufnr, true) 116 vim.api.nvim_buf_set_lines(bufnr, start_row0, end_row0, false, lines) 117 session_store.set_updating_buffer(bufnr, false) 118 end 119 120 local function set_text_unmanaged(bufnr, sr, sc, er, ec, lines) 121 session_store.set_updating_buffer(bufnr, true) 122 vim.api.nvim_buf_set_text(bufnr, sr, sc, er, ec, lines) 123 session_store.set_updating_buffer(bufnr, false) 124 end 125 126 function M.set_cell(bufnr, irec, icol, value) 127 local session = session_store.get(bufnr) 128 if not session then 129 return false, "session not found" 130 end 131 local layout = cell_layout.get(bufnr) 132 if not layout then 133 return false, "layout not built" 134 end 135 local record = layout.records[irec] 136 if not record then 137 return false, "record out of range" 138 end 139 local field = record.fields[icol] 140 local delim = delim_for(session.format) 141 local encoded = encode_cell(value, delim) 142 143 if field then 144 local sr = field.byte_start_row - 1 145 local sc = field.byte_start_col - 1 146 local er = field.byte_end_row - 1 147 local ec = field.byte_end_col 148 local replacement = vim.split(encoded, "\n", { plain = true }) 149 set_text_unmanaged(bufnr, sr, sc, er, ec, replacement) 150 else 151 local last_field = record.fields[#record.fields] 152 if not last_field then 153 return false, "record has no fields" 154 end 155 local missing = icol - #record.fields 156 local insert = "" 157 for _ = 1, missing - 1 do 158 insert = insert .. delim 159 end 160 insert = insert .. delim .. encoded 161 local sr = last_field.byte_end_row - 1 162 local sc = last_field.byte_end_col 163 set_text_unmanaged(bufnr, sr, sc, sr, sc, vim.split(insert, "\n", { plain = true })) 164 end 165 166 cell_layout.build(bufnr, session.format) 167 overlay.redraw(bufnr) 168 return true 169 end 170 171 function M.insert_row(bufnr, irec, values) 172 local session = session_store.get(bufnr) 173 if not session then 174 return false, "session not found" 175 end 176 local layout = cell_layout.get(bufnr) 177 if not layout then 178 return false, "layout not built" 179 end 180 local delim = delim_for(session.format) 181 local cells = {} 182 for i = 1, #(values or {}) do 183 cells[i] = encode_cell(values[i], delim) 184 end 185 local row_text = table.concat(cells, delim) 186 local row_lines = vim.split(row_text, "\n", { plain = true }) 187 188 local insert_at0 189 local total_records = #layout.records 190 if irec <= 0 then 191 insert_at0 = 0 192 elseif irec > total_records then 193 insert_at0 = layout.line_count 194 else 195 insert_at0 = layout.records[irec].buf_row_start - 1 196 end 197 set_lines_unmanaged(bufnr, insert_at0, insert_at0, row_lines) 198 cell_layout.build(bufnr, session.format) 199 overlay.redraw(bufnr) 200 return true 201 end 202 203 function M.delete_row(bufnr, irec) 204 local session = session_store.get(bufnr) 205 if not session then 206 return false, "session not found" 207 end 208 local layout = cell_layout.get(bufnr) 209 if not layout then 210 return false, "layout not built" 211 end 212 local record = layout.records[irec] 213 if not record then 214 return false, "record out of range" 215 end 216 set_lines_unmanaged(bufnr, record.buf_row_start - 1, record.buf_row_end, {}) 217 cell_layout.build(bufnr, session.format) 218 overlay.redraw(bufnr) 219 return true 220 end 221 222 return M