/ lua / cellmode / view / overlay.lua
overlay.lua
  1  local config = require("cellmode.config")
  2  local cell_layout = require("cellmode.view.cell_layout")
  3  
  4  local M = {}
  5  
  6  local ns = vim.api.nvim_create_namespace("cellmode_overlay")
  7  local hl_ready = false
  8  local visibility = {}
  9  
 10  local function setup_highlights()
 11    if hl_ready then
 12      return
 13    end
 14    hl_ready = true
 15    vim.api.nvim_set_hl(0, "CellmodePadding", { default = true })
 16    local target = "Delimiter"
 17    local target_hl = vim.api.nvim_get_hl(0, { name = target, link = false })
 18    local normal_hl = vim.api.nvim_get_hl(0, { name = "Normal", link = false })
 19    local fg = target_hl.fg or normal_hl.fg
 20    vim.api.nvim_set_hl(0, "CellmodePipe", { default = true, fg = fg, nocombine = true })
 21    vim.api.nvim_set_hl(0, "CellmodeHbar", { default = true, underline = true, sp = fg, nocombine = true })
 22    vim.api.nvim_set_hl(0, "CellmodeContinuation", { default = true, link = "NonText" })
 23    vim.api.nvim_set_hl(0, "CellmodeSpecialChar", { default = true, link = "NonText" })
 24  end
 25  
 26  function M.setup()
 27    setup_highlights()
 28  end
 29  
 30  function M.namespace()
 31    return ns
 32  end
 33  
 34  local function pipe_glyph()
 35    return config.marks.pipe or "│"
 36  end
 37  
 38  local function continuation_glyph()
 39    return config.marks.pipec or "┊"
 40  end
 41  
 42  local function place_inline(bufnr, row0, col0, chunks, opts)
 43    opts = opts or {}
 44    opts.virt_text = chunks
 45    opts.virt_text_pos = "inline"
 46    if opts.right_gravity == nil then
 47      opts.right_gravity = false
 48    end
 49    vim.api.nvim_buf_set_extmark(bufnr, ns, row0, col0, opts)
 50  end
 51  
 52  local function place_conceal(bufnr, row0, col0, end_col0)
 53    vim.api.nvim_buf_set_extmark(bufnr, ns, row0, col0, {
 54      end_row = row0,
 55      end_col = end_col0,
 56      conceal = "",
 57    })
 58  end
 59  
 60  local function pad_str(width)
 61    if width <= 0 then
 62      return ""
 63    end
 64    return string.rep(" ", width)
 65  end
 66  
 67  local function place_hbar(bufnr, row0)
 68    local line = vim.api.nvim_buf_get_lines(bufnr, row0, row0 + 1, false)[1] or ""
 69    if #line == 0 then
 70      return
 71    end
 72    vim.api.nvim_buf_set_extmark(bufnr, ns, row0, 0, {
 73      end_row = row0,
 74      end_col = #line,
 75      hl_group = "CellmodeHbar",
 76    })
 77  end
 78  
 79  local function decorate_single_line_record(bufnr, layout, record)
 80    local row0 = record.buf_row_start - 1
 81    local fields = record.fields
 82    local widths = layout.widths or {}
 83    local pipe = pipe_glyph()
 84  
 85    place_inline(bufnr, row0, 0, { { pipe, { "CellmodePipe", "CellmodeHbar" } } })
 86    place_hbar(bufnr, row0)
 87  
 88    for icol = 1, #fields do
 89      local field = fields[icol]
 90      local width = widths[icol] or 0
 91      local field_display = cell_layout.display_width(field.value)
 92      local padding = pad_str(width - field_display)
 93      local chunks = {}
 94      if padding ~= "" then
 95        chunks[#chunks + 1] = { padding, { "CellmodePadding", "CellmodeHbar" } }
 96      end
 97      chunks[#chunks + 1] = { pipe, { "CellmodePipe", "CellmodeHbar" } }
 98  
 99      local end_col
100      if field.delim_col then
101        end_col = field.delim_col
102      else
103        local line = vim.api.nvim_buf_get_lines(bufnr, row0, row0 + 1, false)[1] or ""
104        end_col = #line + 1
105      end
106      place_inline(bufnr, row0, end_col - 1, chunks, { right_gravity = true })
107  
108      if field.delim_col then
109        place_conceal(bufnr, row0, field.delim_col - 1, field.delim_col)
110      end
111  
112      if field.quoted then
113        place_conceal(bufnr, row0, field.byte_start_col - 1, field.byte_start_col)
114        place_conceal(bufnr, row0, field.byte_end_col - 1, field.byte_end_col)
115      end
116    end
117  end
118  
119  local function decorate_multiline_record(bufnr, record)
120    local cont = continuation_glyph()
121    for row = record.buf_row_start, record.buf_row_end - 1 do
122      vim.api.nvim_buf_set_extmark(bufnr, ns, row - 1, 0, {
123        virt_text = { { cont, "CellmodeContinuation" } },
124        virt_text_pos = "eol",
125        right_gravity = false,
126      })
127    end
128    place_hbar(bufnr, record.buf_row_end - 1)
129  end
130  
131  local function clear_lines(bufnr, row_start, row_end)
132    vim.api.nvim_buf_clear_namespace(bufnr, ns, row_start - 1, row_end)
133  end
134  
135  function M.clear(bufnr)
136    vim.api.nvim_buf_clear_namespace(bufnr, ns, 0, -1)
137  end
138  
139  function M.redraw(bufnr)
140    setup_highlights()
141    if not vim.api.nvim_buf_is_valid(bufnr) then
142      return
143    end
144    M.clear(bufnr)
145    if visibility[bufnr] == false then
146      return
147    end
148    local layout = cell_layout.get(bufnr)
149    if not layout then
150      return
151    end
152    local records = layout.records
153    for irec = 1, #records do
154      local record = records[irec]
155      if record.multiline then
156        decorate_multiline_record(bufnr, record)
157      else
158        decorate_single_line_record(bufnr, layout, record)
159      end
160    end
161  end
162  
163  function M.redraw_range(bufnr, first_record, last_record, widths_changed)
164    setup_highlights()
165    if not vim.api.nvim_buf_is_valid(bufnr) then
166      return
167    end
168    if visibility[bufnr] == false then
169      return
170    end
171    local layout = cell_layout.get(bufnr)
172    if not layout then
173      return
174    end
175    if widths_changed then
176      M.redraw(bufnr)
177      return
178    end
179    local records = layout.records
180    if not first_record or first_record < 1 then
181      first_record = 1
182    end
183    if not last_record or last_record > #records then
184      last_record = #records
185    end
186    if first_record > last_record then
187      return
188    end
189    local row_start = records[first_record].buf_row_start
190    local row_end = records[last_record].buf_row_end
191    clear_lines(bufnr, row_start, row_end)
192    for irec = first_record, last_record do
193      local record = records[irec]
194      if record.multiline then
195        decorate_multiline_record(bufnr, record)
196      else
197        decorate_single_line_record(bufnr, layout, record)
198      end
199    end
200  end
201  
202  function M.set_visible(bufnr, visible)
203    visibility[bufnr] = visible and true or false
204    if not visible then
205      M.clear(bufnr)
206    else
207      M.redraw(bufnr)
208    end
209  end
210  
211  function M.is_visible(bufnr)
212    return visibility[bufnr] ~= false
213  end
214  
215  function M.forget(bufnr)
216    visibility[bufnr] = nil
217    M.clear(bufnr)
218  end
219  
220  function M.apply_window_options(winid)
221    if not vim.api.nvim_win_is_valid(winid) then
222      return
223    end
224    vim.wo[winid].conceallevel = 2
225    vim.wo[winid].concealcursor = "nvic"
226  end
227  
228  return M