~vigoux/complementree.nvim

66e7d63ad2e11c2b48dcd48b903c729cf1993f69 — Thomas Vigouroux 1 year, 23 days ago fdb1f4d
feat: remove teal and add snippy support
16 files changed, 640 insertions(+), 1652 deletions(-)

M lua/complementree/combinators.lua
M lua/complementree/comparators.lua
M lua/complementree/defaults.lua
M lua/complementree/filters.lua
M lua/complementree/init.lua
M lua/complementree/options.lua
M lua/complementree/sources.lua
M lua/complementree/utils.lua
D teal/complementree/combinators.tl
D teal/complementree/comparators.tl
D teal/complementree/defaults.tl
D teal/complementree/filters.tl
D teal/complementree/init.tl
D teal/complementree/options.tl
D teal/complementree/sources.tl
D teal/complementree/utils.tl
M lua/complementree/combinators.lua => lua/complementree/combinators.lua +57 -57
@@ 1,87 1,87 @@
local M = {}

local function complete(col, matches)
   if matches and #matches > 0 then
      vim.fn.complete(col, matches)
      return true
   else
      return false
   end
  if matches and #matches > 0 then
    vim.fn.complete(col, matches)
    return true
  else
    return false
  end
end

function M.combine(...)
   local funcs = { ... }
   return function(ltc, lnum)
      local matches = {}
      local coherent_p
      for _, f in ipairs(funcs) do
         local m, p = f(ltc, lnum)
         if not coherent_p then
            coherent_p = p
         end
  local funcs = { ... }
  return function(ltc, lnum)
    local matches = {}
    local coherent_p
    for _, f in ipairs(funcs) do
      local m, p = f(ltc, lnum)
      if not coherent_p then
        coherent_p = p
      end

         if coherent_p == p then
            vim.list_extend(matches, m)
         end
      if coherent_p == p then
        vim.list_extend(matches, m)
      end
      return matches, coherent_p
   end
    end
    return matches, coherent_p
  end
end

function M.optional(mandat, opt)
   return function(ltc, lnum)
      local matches, prefix = mandat(ltc, lnum)
      if #matches > 0 then
         local m, p = opt(ltc, lnum)
         if p == prefix then
            vim.list_extend(matches, m)
         end
         return matches, prefix
      else
         return {}, ''
  return function(ltc, lnum)
    local matches, prefix = mandat(ltc, lnum)
    if #matches > 0 then
      local m, p = opt(ltc, lnum)
      if p == prefix then
        vim.list_extend(matches, m)
      end
   end
      return matches, prefix
    else
      return {}, ''
    end
  end
end


