cell_layout.lua
1 local csv_parser = require("cellmode.codec.csv_parser") 2 3 local M = {} 4 5 local fn = vim.fn 6 7 local layouts = {} 8 9 local function display_width(text) 10 if not text or text == "" then 11 return 0 12 end 13 if not text:find("[\t\128-\255]") then 14 return #text 15 end 16 return fn.strdisplaywidth(text) 17 end 18 19 M.display_width = display_width 20 21 local function compute_field_widths(record) 22 local widths = {} 23 for i = 1, #record.fields do 24 local field = record.fields[i] 25 if field.multiline then 26 widths[i] = display_width(field.value:gsub("\n.*", "")) 27 else 28 widths[i] = display_width(field.value) 29 end 30 end 31 return widths 32 end 33 34 local function rebuild_widths(layout) 35 local widths = {} 36 local max_row_by_col = {} 37 local records = layout.records 38 for irec = 1, #records do 39 local fw = compute_field_widths(records[irec]) 40 for icol = 1, #fw do 41 if fw[icol] > (widths[icol] or 0) then 42 widths[icol] = fw[icol] 43 max_row_by_col[icol] = irec 44 end 45 end 46 end 47 layout.widths = widths 48 layout.max_row_by_col = max_row_by_col 49 end 50 51 local function build_record_index(layout) 52 local by_buf_row = {} 53 local records = layout.records 54 for irec = 1, #records do 55 local r = records[irec] 56 for row = r.buf_row_start, r.buf_row_end do 57 by_buf_row[row] = irec 58 end 59 end 60 layout.record_by_row = by_buf_row 61 end 62 63 local function buffer_lines(bufnr) 64 return vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) 65 end 66 67 function M.build(bufnr, format) 68 local lines = buffer_lines(bufnr) 69 local records = csv_parser.parse(lines, format) 70 local layout = { 71 bufnr = bufnr, 72 format = format, 73 records = records, 74 line_count = #lines, 75 } 76 rebuild_widths(layout) 77 build_record_index(layout) 78 layouts[bufnr] = layout 79 return layout 80 end 81 82 function M.get(bufnr) 83 return layouts[bufnr] 84 end 85 86 function M.clear(bufnr) 87 layouts[bufnr] = nil 88 end 89 90 local function records_overlap(record, row_start, row_end) 91 return record.buf_row_end >= row_start and record.buf_row_start <= row_end 92 end 93 94 local function widths_equal(a, b) 95 if #a ~= #b then 96 return false 97 end 98 for i = 1, #a do 99 if a[i] ~= b[i] then 100 return false 101 end 102 end 103 return true 104 end 105 106 function M.apply_edit(bufnr, change) 107 local layout = layouts[bufnr] 108 if not layout then 109 return nil, "no layout" 110 end 111 local lines = buffer_lines(bufnr) 112 layout.line_count = #lines 113 114 local probe_row = csv_parser.find_record_start(lines, change.first_line + 1) 115 local prev_widths = layout.widths or {} 116 local records = csv_parser.parse(lines, layout.format) 117 118 local record_by_row = {} 119 for irec = 1, #records do 120 local r = records[irec] 121 for row = r.buf_row_start, r.buf_row_end do 122 record_by_row[row] = irec 123 end 124 end 125 126 layout.records = records 127 layout.record_by_row = record_by_row 128 rebuild_widths(layout) 129 130 local widths_changed = not widths_equal(prev_widths, layout.widths) 131 132 local affected_first_row = math.min(probe_row, change.first_line + 1) 133 local affected_last_row = math.max(change.new_last_line, change.last_line, affected_first_row) 134 local first_record = record_by_row[affected_first_row] 135 local last_record = record_by_row[affected_last_row] 136 if not first_record then 137 for r = affected_first_row, #lines do 138 if record_by_row[r] then 139 first_record = record_by_row[r] 140 break 141 end 142 end 143 end 144 if not last_record then 145 for r = math.min(affected_last_row, #lines), 1, -1 do 146 if record_by_row[r] then 147 last_record = record_by_row[r] 148 break 149 end 150 end 151 end 152 153 return { 154 first_record = first_record or 1, 155 last_record = last_record or #records, 156 widths_changed = widths_changed, 157 full = widths_changed, 158 } 159 end 160 161 function M.cell_at(bufnr, row1, col1) 162 local layout = layouts[bufnr] 163 if not layout then 164 return nil 165 end 166 local irec = layout.record_by_row[row1] 167 if not irec then 168 return nil 169 end 170 local record = layout.records[irec] 171 for icol = 1, #record.fields do 172 local f = record.fields[icol] 173 if (row1 > f.byte_start_row or (row1 == f.byte_start_row and col1 >= f.byte_start_col)) 174 and (row1 < f.byte_end_row or (row1 == f.byte_end_row and col1 <= f.byte_end_col)) then 175 return { record = irec, col = icol, field = f } 176 end 177 end 178 return { record = irec, col = nil } 179 end 180 181 function M.cell_range(bufnr, irec, icol) 182 local layout = layouts[bufnr] 183 if not layout then 184 return nil 185 end 186 local record = layout.records[irec] 187 if not record then 188 return nil 189 end 190 local field = record.fields[icol] 191 if not field then 192 return nil 193 end 194 return field 195 end 196 197 return M