summaryrefslogtreecommitdiff
path: root/lua/blink/cmp/completion/windows/render/init.lua
blob: 1422ec2e22a8ffe1cd151ebade36ca89f909bb6f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
--- @class blink.cmp.Renderer
--- @field def blink.cmp.Draw
--- @field padding number[]
--- @field gap number
--- @field columns blink.cmp.DrawColumn[]
---
--- @field new fun(draw: blink.cmp.Draw): blink.cmp.Renderer
--- @field draw fun(self: blink.cmp.Renderer, context: blink.cmp.Context, bufnr: number, items: blink.cmp.CompletionItem[])
--- @field get_component_column_location fun(self: blink.cmp.Renderer, component_name: string): { column_idx: number, component_idx: number }
--- @field get_component_start_col fun(self: blink.cmp.Renderer, component_name: string): number
--- @field get_alignment_start_col fun(self: blink.cmp.Renderer): number

local ns = vim.api.nvim_create_namespace('blink_cmp_renderer')

--- @type blink.cmp.Renderer
--- @diagnostic disable-next-line: missing-fields
local renderer = {}

function renderer.new(draw)
  --- Convert the component names in the columns to the component definitions
  --- @type blink.cmp.DrawComponent[][]
  local columns_definitions = vim.tbl_map(function(column)
    local components = {}
    for _, component_name in ipairs(column) do
      local component = draw.components[component_name]
      assert(component ~= nil, 'No component definition found for component: "' .. component_name .. '"')
      table.insert(components, draw.components[component_name])
    end

    return {
      components = components,
      gap = column.gap or 0,
    }
  end, draw.columns)

  local padding = type(draw.padding) == 'number' and { draw.padding, draw.padding } or draw.padding
  --- @cast padding number[]

  local self = setmetatable({}, { __index = renderer })
  self.padding = padding
  self.gap = draw.gap
  self.def = draw
  self.columns = vim.tbl_map(
    function(column_definition)
      return require('blink.cmp.completion.windows.render.column').new(
        column_definition.components,
        column_definition.gap
      )
    end,
    columns_definitions
  )
  return self
end

function renderer:draw(context, bufnr, items)
  -- gather contexts
  local draw_contexts = require('blink.cmp.completion.windows.render.context').get_from_items(context, self.def, items)

  -- render the columns
  for _, column in ipairs(self.columns) do
    column:render(draw_contexts)
  end

  -- apply to the buffer
  local lines = {}
  for idx, _ in ipairs(draw_contexts) do
    local line = ''
    if self.padding[1] > 0 then line = string.rep(' ', self.padding[1]) end

    for _, column in ipairs(self.columns) do
      local text = column:get_line_text(idx)
      if #text > 0 then line = line .. text .. string.rep(' ', self.gap) end
    end
    line = line:sub(1, -self.gap - 1)

    if self.padding[2] > 0 then line = line .. string.rep(' ', self.padding[2]) end

    table.insert(lines, line)
  end
  vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)
  vim.api.nvim_set_option_value('modified', false, { buf = bufnr })

  -- Setting highlights is slow and we update on every keystroke so we instead use a decoration provider
  -- which will only render highlights of the visible lines. This also avoids having to do virtual scroll
  -- like nvim-cmp does, which breaks on UIs like neovide
  vim.api.nvim_set_decoration_provider(ns, {
    on_win = function(_, _, win_bufnr) return bufnr == win_bufnr end,
    on_line = function(_, _, _, line)
      local offset = self.padding[1]
      for _, column in ipairs(self.columns) do
        local text = column:get_line_text(line + 1)
        if #text > 0 then
          local highlights = column:get_line_highlights(line + 1)
          for _, highlight in ipairs(highlights) do
            local col = offset + highlight[1]
            local end_col = offset + highlight[2]
            vim.api.nvim_buf_set_extmark(bufnr, ns, line, col, {
              end_col = end_col,
              hl_group = highlight.group,
              hl_mode = 'combine',
              hl_eol = true,
              ephemeral = true,
            })
          end
          offset = offset + #text + self.gap
        end
      end
    end,
  })
end

function renderer:get_component_column_location(component_name)
  for column_idx, column in ipairs(self.def.columns) do
    for component_idx, other_component_name in ipairs(column) do
      if other_component_name == component_name then return { column_idx, component_idx } end
    end
  end
  error('No component found with name: ' .. component_name)
end

function renderer:get_component_start_col(component_name)
  local column_idx, component_idx = unpack(self:get_component_column_location(component_name))

  -- add previous columns
  local start_col = self.padding[1]
  for i = 1, column_idx - 1 do
    start_col = start_col + self.columns[i].width + self.gap
  end

  -- add previous components
  local line = self.columns[column_idx].lines[1]
  if not line then return start_col end
  for i = 1, component_idx - 1 do
    start_col = start_col + #line[i]
  end

  return start_col
end

function renderer:get_alignment_start_col()
  local component_name = self.def.align_to
  if component_name == nil or component_name == 'none' or component_name == 'cursor' then return 0 end
  return self:get_component_start_col(component_name)
end

return renderer