summaryrefslogtreecommitdiff
path: root/stowables-dotlocal/share/nvim/site/pack/manual/start/nvim-cmp-v0.0.1/lua/cmp/entry.lua
blob: 83eb448b01941911ace5e0077702776ab81bfe7b (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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
local cache = require('cmp.utils.cache')
local char = require('cmp.utils.char')
local misc = require('cmp.utils.misc')
local str = require('cmp.utils.str')
local config = require('cmp.config')
local types = require('cmp.types')
local matcher = require('cmp.matcher')

---@class cmp.Entry
---@field public id integer
---@field public cache cmp.Cache
---@field public match_cache cmp.Cache
---@field public score integer
---@field public exact boolean
---@field public matches table
---@field public context cmp.Context
---@field public source cmp.Source
---@field public source_offset integer
---@field public source_insert_range lsp.Range
---@field public source_replace_range lsp.Range
---@field public completion_item lsp.CompletionItem
---@field public resolved_completion_item lsp.CompletionItem|nil
---@field public resolved_callbacks fun()[]
---@field public resolving boolean
---@field public confirmed boolean
local entry = {}

---Create new entry
---@param ctx cmp.Context
---@param source cmp.Source
---@param completion_item lsp.CompletionItem
---@return cmp.Entry
entry.new = function(ctx, source, completion_item)
  local self = setmetatable({}, { __index = entry })
  self.id = misc.id('entry.new')
  self.cache = cache.new()
  self.match_cache = cache.new()
  self.score = 0
  self.exact = false
  self.matches = {}
  self.context = ctx
  self.source = source
  self.source_offset = source.request_offset
  self.source_insert_range = source:get_default_insert_range()
  self.source_replace_range = source:get_default_replace_range()
  self.completion_item = completion_item
  self.resolved_completion_item = nil
  self.resolved_callbacks = {}
  self.resolving = false
  self.confirmed = false
  return self
end

---Make offset value
---@return integer
entry.get_offset = function(self)
  return self.cache:ensure({ 'get_offset', self.resolved_completion_item and 1 or 0 }, function()
    local offset = self.source_offset
    if misc.safe(self:get_completion_item().textEdit) then
      local range = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
      if range then
        local c = misc.to_vimindex(self.context.cursor_line, range.start.character)
        for idx = c, self.source_offset do
          if not char.is_white(string.byte(self.context.cursor_line, idx)) then
            offset = idx
            break
          end
        end
      end
    else
      -- NOTE
      -- The VSCode does not implement this but it's useful if the server does not care about word patterns.
      -- We should care about this performance.
      local word = self:get_word()
      for idx = self.source_offset - 1, self.source_offset - #word, -1 do
        if char.is_semantic_index(self.context.cursor_line, idx) then
          local c = string.byte(self.context.cursor_line, idx)
          if char.is_white(c) then
            break
          end
          local match = true
          for i = 1, self.source_offset - idx do
            local c1 = string.byte(word, i)
            local c2 = string.byte(self.context.cursor_line, idx + i - 1)
            if not c1 or not c2 or c1 ~= c2 then
              match = false
              break
            end
          end
          if match then
            offset = math.min(offset, idx)
          end
        end
      end
    end
    return offset
  end)
end

---Create word for vim.CompletedItem
---NOTE: This method doesn't clear the cache after completionItem/resolve.
---@return string
entry.get_word = function(self)
  return self.cache:ensure({ 'get_word' }, function()
    --NOTE: This is nvim-cmp specific implementation.
    if misc.safe(self:get_completion_item().word) then
      return self:get_completion_item().word
    end

    local word
    if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
      word = str.trim(self:get_completion_item().textEdit.newText)
      if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
        word = vim.lsp.util.parse_snippet(word)
      end
      local overwrite = self:get_overwrite()
      if 0 < overwrite[2] or self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
        word = str.get_word(word, string.byte(self.context.cursor_after_line, 1), overwrite[1] or 0)
      end
    elseif not misc.empty(self:get_completion_item().insertText) then
      word = str.trim(self:get_completion_item().insertText)
      if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
        word = str.get_word(vim.lsp.util.parse_snippet(word))
      end
    else
      word = str.trim(self:get_completion_item().label)
    end
    return str.oneline(word)
  end)
end

---Get overwrite information
---@return integer, integer
entry.get_overwrite = function(self)
  return self.cache:ensure({ 'get_overwrite', self.resolved_completion_item and 1 or 0 }, function()
    if misc.safe(self:get_completion_item().textEdit) then
      local r = misc.safe(self:get_completion_item().textEdit.insert) or misc.safe(self:get_completion_item().textEdit.range)
      if r then
        local s = misc.to_vimindex(self.context.cursor_line, r.start.character)
        local e = misc.to_vimindex(self.context.cursor_line, r['end'].character)
        local before = self.context.cursor.col - s
        local after = e - self.context.cursor.col
        return { before, after }
      end
    end
    return { 0, 0 }
  end)
end

---Create filter text
---@return string
entry.get_filter_text = function(self)
  return self.cache:ensure({ 'get_filter_text', self.resolved_completion_item and 1 or 0 }, function()
    local word
    if misc.safe(self:get_completion_item().filterText) then
      word = self:get_completion_item().filterText
    else
      word = str.trim(self:get_completion_item().label)
    end
    return word
  end)
end

---Get LSP's insert text
---@return string
entry.get_insert_text = function(self)
  return self.cache:ensure({ 'get_insert_text', self.resolved_completion_item and 1 or 0 }, function()
    local word
    if misc.safe(self:get_completion_item().textEdit) then
      word = str.trim(self:get_completion_item().textEdit.newText)
      if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
        word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
      end
    elseif misc.safe(self:get_completion_item().insertText) then
      word = str.trim(self:get_completion_item().insertText)
      if self:get_completion_item().insertTextFormat == types.lsp.InsertTextFormat.Snippet then
        word = str.remove_suffix(str.remove_suffix(word, '$0'), '${0}')
      end
    else
      word = str.trim(self:get_completion_item().label)
    end
    return word
  end)
end

---Return the item is deprecated or not.
---@return boolean
entry.is_deprecated = function(self)
  return self:get_completion_item().deprecated or vim.tbl_contains(self:get_completion_item().tags or {}, types.lsp.CompletionItemTag.Deprecated)
end

---Return view information.
---@param suggest_offset integer
---@param entries_buf integer The buffer this entry will be rendered into.
---@return { abbr: { text: string, bytes: integer, width: integer, hl_group: string }, kind: { text: string, bytes: integer, width: integer, hl_group: string }, menu: { text: string, bytes: integer, width: integer, hl_group: string } }
entry.get_view = function(self, suggest_offset, entries_buf)
  local item = self:get_vim_item(suggest_offset)
  return self.cache:ensure({ 'get_view', self.resolved_completion_item and 1 or 0, entries_buf }, function()
    local view = {}
    -- The result of vim.fn.strdisplaywidth depends on which buffer it was
    -- called in because it reads the values of the option 'tabstop' when
    -- rendering <Tab> characters.
    vim.api.nvim_buf_call(entries_buf, function()
      view.abbr = {}
      view.abbr.text = item.abbr or ''
      view.abbr.bytes = #view.abbr.text
      view.abbr.width = vim.fn.strdisplaywidth(view.abbr.text)
      view.abbr.hl_group = item.abbr_hl_group or (self:is_deprecated() and 'CmpItemAbbrDeprecated' or 'CmpItemAbbr')
      view.kind = {}
      view.kind.text = item.kind or ''
      view.kind.bytes = #view.kind.text
      view.kind.width = vim.fn.strdisplaywidth(view.kind.text)
      view.kind.hl_group = item.kind_hl_group or ('CmpItemKind' .. (types.lsp.CompletionItemKind[self:get_kind()] or ''))
      view.menu = {}
      view.menu.text = item.menu or ''
      view.menu.bytes = #view.menu.text
      view.menu.width = vim.fn.strdisplaywidth(view.menu.text)
      view.menu.hl_group = item.menu_hl_group or 'CmpItemMenu'
      view.dup = item.dup
    end)
    return view
  end)
end

---Make vim.CompletedItem
---@param suggest_offset integer
---@return vim.CompletedItem
entry.get_vim_item = function(self, suggest_offset)
  return self.cache:ensure({ 'get_vim_item', suggest_offset, self.resolved_completion_item and 1 or 0 }, function()
    local completion_item = self:get_completion_item()
    local word = self:get_word()
    local abbr = str.oneline(completion_item.label)

    -- ~ indicator
    local is_snippet = false
    if #(misc.safe(completion_item.additionalTextEdits) or {}) > 0 then
      is_snippet = true
    elseif completion_item.insertTextFormat == types.lsp.InsertTextFormat.Snippet then
      is_snippet = self:get_insert_text() ~= word
    elseif completion_item.kind == types.lsp.CompletionItemKind.Snippet then
      is_snippet = true
    end
    if is_snippet then
      abbr = abbr .. '~'
    end

    -- append delta text
    if suggest_offset < self:get_offset() then
      word = string.sub(self.context.cursor_before_line, suggest_offset, self:get_offset() - 1) .. word
    end

    -- labelDetails.
    local menu = nil
    if misc.safe(completion_item.labelDetails) then
      menu = ''
      if misc.safe(completion_item.labelDetails.detail) then
        menu = menu .. completion_item.labelDetails.detail
      end
      if misc.safe(completion_item.labelDetails.description) then
        menu = menu .. completion_item.labelDetails.description
      end
    end

    -- remove duplicated string.
    if self:get_offset() ~= self.context.cursor.col then
      for i = 1, #word - 1 do
        if str.has_prefix(self.context.cursor_after_line, string.sub(word, i, #word)) then
          word = string.sub(word, 1, i - 1)
          break
        end
      end
    end

    local cmp_opts = self:get_completion_item().cmp or {}

    local vim_item = {
      word = word,
      abbr = abbr,
      kind = cmp_opts.kind_text or types.lsp.CompletionItemKind[self:get_kind()] or types.lsp.CompletionItemKind[1],
      kind_hl_group = cmp_opts.kind_hl_group,
      menu = menu,
      dup = self:get_completion_item().dup or 1,
    }
    if config.get().formatting.format then
      vim_item = config.get().formatting.format(self, vim_item)
    end
    vim_item.word = str.oneline(vim_item.word or '')
    vim_item.abbr = str.oneline(vim_item.abbr or '')
    vim_item.kind = str.oneline(vim_item.kind or '')
    vim_item.menu = str.oneline(vim_item.menu or '')
    vim_item.equal = 1
    vim_item.empty = 1

    return vim_item
  end)
end

---Get commit characters
---@return string[]
entry.get_commit_characters = function(self)
  return misc.safe(self:get_completion_item().commitCharacters) or {}
end

---Return insert range
---@return lsp.Range|nil
entry.get_insert_range = function(self)
  local insert_range
  if misc.safe(self:get_completion_item().textEdit) then
    if misc.safe(self:get_completion_item().textEdit.insert) then
      insert_range = self:get_completion_item().textEdit.insert
    else
      insert_range = self:get_completion_item().textEdit.range
    end
  else
    insert_range = {
      start = {
        line = self.context.cursor.row - 1,
        character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_insert_range.start.character),
      },
      ['end'] = self.source_insert_range['end'],
    }
  end
  return insert_range
end

---Return replace range
---@return lsp.Range|nil
entry.get_replace_range = function(self)
  return self.cache:ensure({ 'get_replace_range', self.resolved_completion_item and 1 or 0 }, function()
    local replace_range
    if misc.safe(self:get_completion_item().textEdit) and misc.safe(self:get_completion_item().textEdit.replace) then
      replace_range = self:get_completion_item().textEdit.replace
    else
      replace_range = {
        start = {
          line = self.source_replace_range.start.line,
          character = math.min(misc.to_utfindex(self.context.cursor_line, self:get_offset()), self.source_replace_range.start.character),
        },
        ['end'] = self.source_replace_range['end'],
      }
    end
    return replace_range
  end)
end

---Match line.
---@param input string
---@param matching_config cmp.MatchingConfig
---@return { score: integer, matches: table[] }
entry.match = function(self, input, matching_config)
  return self.match_cache:ensure({
    input,
    self.resolved_completion_item and 1 or 0,
    matching_config.disallow_fuzzy_matching and 1 or 0,
    matching_config.disallow_partial_matching and 1 or 0,
    matching_config.disallow_prefix_unmatching and 1 or 0,
  }, function()
    local option = {
      disallow_fuzzy_matching = matching_config.disallow_fuzzy_matching,
      disallow_partial_matching = matching_config.disallow_partial_matching,
      disallow_prefix_unmatching = matching_config.disallow_prefix_unmatching,
      synonyms = {
        self:get_word(),
        self:get_completion_item().label,
      },
    }

    local score, matches, _
    score, matches = matcher.match(input, self:get_filter_text(), option)

    -- Support the language server that doesn't respect VSCode's behaviors.
    if score == 0 then
      if misc.safe(self:get_completion_item().textEdit) and not misc.empty(self:get_completion_item().textEdit.newText) then
        local diff = self.source_offset - self:get_offset()
        if diff > 0 then
          local prefix = string.sub(self.context.cursor_line, self:get_offset(), self:get_offset() + diff)
          local accept = nil
          accept = accept or string.match(prefix, '^[^%a]+$')
          accept = accept or string.find(self:get_completion_item().textEdit.newText, prefix, 1, true)
          if accept then
            score, matches = matcher.match(input, prefix .. self:get_filter_text(), option)
          end
        end
      end
    end

    if self:get_filter_text() ~= self:get_completion_item().label then
      _, matches = matcher.match(input, self:get_completion_item().label, { synonyms = { self:get_word() } })
    end

    return { score = score, matches = matches }
  end)
end

---Get resolved completion item if possible.
---@return lsp.CompletionItem
entry.get_completion_item = function(self)
  return self.cache:ensure({ 'get_completion_item', self.resolved_completion_item and 1 or 0 }, function()
    if self.resolved_completion_item then
      local completion_item = misc.copy(self.completion_item)
      for k, v in pairs(self.resolved_completion_item) do
        completion_item[k] = v or completion_item[k]
      end
      return completion_item
    end
    return self.completion_item
  end)
end

---Create documentation
---@return string
entry.get_documentation = function(self)
  local item = self:get_completion_item()

  local documents = {}

  -- detail
  if misc.safe(item.detail) and item.detail ~= '' then
    local ft = self.context.filetype
    local dot_index = string.find(ft, '%.')
    if dot_index ~= nil then
      ft = string.sub(ft, 0, dot_index - 1)
    end
    table.insert(documents, {
      kind = types.lsp.MarkupKind.Markdown,
      value = ('```%s\n%s\n```'):format(ft, str.trim(item.detail)),
    })
  end

  local documentation = item.documentation
  if type(documentation) == 'string' and documentation ~= '' then
    local value = str.trim(documentation)
    if value ~= '' then
      table.insert(documents, {
        kind = types.lsp.MarkupKind.PlainText,
        value = value,
      })
    end
  elseif type(documentation) == 'table' and not misc.empty(documentation.value) then
    local value = str.trim(documentation.value)
    if value ~= '' then
      table.insert(documents, {
        kind = documentation.kind,
        value = value,
      })
    end
  end

  return vim.lsp.util.convert_input_to_markdown_lines(documents)
end

---Get completion item kind
---@return lsp.CompletionItemKind
entry.get_kind = function(self)
  return misc.safe(self:get_completion_item().kind) or types.lsp.CompletionItemKind.Text
end

---Execute completion item's command.
---@param callback fun()
entry.execute = function(self, callback)
  self.source:execute(self:get_completion_item(), callback)
end

---Resolve completion item.
---@param callback fun()
entry.resolve = function(self, callback)
  if self.resolved_completion_item then
    return callback()
  end
  table.insert(self.resolved_callbacks, callback)

  if not self.resolving then
    self.resolving = true
    self.source:resolve(self.completion_item, function(completion_item)
      self.resolved_completion_item = misc.safe(completion_item) or self.completion_item
      for _, c in ipairs(self.resolved_callbacks) do
        c()
      end
    end)
  end
end

return entry