function M.non_empty_prefix(func)
   return function(ltc, lnum)
      local compl, prefix = func(ltc, lnum)
      if #prefix > 1 then
         return complete(#ltc - #prefix + 1, compl)
      else
         return false
      end
   end
  return function(ltc, lnum)
    local compl, prefix = func(ltc, lnum)
    if #prefix > 1 then
      return complete(#ltc - #prefix + 1, compl)
    else
      return false
    end
  end
end

function M.wrap(func)
   return function(ltc, lnum)
      local compl, prefix = func(ltc, lnum)
      return complete(#ltc - #prefix + 1, compl)
   end
  return function(ltc, lnum)
    local compl, prefix = func(ltc, lnum)
    return complete(#ltc - #prefix + 1, compl)
  end
end

function M.pipeline(source, ...)
   local current = source
   for _, func in ipairs({ ... }) do
      current = func(current)
   end
  local current = source
  for _, func in ipairs({ ... }) do
    current = func(current)
  end

   return M.wrap(current)
  return M.wrap(current)
end

function M.chain(...)
   local funcs = { ... }
   return function(ltc, lnum)
      for _, f in ipairs(funcs) do
         local c, pref = f(ltc, lnum)
         if #c > 0 then
            return c, pref
         end
  local funcs = { ... }
  return function(ltc, lnum)
    for _, f in ipairs(funcs) do
      local c, pref = f(ltc, lnum)
      if #c > 0 then
        return c, pref
      end
      return {}, ''
   end
    end
    return {}, ''
  end
end

return M

M lua/complementree/comparators.lua => lua/complementree/comparators.lua +37 -37
@@ 10,56 10,56 @@ local Comparators = {}


local function mk_comparator(func)
   return function(msource)
      return function(ltc, lnum)
         local orig, prefix = msource(ltc, lnum)
         local cmp_cache = {}
         table.sort(orig, function(a, b)
            local key = { a, b }
  return function(msource)
    return function(ltc, lnum)
      local orig, prefix = msource(ltc, lnum)
      local cmp_cache = {}
      table.sort(orig, function(a, b)
        local key = { a, b }

            if not cmp_cache[key] then
               cmp_cache[key] = func(utils.cword(a), utils.cword(b))
            end
        if not cmp_cache[key] then
          cmp_cache[key] = func(utils.cword(a), utils.cword(b))
        end

            return cmp_cache[key]
         end)
         return orig, prefix
      end
   end
        return cmp_cache[key]
      end)
      return orig, prefix
    end
  end
end

Comparators.alphabetic = mk_comparator(function(a, b)
   return a < b
  return a < b
end)

Comparators.length = mk_comparator(function(a, b)
   return #a < #b
  return #a < #b
end)

local ok_fzy, fzy = pcall(require, 'fzy')
if ok_fzy then
   function Comparators.fzy(msource)
      return function(ltc, lnum)
         local orig, prefix = msource(ltc, lnum)
         local scores = {}
         local matching = {}
         if prefix ~= "" then
            for _, a in ipairs(orig) do
               local s, _ = fzy.match(prefix, utils.cword(a))
               if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
                  scores[a] = s
                  table.insert(matching, a)
               end
            end
            table.sort(matching, function(a, b)
               return scores[a] > scores[b]
            end)
            return matching, prefix
         else
            return orig, prefix
         end
  function Comparators.fzy(msource)
    return function(ltc, lnum)
      local orig, prefix = msource(ltc, lnum)
      local scores = {}
      local matching = {}
      if prefix ~= "" then
        for _, a in ipairs(orig) do
          local s, _ = fzy.match(prefix, utils.cword(a))
          if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
            scores[a] = s
            table.insert(matching, a)
          end
        end
        table.sort(matching, function(a, b)
          return scores[a] > scores[b]
        end)
        return matching, prefix
      else
        return orig, prefix
      end
   end
    end
  end
end

return Comparators

M lua/complementree/defaults.lua => lua/complementree/defaults.lua +7 -13
@@ 1,13 1,5 @@
local Defaults = {}









local comb = require('complementree.combinators')
local sources = require('complementree.sources')
local filters = require('complementree.filters')


@@ 15,17 7,19 @@ local comp = require('complementree.comparators')
local utils = require('complementree.utils')

function Defaults.ins_completion(mode)
   return function()
      utils.feed(string.format('<C-X><%s>', mode))
      return vim.fn.pumvisible() == 1
   end
  return function()
    utils.feed(string.format('<C-X><%s>', mode))
    return vim.fn.pumvisible() == 1
  end
end

function Defaults.dummy(_, _)

end

Defaults.luasnip = comb.pipeline(sources.luasnip_matches({}), filters.prefix, comp.alphabetic)
if sources.luasnip_matches then
  Defaults.luasnip = comb.pipeline(sources.luasnip_matches({}), filters.prefix, comp.alphabetic)
end

Defaults.lsp = comb.pipeline(sources.lsp_matches({}), filters.prefix, comp.alphabetic)


M lua/complementree/filters.lua => lua/complementree/filters.lua +20 -20
@@ 11,39 11,39 @@ local Filters = {}


local function mk_filter(func)
   return function(msource)
      return function(ltc, lnum)
         local orig, prefix = msource(ltc, lnum)
         local filtered = {}
         for i, v in ipairs(orig) do
            if func(i, v, prefix) then
               table.insert(filtered, v)
            end
         end
         return filtered, prefix
  return function(msource)
    return function(ltc, lnum)
      local orig, prefix = msource(ltc, lnum)
      local filtered = {}
      for i, v in ipairs(orig) do
        if func(i, v, prefix) then
          table.insert(filtered, v)
        end
      end
   end
      return filtered, prefix
    end
  end
end

function Filters.amount(n)
   return mk_filter(function(i, _, _)
      return i <= n
   end)
  return mk_filter(function(i, _, _)
    return i <= n
  end)
end

Filters.prefix = mk_filter(function(_, v, prefix)
   return vim.startswith(utils.cword(v), prefix)
  return vim.startswith(utils.cword(v), prefix)
end)

Filters.strict_prefix = mk_filter(function(_, v, prefix)
   local w = utils.cword(v)
   return vim.startswith(w, prefix) and #w ~= #prefix
  local w = utils.cword(v)
  return vim.startswith(w, prefix) and #w ~= #prefix
end)

Filters.substr = mk_filter(function(_, v, prefix)
   local w = utils.cword(v)
   local start = w:find(prefix, 1, true)
   return start ~= nil
  local w = utils.cword(v)
  local start = w:find(prefix, 1, true)
  return start ~= nil
end)

return Filters

M lua/complementree/init.lua => lua/complementree/init.lua +127 -121
@@ 5,178 5,184 @@ local sources = require('complementree.sources')
local tsutils = require('nvim-treesitter.ts_utils')
local M = {}






---@class CompleteItem
---@field word string The text that will be inserted
---@field abbr string? Abbreviation of word
---@field menu string? Extra text for the popup menu
---@field info string? More information, can be displayed in a preview window
---@field kiend string? Single letter indicating the type of completion
---@field icase boolean Ignore case when comparing for equality of items
---@field equal boolean Disable filtering of this item
---@field dup boolean Add this item even if item already present
---@field empty boolean Add even if it is an empty string
---@field user_data any? Custom data

local user_config = {
   default = defaults.dummy,
   vim = defaults.ins_completion('C-V'),
  default = defaults.dummy,
  vim = defaults.ins_completion('C-V'),
}

function M.setup(config)
   if not config.default then
      error('This config does not have a default key.')
   end
  if not config.default then
    error('This config does not have a default key.')
  end

   local def = config.default
   if not (type(def) == "function") then
      error('Invalid default completion')
   end
  local def = config.default
  if not (type(def) == "function") then
    error('Invalid default completion')
  end

   user_config = config
  user_config = config
end

function M.print_config()
   print(vim.inspect(user_config))
  print(vim.inspect(user_config))
end

local function correct_position(line_to_cursor, linenr)
   local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
   return linenr - 1, col - 1
  local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
  return linenr - 1, col - 1
end

local function node_type_at_position(l, c)
   local root = tsutils.get_root_for_position(l, c)
   if not root then
      return
   end
  local root = tsutils.get_root_for_position(l, c)
  if not root then
    return
  end

   local node = root:named_descendant_for_range(l, c, l, c)
   if not node then
      return
   end
  local node = root:named_descendant_for_range(l, c, l, c)
  if not node then
    return
  end

   return node:type()
  return node:type()
end

local function get_completion(ft, line_to_cursor, lnum, _col)
   local l, c = correct_position(line_to_cursor, lnum)
   local ft_completion = user_config[ft] or user_config.default
   if ft_completion then
      if type(ft_completion) == "table" then
         local root = tsutils.get_root_for_position(l, c)

         for q, sub in pairs(ft_completion) do

            if vim.startswith(q, '(') then
               local query = vim.treesitter.query.parse(ft, q)
               for id, node in query:iter_captures(root, 0, l, l + 1) do
                  local cname = query.captures[id]
                  if tsutils.is_in_node_range(node, l, c) then
                     if type(sub) == "table" and sub[cname] then
                        return sub[cname]
                     elseif type(sub) == "function" then
                        return sub
                     else

                        break
                     end
                  end
               end
            end
         end

         local t = node_type_at_position(l, c)
         if not t then
            local def = ft_completion.default
            if type(def) == "function" then
               return def
            else
               error('Invalid default completion source.')
  local l, c = correct_position(line_to_cursor, lnum)
  local ft_completion = user_config[ft] or user_config.default
  if ft_completion then
    if type(ft_completion) == "table" then
      local root = tsutils.get_root_for_position(l, c)

      for q, sub in pairs(ft_completion) do

        if vim.startswith(q, '(') then
          local query = vim.treesitter.query.parse(ft, q)
          for id, node in query:iter_captures(root, 0, l, l + 1) do
            local cname = query.captures[id]
            if tsutils.is_in_node_range(node, l, c) then
              if type(sub) == "table" and sub[cname] then
                return sub[cname]
              elseif type(sub) == "function" then
                return sub
              else

                break
              end
            end
         end
         local sub_completion = ft_completion[t] or ft_completion.default
         if sub_completion and type(sub_completion) == "function" then
            return sub_completion
         end
      elseif type(ft_completion) == 'function' then
         return ft_completion
          end
        end
      end

      local t = node_type_at_position(l, c)
      if not t then
        local def = ft_completion.default
        if type(def) == "function" then
          return def
        else
          error('Invalid default completion source.')
        end
      end
   end
      local sub_completion = ft_completion[t] or ft_completion.default
      if sub_completion and type(sub_completion) == "function" then
        return sub_completion
      end
    elseif type(ft_completion) == 'function' then
      return ft_completion
    end
  end
end

function M.separate_prefix(line, cursor)
   local line_to_cursor = line:sub(1, cursor)
   local pref_start = line_to_cursor:find('%S*$')
   local prefix = line_to_cursor:sub(pref_start)
  local line_to_cursor = line:sub(1, cursor)
  local pref_start = line_to_cursor:find('%S*$')
  local prefix = line_to_cursor:sub(pref_start)

   return line_to_cursor, pref_start, prefix
  return line_to_cursor, pref_start, prefix
end

function M.complete()

   if vim.fn.pumvisible() == 0 then
      sources.invalidate_cache()
   end
   if not vim.fn.mode():find('i') then
      return false
   end
  if vim.fn.pumvisible() == 0 then
    sources.invalidate_cache()
  end
  if not vim.fn.mode():find('i') then
    return false
  end

   local bufnr = api.nvim_get_current_buf()
   local ft = api.nvim_buf_get_option(bufnr, 'filetype')
  local bufnr = api.nvim_get_current_buf()
  local ft = api.nvim_buf_get_option(bufnr, 'filetype')

   local line = api.nvim_get_current_line()
   local cursor = api.nvim_win_get_cursor(0)
   local lnum = cursor[1]
   local cursor_pos = cursor[2]
   local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)
  local line = api.nvim_get_current_line()
  local cursor = api.nvim_win_get_cursor(0)
  local lnum = cursor[1]
  local cursor_pos = cursor[2]
  local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)




   local func = get_completion(ft, line_to_cursor, lnum, pref_start)
   if not (func == nil) then
      if func(line_to_cursor, lnum) then
         return true
      end
   end
   return false
  local func = get_completion(ft, line_to_cursor, lnum, pref_start)
  if not (func == nil) then
    if func(line_to_cursor, lnum) then
      return true
    end
  end
  return false
end

function M._CompleteDone()
   local completed_item = api.nvim_get_vvar('completed_item')
   if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
      return
   end
   local func = sources.complete_done_cbs[completed_item.user_data.source]
   if func then
  local completed_item = api.nvim_get_vvar('completed_item')
  if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
    return
  end
  local func = sources.complete_done_cbs[completed_item.user_data.source]
  if func then






      local previous_opt = api.nvim_get_option('eventignore')
      local newval = previous_opt
      if #newval == 0 then
         newval = 'InsertLeave'
      else
         newval = 'InsertLeave,' .. newval
      end
      api.nvim_set_option('eventignore', newval)
      func(completed_item)
      vim.schedule(function()
         api.nvim_set_option('eventignore', previous_opt)
      end)
   end
    local previous_opt = api.nvim_get_option('eventignore')
    local newval = previous_opt
    if #newval == 0 then
      newval = 'InsertLeave'
    else
      newval = 'InsertLeave,' .. newval
    end
    api.nvim_set_option('eventignore', newval)
    func(completed_item)
    vim.schedule(function()
      api.nvim_set_option('eventignore', previous_opt)
    end)
  end
end

function M._InsertCharPre()
   if vim.fn.pumvisible() == 1 then
      local char = api.nvim_get_vvar('char')
      if char:find('%s') then
  if vim.fn.pumvisible() == 1 then
    local char = api.nvim_get_vvar('char')
    if char:find('%s') then

         utils.feed('<C-Y>')
      else
      utils.feed('<C-Y>')
    else

         vim.schedule(function()
            M.complete()
         end)
      end
   end
      vim.schedule(function()
        M.complete()
      end)
    end
  end
end

return M

M lua/complementree/options.lua => lua/complementree/options.lua +2 -2
@@ 1,8 1,8 @@
local M = {}

function M.get(defaults, user)
   vim.tbl_deep_extend('force', defaults, user)
   return defaults
  vim.tbl_deep_extend('force', defaults, user)
  return defaults
end

return M

M lua/complementree/sources.lua => lua/complementree/sources.lua +366 -416
@@ 1,40 1,5 @@


























local Sources = {}










local utils = require('complementree.utils')
local options = require('complementree.options')
local api = vim.api


@@ 43,453 8,438 @@ local lsp = vim.lsp
local cache = {}

function Sources.invalidate_cache()
   cache = {}
  cache = {}
end

Sources.complete_done_cbs = {
}

local function cached(kind, func)
   return function(ltc, lnum)
      local m
      local p
      if not cache[kind] then
         m, p = func(ltc, lnum)
         cache[kind] = { m, p }
      else
         m = cache[kind][1]
         p = cache[kind][2]
  return function(ltc, lnum)
    local m
    local p
    if not cache[kind] then
      m, p = func(ltc, lnum)
      cache[kind] = { m, p }
    else
      m = cache[kind][1]
      p = cache[kind][2]

      local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
      if pref_start >= 1 then
        p = ltc:sub(pref_start)
      end
    end
    local new = {}
    for _, v in ipairs(m) do
      table.insert(new, v)
    end
    return new, p
  end
end

local apply_snippet = function(...)
end

function Sources.lsp_matches(opts)
  opts = options.get({}, opts)
  return cached('lsp', function(line_to_cursor, lnum)

    local function adjust_start_col(line_number, line, items, encoding)
      local min_start_char = nil

         local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
         if pref_start >= 1 then
            p = ltc:sub(pref_start)
         end
      for _, item in ipairs(items) do
        if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
          if min_start_char and min_start_char ~= item.textEdit.range.start.character then
            return nil
          end
          min_start_char = item.textEdit.range.start.character
        end
      end
      if min_start_char then
        if encoding == 'utf-8' then
          return min_start_char + 1
        else
          return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
        end
      else
        return nil
      end
      local new = {}
      for _, v in ipairs(m) do
         table.insert(new, v)
    end

    local params = lsp.util.make_position_params()
    local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
    if err then
      api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
      return {}, ''
    end
    if not result_all then
      return {}, ''
    end

    local matches = {}
    local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
    for client_id, result in pairs(result_all) do
      local client = lsp.get_client_by_id(client_id)
      local items = lsp.util.extract_completion_items(result.result) or {}

      local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
      if tmp_col and tmp_col < start_col then
        start_col = tmp_col
      end
      return new, p
   end
end

      for _, item in ipairs(items) do
        local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
        local word = nil
        if kind == 'Snippet' then
          word = item.label
        elseif item.insertTextFormat == 2 then
          if item.textEdit then
            word = item.insertText or item.textEdit.newText
          elseif item.insertText then
            if #item.label < #item.insertText then
              word = item.label
            else
              word = item.insertText
            end
          else
            word = item.label
          end
        else
          word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
        end
        local ud = {
          source = 'lsp',
          extra = { client_id = client_id, item = item },
        }
        table.insert(matches, {
          word = word,
          abbr = item.label,
          kind = kind,
          menu = item.detail or '',
          icase = 1,
          dup = 1,
          empty = 1,
          equal = 1,
          user_data = ud,
        })
      end
    end
    local prefix = line_to_cursor:sub(start_col)
    return matches, prefix
  end)
end

local function lsp_completedone(completed_item)
  -- TODO: over complicated
  local cursor = api.nvim_win_get_cursor(0)
  local col = cursor[2]
  local lnum = cursor[1] - 1
  local bufnr = api.nvim_get_current_buf()

  local extra = completed_item.user_data.extra
  local item = extra.item

  local client = lsp.get_client_by_id(extra.client_id)
  if not client then
    error(string.format("Could not find client %d", extra.client_id))
  end

  local expand_snippet = item.insertTextFormat == 2
  local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
  local offset_encoding = client and client.offset_encoding or 'utf-16'

  local tidy = function() end
  local suffix = nil

  if expand_snippet then
    local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
    tidy = function()
      local start_char = col - #completed_item.word
      local l = line
      api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
    end
    suffix = line:sub(col + 1)
  end

  if item.additionalTextEdits then
    tidy()
    tidy = function() end
    lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
  elseif resolveEdits and type(item) == 'table' then
    local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
    assert(not v.err, vim.inspect(v.err))
    local res = v.result
    if res.additionalTextEdits then
      tidy()
      tidy = function() end
      lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
    end
  end
  if expand_snippet then
    tidy()
    apply_snippet(item, suffix, lnum)
  end
end

Sources.complete_done_cbs.lsp = lsp_completedone

-- Conditional loading of the snippets handling / sources
local lsnip_present, luasnip = pcall(require, "luasnip")
local snippy_present, snippy = pcall(require, "snippy")

function Sources.luasnip_matches(opts)
   opts = options.get(opts, {
if lsnip_present then
  function Sources.luasnip_matches(opts)
    opts = options.get(opts, {
      exclude_defaults = false,
      filetype = nil,
   })
    })

   local lsnip_present, luasnip = pcall(require, "luasnip")
   if not lsnip_present then
      error("LuaSnip is not installed")
   end

   local function add_snippet(items, s)
    local function add_snippet(items, s)
      table.insert(items, {
         word = s.trigger,
         kind = 'S',
         menu = table.concat(s.description or {}),
         icase = 1,
         dup = 1,
         empty = 1,
         equal = 1,
         user_data = { source = 'luasnip' },
        word = s.trigger,
        kind = 'S',
        menu = table.concat(s.description or {}),
        icase = 1,
        dup = 1,
        empty = 1,
        equal = 1,
        user_data = { source = 'luasnip' },
      })
   end
    end

   return cached('luasnip', function(line_to_cursor, _)
    return cached('luasnip', function(line_to_cursor, _)
      local prefix = utils.prefix.lua_regex('%w*$', line_to_cursor)
      local items = {}










      for ftname, snips in pairs(luasnip.available()) do
         if not (ftname == 'all' and opts.exclude_defaults) then
            vim.tbl_map(function(s)
               add_snippet(items, s)
            end, snips)
         end
        if not (ftname == 'all' and opts.exclude_defaults) then
          vim.tbl_map(function(s)
            add_snippet(items, s)
          end, snips)
        end
      end

      return items, prefix
   end)
end



    end)
  end




function Sources.lsp_matches(opts)
   opts = options.get({}, opts)
   return cached('lsp', function(line_to_cursor, lnum)


      local function adjust_start_col(line_number, line, items, encoding)
         local min_start_char = nil

         for _, item in ipairs(items) do
            if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
               if min_start_char and min_start_char ~= item.textEdit.range.start.character then
                  return nil
               end
               min_start_char = item.textEdit.range.start.character
            end
         end
         if min_start_char then
            if encoding == 'utf-8' then
               return min_start_char + 1
            else
               return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
            end
         else
            return nil
         end
      end

      local params = lsp.util.make_position_params()
      local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
      if err then
         api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
         return {}, ''
      end
      if not result_all then
         return {}, ''
      end

      local matches = {}
      local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
      for client_id, result in pairs(result_all) do
         local client = lsp.get_client_by_id(client_id)
         local items = lsp.util.extract_completion_items(result.result) or {}

         local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
         if tmp_col and tmp_col < start_col then
            start_col = tmp_col
         end

         for _, item in ipairs(items) do
            local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
            local word = nil
            if kind == 'Snippet' then
               word = item.label
            elseif item.insertTextFormat == 2 then
               if item.textEdit then
                  word = item.insertText or item.textEdit.newText
               elseif item.insertText then
                  if #item.label < #item.insertText then
                     word = item.label
                  else
                     word = item.insertText
                  end
               else
                  word = item.label
               end
            else
               word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
            end
            local ud = {
               source = 'lsp',
               extra = { client_id = client_id, item = item },
            }
            table.insert(matches, {
               word = word,
               abbr = item.label,
               kind = kind,
               menu = item.detail or '',
               icase = 1,
               dup = 1,
               empty = 1,
               equal = 1,
               user_data = ud,
            })
         end
      end
      local prefix = line_to_cursor:sub(start_col)
      return matches, prefix
   end)
end

local function apply_snippet(item, suffix, lnum)
   local luasnip = require('luasnip')
   if item.textEdit then
  apply_snippet = function(item, suffix, lnum)
    local luasnip = require('luasnip')
    if item.textEdit then
      luasnip.lsp_expand(item.textEdit.newText .. suffix)
   elseif item.insertText then
    elseif item.insertText then
      luasnip.lsp_expand(item.insertText .. suffix)
   elseif item.label then
    elseif item.label then
      luasnip.lsp_expand(item.label .. suffix)
   end
   vim.schedule(function()
    end
    vim.schedule(function()
      local curline = api.nvim_get_current_line()
      if vim.endswith(curline, suffix) and not luasnip.get_active_snip() then
         local newcol = #curline - #suffix
         api.nvim_win_set_cursor(0, { lnum + 1, newcol })
        local newcol = #curline - #suffix
        api.nvim_win_set_cursor(0, { lnum + 1, newcol })
      end
   end)
    end)
  end

  Sources.complete_done_cbs.luasnip = function(_)
    if require('luasnip').expandable() then
      require('luasnip').expand()
    end
  end

elseif snippy_present then
  Sources.complete_done_cbs.lsp = function(completed_item)
    local lsp_item = completed_item.user_data.extra.item
    local snippet
    if lsp_item.textEdit then
      snippet = lsp_item.textEdit.newText
    elseif lsp_item.insertTextFormat == 2 then
      snippet = lsp_item.insertText
    end
    if snippet then
      snippy.expand_snippet(snippet, completed_item.word)
    end
  end
end

local ctags_extension = {
   default = {
      ['c'] = 'class',
      ['d'] = 'define',
      ['e'] = 'enumerator',
      ['f'] = 'function',
      ['F'] = 'file',
      ['g'] = 'enumeration',
      ['m'] = 'member',
      ['p'] = 'prototype',
      ['s'] = 'structure',
      ['t'] = 'typedef',
      ['u'] = 'union',
      ['v'] = 'variable',
   },
  default = {
    ['c'] = 'class',
    ['d'] = 'define',
    ['e'] = 'enumerator',
    ['f'] = 'function',
    ['F'] = 'file',
    ['g'] = 'enumeration',
    ['m'] = 'member',
    ['p'] = 'prototype',
    ['s'] = 'structure',
    ['t'] = 'typedef',
    ['u'] = 'union',
    ['v'] = 'variable',
  },
}

function Sources.ctags_matches(opts)
   opts = options.get({}, opts)
   return cached('ctags', function(line_to_cursor, _)
      local prefix = utils.prefix.vim_keyword(line_to_cursor)

      local filetype = api.nvim_buf_get_option(0, 'filetype')
      local extensions = ctags_extension[filetype] or ctags_extension.default
      local tags = vim.fn.taglist('.*')

      local items = {}
      for _, t in ipairs(tags) do
         local ud = { source = 'ctags' }
         items[#items + 1] = {
            word = t.name,
            kind = (t.kind and extensions[t.kind] or 'undefined'),
            icase = 1,
            dup = 0,
            equal = 1,
            empty = 1,
            user_data = ud,
         }
      end

      return items, prefix
   end)
  opts = options.get({}, opts)
  return cached('ctags', function(line_to_cursor, _)
    local prefix = utils.prefix.vim_keyword(line_to_cursor)

    local filetype = api.nvim_buf_get_option(0, 'filetype')
    local extensions = ctags_extension[filetype] or ctags_extension.default
    local tags = vim.fn.taglist('.*')

    local items = {}
    for _, t in ipairs(tags) do
      local ud = { source = 'ctags' }
      items[#items + 1] = {
        word = t.name,
        kind = (t.kind and extensions[t.kind] or 'undefined'),
        icase = 1,
        dup = 0,
        equal = 1,
        empty = 1,
        user_data = ud,
      }
    end

    return items, prefix
  end)
end



local os_name = string.lower(jit.os)
local is_linux = (os_name == 'linux' or os_name == 'osx' or os_name == 'bsd')
local os_sep = is_linux and '/' or '\\'
local os_path = '[' .. os_sep .. '%w+%-%.%_]*$'

function Sources.filepath_matches(opts)
   local relpath = utils.make_relative_path
   local config = options.get({
      show_hidden = false,
      ignore_directories = true,
      max_depth = math.huge,
      relative_paths = false,
      ignore_pattern = '',
      root_dirs = { '.' },
   }, opts)

   local function iter_files()
      local path_stack = vim.fn.reverse(config.root_dirs or { '.' })
      local iter_stack = {}
      for _, p in ipairs(path_stack) do
         table.insert(iter_stack, vim.loop.fs_scandir(p))
  local relpath = utils.make_relative_path
  local config = options.get({
    show_hidden = false,
    ignore_directories = true,
    max_depth = math.huge,
    relative_paths = false,
    ignore_pattern = '',
    root_dirs = { '.' },
  }, opts)

  local function iter_files()
    local path_stack = vim.fn.reverse(config.root_dirs or { '.' })
    local iter_stack = {}
    for _, p in ipairs(path_stack) do
      table.insert(iter_stack, vim.loop.fs_scandir(p))
    end

    if config.max_depth == 0 then
      return function()
        return nil
      end

      if config.max_depth == 0 then
         return function()
    end

    return function()
      local iter = iter_stack[#iter_stack]
      local path = path_stack[#path_stack]
      while true do
        local next_path, path_type = vim.loop.fs_scandir_next(iter)

        if not next_path then
          table.remove(iter_stack)
          table.remove(path_stack)
          if #iter_stack == 0 then
            return nil
         end
      end

      return function()
         local iter = iter_stack[#iter_stack]
         local path = path_stack[#path_stack]
         while true do
            local next_path, path_type = vim.loop.fs_scandir_next(iter)

            if not next_path then
               table.remove(iter_stack)
               table.remove(path_stack)
               if #iter_stack == 0 then
                  return nil
               end
               iter = iter_stack[#iter_stack]
               path = path_stack[#path_stack]
            elseif
(vim.startswith(next_path, '.') and not config.show_hidden) or
               (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil) then

               next_path = nil
               path_type = nil
            else
               local full_path = path .. os_sep .. next_path
               if path_type == 'directory' then
                  if #iter_stack < config.max_depth then
                     iter = vim.loop.fs_scandir(full_path)
                     path = full_path
                     table.insert(path_stack, full_path)
                     table.insert(iter_stack, iter)
                  end
                  if not config.ignore_directories then
                     return full_path
                  end
               else
                  return full_path
               end
          end
          iter = iter_stack[#iter_stack]
          path = path_stack[#path_stack]
        elseif
          (vim.startswith(next_path, '.') and not config.show_hidden) or
          (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil) then

          next_path = nil
          path_type = nil
        else
          local full_path = path .. os_sep .. next_path
          if path_type == 'directory' then
            if #iter_stack < config.max_depth then
              iter = vim.loop.fs_scandir(full_path)
              path = full_path
              table.insert(path_stack, full_path)
              table.insert(iter_stack, iter)
            end
         end
      end
   end

   return cached('filepath', function(line_to_cursor, _)
      local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)

      local cwd = vim.fn.getcwd()
      local fpath
      local matches = {}
      for path in iter_files() do
         fpath = config.relative_paths and relpath(path, cwd) or path

         matches[#matches + 1] = {
            word = fpath,
            abbr = fpath,
            kind = '[path]',
            icase = 1,
            dup = 1,
            empty = 1,
            equal = 1,
            user_data = { source = 'filepath' },
         }
            if not config.ignore_directories then
              return full_path
            end
          else
            return full_path
          end
        end
      end

      return matches, prefix
   end)
    end
  end

  return cached('filepath', function(line_to_cursor, _)
    local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)

    local cwd = vim.fn.getcwd()
    local fpath
    local matches = {}
    for path in iter_files() do
      fpath = config.relative_paths and relpath(path, cwd) or path

      matches[#matches + 1] = {
        word = fpath,
        abbr = fpath,
        kind = '[path]',
        icase = 1,
        dup = 1,
        empty = 1,
        equal = 1,
        user_data = { source = 'filepath' },
      }
    end

    return matches, prefix
  end)
end



local tslocals = require('nvim-treesitter.locals')

function Sources.treesitter_matches(opts)
   local _config = options.get({}, opts)
  local _config = options.get({}, opts)

   return cached('treesitter', function(line_to_cursor, _lnum)
      local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
      local defs = tslocals.get_definitions(0)
  return cached('treesitter', function(line_to_cursor, _lnum)
    local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
    local defs = tslocals.get_definitions(0)

      local items = {}
    local items = {}

      for _, def in ipairs(defs) do
    for _, def in ipairs(defs) do

         local node
         local kind
         for k, cap in pairs(def) do
            if k ~= 'associated' then
               node = cap.node
               kind = k
               break
            end
         end

         if node then
            items[#items + 1] = {
               word = vim.treesitter.get_node_text(node, 0),
               kind = kind,
               icase = 1,
               dup = 0,
               empty = 1,
               equal = 1,
               user_data = { source = 'treesitter' },
            }
         end
      local node
      local kind
      for k, cap in pairs(def) do
        if k ~= 'associated' then
          node = cap.node
          kind = k
          break
        end
      end

      return items, prefix
   end)
end



local function lsp_completedone(completed_item)
   local cursor = api.nvim_win_get_cursor(0)
   local col = cursor[2]
   local lnum = cursor[1] - 1
   local bufnr = api.nvim_get_current_buf()

   local extra = completed_item.user_data.extra
   local item = extra.item

   local client = lsp.get_client_by_id(extra.client_id)
   if not client then
      error(string.format("Could not find client %d", extra.client_id))
   end

   local expand_snippet = item.insertTextFormat == 2
   local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
   local offset_encoding = client and client.offset_encoding or 'utf-16'

   local tidy = function() end
   local suffix = nil

   if expand_snippet then
      local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
      tidy = function()

         local start_char = col - #completed_item.word
         local l = line
         api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
      end
      suffix = line:sub(col + 1)
   end

   if item.additionalTextEdits then
      tidy()
      lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
      if expand_snippet then
         apply_snippet(item, suffix, lnum)
      if node then
        items[#items + 1] = {
          word = vim.treesitter.get_node_text(node, 0),
          kind = kind,
          icase = 1,
          dup = 0,
          empty = 1,
          equal = 1,
          user_data = { source = 'treesitter' },
        }
      end
   elseif resolveEdits and type(item) == 'table' then
      local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
      assert(not v.err, vim.inspect(v.err))
      local res = v.result
      if res.additionalTextEdits then
         tidy()
         tidy = function() end
         lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
      end
      if expand_snippet then
         tidy()
         apply_snippet(item, suffix, lnum)
      end
   elseif expand_snippet then
      tidy()
      apply_snippet(item, suffix, lnum)
   end
end
    end

local function luasnip_completedone(_)
   if require('luasnip').expandable() then
      require('luasnip').expand()
   end
    return items, prefix
  end)
end

Sources.complete_done_cbs = {
   lsp = lsp_completedone,
   luasnip = luasnip_completedone,
}

return Sources

M lua/complementree/utils.lua => lua/complementree/utils.lua +24 -18
@@ 2,41 2,47 @@ local api = vim.api

local Prefix = {}


function Prefix.lua_regex(regex, line)
   local pref_start = line:find(regex)
   local prefix = line:sub(pref_start)
  local pref_start = line:find(regex)
  local prefix = line:sub(pref_start)

   return prefix
  return prefix
end

function Prefix.vim_keyword(line)
   local pref_start = vim.fn.match(line, '\\k*$') + 1
   local prefix = line:sub(pref_start)
  local pref_start = vim.fn.match(line, '\\k*$') + 1
  local prefix = line:sub(pref_start)

   return prefix
  return prefix
end

local Utils = {}



--- Feed a bunch of keys to nvim
---@param codes string
function Utils.feed(codes)
   api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
  api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
end

--- Gets the word to use for completion
---@param complete_item CompleteItem
---@return string
function Utils.cword(complete_item)
   return (complete_item.abbr or complete_item.word)
  return (complete_item.abbr or complete_item.word)
end

--- Makes the provided path relative to root
---@param path string Path to make relative
---@param root sring Root path
---@return string
function Utils.make_relative_path(path, root)
   if vim.startswith(path, root) then
      local baselen = #root
      if path:sub(0, baselen) == root then
         path = path:sub(baselen + 2)
      end
   end
   return path
  if vim.startswith(path, root) then
    local baselen = #root
    if path:sub(0, baselen) == root then
      path = path:sub(baselen + 2)
    end
  end
  return path
end

Utils.prefix = Prefix

D teal/complementree/combinators.tl => teal/complementree/combinators.tl +0 -87
@@ 1,87 0,0 @@
local M = {}

local function complete(col: integer, matches: {CompleteItem}): boolean
  if matches and #matches > 0 then
    vim.fn.complete(col, matches)
    return true
  else
    return false
  end
end

function M.combine(...: Source): Source
  local funcs = { ... }
  return function(ltc: string, lnum: integer): {CompleteItem}, string
    local matches: {CompleteItem} = {}
    local coherent_p: string
    for _, f in ipairs(funcs) do
      local m, p = f(ltc, lnum)
      if not coherent_p then
        coherent_p = p
      end

      if coherent_p == p then
        vim.list_extend(matches, m)
      end
    end
    return matches, coherent_p
  end
end

function M.optional(mandat: Source, opt: Source): Source
  return function(ltc: string, lnum: integer): {CompleteItem}, string
    local matches, prefix = mandat(ltc, lnum)
    if #matches > 0 then
      local m, p = opt(ltc, lnum)
      if p == prefix then
        vim.list_extend(matches, m)
      end
      return matches, prefix
    else
      return {}, ''
    end
  end
end

-- TODO(vigoux): do we even need this anymore ?
function M.non_empty_prefix(func: Source): Completor
  return function(ltc: string, lnum: integer): boolean
    local compl, prefix = func(ltc, lnum)
    if #prefix > 1 then
      return complete(#ltc - #prefix + 1, compl)
    else
      return false
    end
  end
end

function M.wrap(func: Source): Completor
  return function(ltc: string, lnum: integer): boolean
    local compl, prefix = func(ltc, lnum)
    return complete(#ltc - #prefix + 1, compl)
  end
end

function M.pipeline(source: Source, ...: Pipe): Completor
  local current = source
  for _, func in ipairs { ... } do
    current = func(current)
  end

  return M.wrap(current)
end

function M.chain(...: Source): Source
  local funcs = { ... }
  return function(ltc: string, lnum: integer): {CompleteItem}, string
    for _, f in ipairs(funcs) do
      local c, pref = f(ltc, lnum)
      if #c > 0 then
        return c, pref
      end
    end
    return {}, ''
  end
end

return M

D teal/complementree/comparators.tl => teal/complementree/comparators.tl +0 -65
@@ 1,65 0,0 @@
local utils = require 'complementree.utils'

local record Comparators
  alphabetic: Pipe
  length: Pipe
  fzy: Pipe
  ifzy: Pipe
end

local type Comparator = function(string, string): boolean

local function mk_comparator(func: Comparator): Pipe
  return function(msource: Source): Source
    return function(ltc: string, lnum: integer): {CompleteItem}, string
      local orig, prefix = msource(ltc, lnum)
      local cmp_cache = {}
      table.sort(orig, function(a: CompleteItem, b: CompleteItem): boolean
        local key = { a, b }

        if not cmp_cache[key] then
          cmp_cache[key] = func(utils.cword(a), utils.cword(b))
        end

        return cmp_cache[key]
      end)
      return orig, prefix
    end
  end
end

Comparators.alphabetic = mk_comparator(function(a: string, b: string): boolean
  return a < b
end)

Comparators.length = mk_comparator(function(a: string, b: string): boolean
  return #a < #b
end)

local ok_fzy, fzy = pcall(require, 'fzy')
if ok_fzy then
  function Comparators.fzy(msource: Source): Source
    return function (ltc: string, lnum: integer): {CompleteItem}, string
      local orig, prefix = msource(ltc, lnum)
      local scores: {CompleteItem:number} = {}
      local matching: {CompleteItem} = {}
      if prefix ~= "" then
        for _, a in ipairs(orig) do
          local s, _ = fzy.match(prefix, utils.cword(a))
          if math.abs(s or math.huge) ~= math.huge or prefix == utils.cword(a) then
            scores[a] = s
            table.insert(matching, a)
          end
        end
        table.sort(matching, function(a: CompleteItem, b: CompleteItem): boolean
          return scores[a] > scores[b]
        end)
        return matching, prefix
      else
        return orig, prefix
      end
    end
  end
end

return Comparators

D teal/complementree/defaults.tl => teal/complementree/defaults.tl +0 -38
@@ 1,38 0,0 @@
local record Defaults
  ins_completion: function(string): Completor
  dummy: Completor
  luasnip: Completor
  lsp: Completor
  ctags: Completor
  filepath: Completor
  treesitter: Completor
end

local comb = require 'complementree.combinators'
local sources = require 'complementree.sources'
local filters = require 'complementree.filters'
local comp = require 'complementree.comparators'
local utils = require 'complementree.utils'

function Defaults.ins_completion(mode: string): Completor
  return function(): boolean
    utils.feed(string.format('<C-X><%s>', mode))
    return vim.fn.pumvisible() == 1
  end
end

function Defaults.dummy(_: string, _: integer): boolean
  -- Does nothing
end

Defaults.luasnip = comb.pipeline(sources.luasnip_matches {}, filters.prefix, comp.alphabetic)

Defaults.lsp = comb.pipeline(sources.lsp_matches {}, filters.prefix, comp.alphabetic)

Defaults.ctags = comb.pipeline(sources.ctags_matches {}, filters.prefix, comp.alphabetic)

Defaults.filepath = comb.pipeline(sources.filepath_matches {}, filters.substr, comp.alphabetic)

Defaults.treesitter = comb.pipeline(sources.treesitter_matches {}, filters.substr, comp.alphabetic)

return Defaults

D teal/complementree/filters.tl => teal/complementree/filters.tl +0 -49
@@ 1,49 0,0 @@
local utils = require 'complementree.utils'

local record Filters
  amount: function(integer): Pipe
  prefix: Pipe
  strict_prefix: Pipe
  substr: Pipe
end

local type Filter = function(integer, CompleteItem, string): boolean

-- A function that returns a function that returns a function
local function mk_filter(func: Filter): Pipe
  return function(msource: Source): Source
    return function(ltc: string, lnum: integer): {CompleteItem}, string
      local orig, prefix = msource(ltc, lnum)
      local filtered: {CompleteItem} = {}
      for i, v in ipairs(orig) do
        if func(i, v, prefix) then
          table.insert(filtered, v)
        end
      end
      return filtered, prefix
    end
  end
end

function Filters.amount(n: integer): Pipe
  return mk_filter(function(i: integer, _: CompleteItem, _: string): boolean
    return i <= n
  end)
end

Filters.prefix = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
  return vim.startswith(utils.cword(v), prefix)
end)

Filters.strict_prefix = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
  local w = utils.cword(v)
  return vim.startswith(w, prefix) and #w ~= #prefix
end)

Filters.substr = mk_filter(function(_: integer, v: CompleteItem, prefix: string): boolean
  local w = utils.cword(v)
  local start = w:find(prefix, 1, true)
  return start ~= nil
end)

return Filters

D teal/complementree/init.tl => teal/complementree/init.tl +0 -182
@@ 1,182 0,0 @@
local api = vim.api
local defaults = require 'complementree.defaults'
local utils = require 'complementree.utils'
local sources = require 'complementree.sources'
local tsutils = require 'nvim-treesitter.ts_utils'
local M = {}

-- Explanation of the Completor nesting:
-- 1. Same completion for all ft
-- 2. Completion based on node type
-- 3. Completion based on query macthing
local type UserConfig = {string: Completor|{string:Completor|{string:Completor}}}

local user_config: UserConfig = {
  default = defaults.dummy,
  vim = defaults.ins_completion 'C-V',
}

function M.setup(config: UserConfig)
  if not config.default then
    error 'This config does not have a default key.'
  end

  local def = config.default
  if not def is Completor then
    error 'Invalid default completion'
  end

  user_config = config
end

function M.print_config()
  print(vim.inspect(user_config))
end

local function correct_position(line_to_cursor: string, linenr: integer): integer, integer
  local col = vim.fn.match(line_to_cursor, '\\s*\\k*$')
  return linenr - 1, col - 1
end

local function node_type_at_position(l: integer, c: integer): string
  local root = tsutils.get_root_for_position(l, c)
  if not root then
    return
  end

  local node = root:named_descendant_for_range(l, c, l, c)
  if not node then
    return
  end

  return node:type()
end

local function get_completion(ft: string, line_to_cursor: string, lnum: integer, _col: integer): Completor|nil
  local l, c = correct_position(line_to_cursor, lnum)
  local ft_completion = user_config[ft] or user_config.default
  if ft_completion then
    if ft_completion is {string: Completor|{string:Completor}} then
      local root = tsutils.get_root_for_position(l, c)
      -- Before attempting to match the node type, try query based filters
      for q, sub in pairs(ft_completion) do
        -- Queries always start with a parenthesis
        if vim.startswith(q, '(') then
          local query = vim.treesitter.query.parse(ft, q)
          for id, node in query:iter_captures(root, 0, l, l + 1) do
            local cname = query.captures[id]
            if tsutils.is_in_node_range(node, l, c) then
              if sub is {string:Completor} and sub[cname] then
                return sub[cname]
              elseif sub is Completor then
                return sub
              else
                -- We will definitely not be able to do anything with this source
                break
              end
            end
          end
        end
      end

      local t = node_type_at_position(l, c)
      if not t then
        local def = ft_completion.default
        if def is Completor then
          return def
        else
          error 'Invalid default completion source.'
        end
      end
      local sub_completion = ft_completion[t] or ft_completion.default
      if sub_completion and sub_completion is Completor then
        return sub_completion
      end
    elseif type(ft_completion) == 'function' then
      return ft_completion
    end
  end
end

function M.separate_prefix(line: string, cursor: integer): string, integer, string
  local line_to_cursor = line:sub(1, cursor)
  local pref_start = line_to_cursor:find '%S*$'
  local prefix = line_to_cursor:sub(pref_start)

  return line_to_cursor, pref_start, prefix
end

function M.complete(): boolean
  -- Only refresh when not restarting
  if vim.fn.pumvisible() == 0 then
    sources.invalidate_cache()
  end
  if not vim.fn.mode():find 'i' then
    return false
  end

  local bufnr = api.nvim_get_current_buf()
  local ft = api.nvim_buf_get_option(bufnr, 'filetype') as string

  local line = api.nvim_get_current_line()
  local cursor = api.nvim_win_get_cursor(0)
  local lnum = cursor[1]
  local cursor_pos = cursor[2]
  local line_to_cursor, pref_start, _prefix = M.separate_prefix(line, cursor_pos)

  -- The source signature is
  -- line_content, line_content_up_to_cursor, prefix, column

  local func = get_completion(ft, line_to_cursor, lnum, pref_start)
  if not func is nil then
    if func(line_to_cursor, lnum) then
      return true
    end
  end
  return false
end

function M._CompleteDone()
  local completed_item = api.nvim_get_vvar 'completed_item' as CompleteItem
  if not completed_item or not completed_item.user_data or not completed_item.user_data.source then
    return
  end
  local func = sources.complete_done_cbs[completed_item.user_data.source]
  if func then
    -- We will force the ignore of InsertLeave events for a certain time, in order to avoid strange
    -- behavior and flickering of the UI. So we _synchronously_ set 'eventignore' to ignore
    -- InsertLeave, and schedule the reset in the loop.
    --
    -- The reset is scheduled during this time, so that when the user's events start to be
    -- processed, everything will be just as if nothing happened.
    local previous_opt = api.nvim_get_option 'eventignore' as string
    local newval = previous_opt
    if #newval == 0 then
      newval = 'InsertLeave'
    else
      newval = 'InsertLeave,' .. newval
    end
    api.nvim_set_option('eventignore', newval)
    func(completed_item)
    vim.schedule(function()
      api.nvim_set_option('eventignore', previous_opt)
    end)
  end
end

function M._InsertCharPre()
  if vim.fn.pumvisible() == 1 then
    local char = api.nvim_get_vvar 'char' as string
    if char:find '%s' then
      -- Whitespace, so accept this choice and stop here
      utils.feed '<C-Y>'
    else
      -- Refresh completion after this char is inserted
      vim.schedule(function()
        M.complete()
      end)
    end
  end
end

return M

D teal/complementree/options.tl => teal/complementree/options.tl +0 -8
@@ 1,8 0,0 @@
local M = {}

function M.get<T>(defaults: T, user: T): T
  vim.tbl_deep_extend('force', defaults as {string: any}, user as {string: any})
  return defaults
end

return M

D teal/complementree/sources.tl => teal/complementree/sources.tl +0 -495
@@ 1,495 0,0 @@
local record LspOptions
  -- Empty
end

local record LuasnipOptions
  exclude_defaults: boolean
  filetype: string
end

local record CtagsOptions
  -- Empty
end

local record FilepathOptions
  show_hidden: boolean
  ignore_directories: boolean
  max_depth: integer
  relative_paths: boolean
  ignore_pattern: string
  root_dirs: {string}
end

local record TreesitterOptions
  -- Empty
end

local record Sources
  lsp_matches: function(LspOptions): Source
  luasnip_matches: function(LuasnipOptions): Source
  ctags_matches: function(CtagsOptions): Source
  filepath_matches: function(FilepathOptions): Source
  treesitter_matches: function(TreesitterOptions): Source

  -- Internal
  complete_done_cbs: {string:function(CompleteItem)}
end

local utils = require 'complementree.utils'
local options = require 'complementree.options'
local api = vim.api
local lsp = vim.lsp

local cache: {string: {{CompleteItem}, string}} = {}

function Sources.invalidate_cache()
  cache = {}
end

local function cached(kind: string, func: Source): Source
  return function(ltc: string, lnum: integer): {CompleteItem}, string
    local m: {CompleteItem}
    local p: string
    if not cache[kind] then
      m, p = func(ltc, lnum)
      cache[kind] = { m, p }
    else
      m = cache[kind][1]
      p = cache[kind][2]
      -- We need to correct the prefix now
      -- in order to include the added character
      -- FIXME(vigoux): this is not right, we lose the whole "prefix resolution" thing by
      -- only using a regex here. But I think it is fine, for performanace reasons
      local pref_start = vim.fn.match(ltc, p .. '\\k*$') + 1
      if pref_start >= 1 then
        p = ltc:sub(pref_start)
      end
    end
    local new = {}
    for _, v in ipairs(m) do
      table.insert(new, v)
    end
    return new, p
  end
end

-- Options:
--
-- filetype: forces the filetype as a source
-- exclude_default: don't include the default snippets
function Sources.luasnip_matches(opts: LuasnipOptions): Source
  opts = options.get(opts, {
    exclude_defaults = false,
    filetype = nil,
  })

  local lsnip_present,luasnip = pcall(require, "luasnip")
  if not lsnip_present then
    error("LuaSnip is not installed")
  end

  local function add_snippet(items: {CompleteItem}, s: luasnip.Snippet)
    table.insert(items, {
      word = s.trigger,
      kind = 'S',
      menu = table.concat(s.description or {}),
      icase = 1,
      dup = 1,
      empty = 1,
      equal = 1,
      user_data = { source = 'luasnip' },
    })
  end

  return cached('luasnip', function(line_to_cursor: string, _: integer): {CompleteItem}, string
    local prefix = utils.prefix.lua_regex('%w*$', line_to_cursor)
    local items: {CompleteItem} = {}

    -- Luasnip format:
    -- {
    --  description = table(string),
    --  name = string,
    --  regTrig = bool,
    --  trigger = string,
    --  wordTrig = bool
    -- }

    for ftname, snips in pairs(luasnip.available()) do
      if not (ftname == 'all' and opts.exclude_defaults) then
        vim.tbl_map(function(s: luasnip.Snippet)
          add_snippet(items, s)
        end, snips)
      end
    end

    return items, prefix
  end)
end

local record LspExtraInfo
  client_id: integer
  item: lsp.LspCompletionItem
end

-- Shamelessly stollen from https://github.com/mfussenegger/nvim-lsp-compl with small adaptations
function Sources.lsp_matches(opts: LspOptions): Source
  opts = options.get({} as LspOptions, opts)
  return cached('lsp', function(line_to_cursor: string, lnum: integer): {CompleteItem}, string
    -- For lsp determining the preffix is painful, but thanks to the great @mfussenegger, we can fix
    -- this all !
    local function adjust_start_col(line_number: integer, line: string, items: {lsp.LspCompletionItem}, encoding: string): integer|nil
      local min_start_char: integer = nil

      for _, item in ipairs(items) do
        if item.textEdit and item.textEdit.range.start.line == line_number - 1 then
          if min_start_char and min_start_char ~= item.textEdit.range.start.character then
            return nil
          end
          min_start_char = item.textEdit.range.start.character
        end
      end
      if min_start_char then
        if encoding == 'utf-8' then
          return min_start_char + 1
        else
          return vim.str_byteindex(line, min_start_char, encoding == 'utf-16') + 1
        end
      else
        return nil
      end
    end

    local params = lsp.util.make_position_params()
    local result_all, err = lsp.buf_request_sync(0, 'textDocument/completion', params)
    if err then
      api.nvim_err_writeln(string.format('Error while completing lsp: %s', err))
      return {}, ''
    end
    if not result_all then
      return {}, ''
    end

    local matches = {}
    local start_col = vim.fn.match(line_to_cursor, '\\k*$') + 1
    for client_id, result in pairs(result_all) do
      local client = lsp.get_client_by_id(client_id)
      local items = lsp.util.extract_completion_items(result.result) or {}

      local tmp_col = adjust_start_col(lnum, line_to_cursor, items, client.offset_encoding or 'utf-16')
      if tmp_col and tmp_col < start_col then
        start_col = tmp_col
      end

      for _, item in ipairs(items) do
        local kind = lsp.protocol.CompletionItemKind[item.kind] or ''
        local word: string = nil
        if kind == 'Snippet' then
          word = item.label
        elseif item.insertTextFormat == 2 then
          if item.textEdit then
            word = item.insertText or item.textEdit.newText
          elseif item.insertText then
            if #item.label < #item.insertText then
              word = item.label
            else
              word = item.insertText
            end
          else
            word = item.label
          end
        else
          word = (item.textEdit and item.textEdit.newText) or item.insertText or item.label
        end
        local ud: CompleteExtraInfo = {
          source = 'lsp',
          extra = { client_id = client_id, item = item } as LspExtraInfo
        }
        table.insert(matches, {
          word = word,
          abbr = item.label,
          kind = kind,
          menu = item.detail or '',
          icase = 1,
          dup = 1,
          empty = 1,
          equal = 1,
          user_data = ud,
        })
      end
    end
    local prefix = line_to_cursor:sub(start_col)
    return matches, prefix
  end)
end

local function apply_snippet(item: lsp.LspCompletionItem, suffix: string, lnum: integer)
  local luasnip = require 'luasnip'
  if item.textEdit then
    luasnip.lsp_expand(item.textEdit.newText .. suffix)
  elseif item.insertText then
    luasnip.lsp_expand(item.insertText .. suffix)
  elseif item.label then
    luasnip.lsp_expand(item.label .. suffix)
  end
  vim.schedule(function()
    local curline = api.nvim_get_current_line()
    if vim.endswith(curline, suffix) and not luasnip.get_active_snip() then
      local newcol = #curline - #suffix
      api.nvim_win_set_cursor(0, { lnum + 1, newcol })
    end
  end)
end

local ctags_extension: {string: {string:string}} = {
  default = {
    ['c'] = 'class',
    ['d'] = 'define',
    ['e'] = 'enumerator',
    ['f'] = 'function',
    ['F'] = 'file',
    ['g'] = 'enumeration',
    ['m'] = 'member',
    ['p'] = 'prototype',
    ['s'] = 'structure',
    ['t'] = 'typedef',
    ['u'] = 'union',
    ['v'] = 'variable',
  },
}

function Sources.ctags_matches(opts: CtagsOptions): Source
  opts = options.get({} as CtagsOptions, opts)
  return cached('ctags', function(line_to_cursor: string, _: integer): {CompleteItem}, string
    local prefix = utils.prefix.vim_keyword(line_to_cursor)

    local filetype = api.nvim_buf_get_option(0, 'filetype') as string
    local extensions = ctags_extension[filetype] or ctags_extension.default
    local tags = vim.fn.taglist '.*'

    local items: {CompleteItem} = {}
    for _, t in ipairs(tags) do
      local ud: CompleteExtraInfo = { source = 'ctags' }
      items[#items + 1] = {
        word = t.name,
        kind = (t.kind and extensions[t.kind] or 'undefined'),
        icase = 1,
        dup = 0,
        equal = 1,
        empty = 1,
        user_data = ud,
      }
    end

    return items, prefix
  end)
end

--

local os_name = string.lower(jit.os)
local is_linux = (os_name == 'linux' or os_name == 'osx' or os_name == 'bsd')
local os_sep = is_linux and '/' or '\\'
local os_path = '[' .. os_sep .. '%w+%-%.%_]*$'

function Sources.filepath_matches(opts: FilepathOptions): Source
  local relpath = utils.make_relative_path
  local config = options.get({
    show_hidden = false,
    ignore_directories = true,
    max_depth = math.huge,
    relative_paths = false,
    ignore_pattern = '',
    root_dirs = { '.' },
  }, opts)

  local function iter_files(): function(): string|nil
    local path_stack: {string} = vim.fn.reverse(config.root_dirs or { '.' })
    local iter_stack = {}
    for _, p in ipairs(path_stack) do
      table.insert(iter_stack, vim.loop.fs_scandir(p))
    end

    if config.max_depth == 0 then
      return function(): string|nil
        return nil
      end
    end

    return function(): string|nil
      local iter = iter_stack[#iter_stack]
      local path = path_stack[#path_stack]
      while true do
        local next_path, path_type = vim.loop.fs_scandir_next(iter)

        if not next_path then
          table.remove(iter_stack)
          table.remove(path_stack)
          if #iter_stack == 0 then
            return nil
          end
          iter = iter_stack[#iter_stack]
          path = path_stack[#path_stack]
        elseif
          (vim.startswith(next_path, '.') and not config.show_hidden)
          or (#config.ignore_pattern > 0 and string.find(next_path, config.ignore_pattern) ~= nil)
        then
          next_path = nil
          path_type = nil
        else
          local full_path = path .. os_sep .. next_path
          if path_type == 'directory' then
            if #iter_stack < config.max_depth then
              iter = vim.loop.fs_scandir(full_path)
              path = full_path
              table.insert(path_stack, full_path)
              table.insert(iter_stack, iter)
            end
            if not config.ignore_directories then
              return full_path
            end
          else
            return full_path
          end
        end
      end
    end
  end

  return cached('filepath', function(line_to_cursor: string, _: integer): {CompleteItem}, string
    local prefix = utils.prefix.lua_regex(os_path, line_to_cursor)

    local cwd = vim.fn.getcwd()
    local fpath: string
    local matches = {}
    for path in iter_files() do
      fpath = config.relative_paths and relpath(path, cwd) or path

      matches[#matches + 1] = {
        word = fpath,
        abbr = fpath,
        kind = '[path]',
        icase = 1,
        dup = 1,
        empty = 1,
        equal = 1,
        user_data = { source = 'filepath' },
      }
    end

    return matches, prefix
  end)
end

-- Treesitter source

local tslocals = require 'nvim-treesitter.locals'

function Sources.treesitter_matches(opts: TreesitterOptions): Source
  local _config = options.get({} as TreesitterOptions, opts)

  return cached('treesitter', function(line_to_cursor: string, _lnum: integer): {CompleteItem}, string
    local prefix = utils.prefix.lua_regex('%S*$', line_to_cursor)
    local defs = tslocals.get_definitions(0)

    local items: {CompleteItem} = {}

    for _, def in ipairs(defs) do
      -- Determine kind and text
      local node: vim.treesitter.TSNode
      local kind: string
      for k,cap in pairs(def as {string:tslocals.LocalCapture.Captured}) do
        if k ~= 'associated' then
          node = cap.node
          kind = k
          break
        end
      end

      if node then
        items[#items + 1] = {
          word = vim.treesitter.get_node_text(node, 0),
          kind = kind,
          icase = 1,
          dup = 0,
          empty = 1,
          equal = 1,
          user_data = { source = 'treesitter' },
        }
      end
    end

    return items, prefix
  end)
end

-- CompleteDone handlers

local function lsp_completedone(completed_item: CompleteItem)
  local cursor = api.nvim_win_get_cursor(0)
  local col = cursor[2]
  local lnum = cursor[1] - 1
  local bufnr = api.nvim_get_current_buf()

  local extra = completed_item.user_data.extra as LspExtraInfo
  local item = extra.item

  local client = lsp.get_client_by_id(extra.client_id)
  if not client then
    error(string.format("Could not find client %d", extra.client_id))
  end

  local expand_snippet = item.insertTextFormat == 2
  local resolveEdits = (client.server_capabilities.completionProvider or {}).resolveProvider
  local offset_encoding = client and client.offset_encoding or 'utf-16'

  local tidy = function() end
  local suffix: string = nil

  if expand_snippet then
    local line = api.nvim_buf_get_lines(bufnr, lnum, lnum + 1, true)[1]
    tidy = function()
      -- Remove the already inserted word
      local start_char = col - #completed_item.word
      local l = line
      api.nvim_buf_set_text(bufnr, lnum, start_char, lnum, #l, { '' })
    end
    suffix = line:sub(col + 1)
  end

  if item.additionalTextEdits then
    tidy()
    lsp.util.apply_text_edits(item.additionalTextEdits, bufnr, offset_encoding)
    if expand_snippet then
      apply_snippet(item, suffix, lnum)
    end
  elseif resolveEdits and type(item) == 'table' then
    local v = client.request_sync('completionItem/resolve', item, 1000, bufnr)
    assert(not v.err, vim.inspect(v.err))
    local res = v.result as lsp.LspCompletionItem
    if res.additionalTextEdits then
      tidy()
      tidy = function() end
      lsp.util.apply_text_edits(res.additionalTextEdits, bufnr, offset_encoding)
    end
    if expand_snippet then
      tidy()
      apply_snippet(item, suffix, lnum)
    end
  elseif expand_snippet then
    tidy()
    apply_snippet(item, suffix, lnum)
  end
end

local function luasnip_completedone(_: CompleteItem)
  if require('luasnip').expandable() then
    require('luasnip').expand()
  end
end

Sources.complete_done_cbs = {
  lsp = lsp_completedone,
  luasnip = luasnip_completedone,
}

return Sources

D teal/complementree/utils.tl => teal/complementree/utils.tl +0 -44
@@ 1,44 0,0 @@
local api = vim.api

local record Prefix
end

function Prefix.lua_regex(regex: string, line: string): string
  local pref_start = line:find(regex)
  local prefix = line:sub(pref_start)

  return prefix
end

function Prefix.vim_keyword(line: string): string
  local pref_start = vim.fn.match(line, '\\k*$') + 1
  local prefix = line:sub(pref_start)

  return prefix
end

local record Utils
  prefix: Prefix
end

function Utils.feed(codes: string)
  api.nvim_feedkeys(api.nvim_replace_termcodes(codes, true, true, true), 'm', true)
end

function Utils.cword(complete_item: CompleteItem): string
  return (complete_item.abbr or complete_item.word)
end

function Utils.make_relative_path(path: string, root: string): string
  if vim.startswith(path, root) then
    local baselen = #root
    if path:sub(0, baselen) == root then
      path = path:sub(baselen + 2)
    end
  end
  return path
end

Utils.prefix = Prefix

return Utils