/ lua / cellmode / view / cell_layout.lua
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