--- *mini.completion* Completion and signature help
--- *MiniCompletion*
---
--- MIT License Copyright (c) 2021 Evgeni Chasnovski
---
--- ==============================================================================
---
--- Key design ideas:
--- - Have an async (with customizable "debounce" delay) "two-stage chain
---   completion": first try to get completion items from LSP client (if set
---   up) and if no result, fallback to custom action.
---
--- - Managing completion is done as much with Neovim's built-in tools as
---   possible. |popupmenu-completion| is used to show completion suggetsions.
---
--- Features:
--- - Two-stage chain completion:
---     - First stage is an LSP completion implemented via
---       |MiniCompletion.completefunc_lsp()|. It should be set up as either
---       |completefunc| or |omnifunc|. It tries to get completion items from
---       LSP client (via 'textDocument/completion' request). Custom
---       preprocessing of response items is possible (with
---       `MiniCompletion.config.lsp_completion.process_items`), for example
---       with fuzzy matching. By default items directly starting with completed
---       word are kept and are sorted according to LSP specification.
---       Supports `additionalTextEdits`, like auto-import and others (see 'Notes'),
---       and snippet items (best results require |mini.snippets| dependency).
---     - If first stage is not set up or resulted into no candidates, fallback
---       action is executed. The most tested actions are Neovim's built-in
---       insert completion (see |ins-completion|).
---
--- - Automatic display in floating window of completion item info (via
---   'completionItem/resolve' request) and signature help (with highlighting
---   of active parameter if LSP server provides such information). After opening,
---   window for signature help is fixed and is closed when there is nothing to
---   show, text is different or when leaving Insert mode.
---   Scroll in either info/signature window (`<C-f>` / `<C-b>` by default).
---
--- - Automatic actions are done after some configurable amount of delay. This
---   reduces computational load and allows fast typing (completion and
---   signature help) and item selection (item info)
---
--- - Force two-stage/fallback completion (`<C-Space>` / `<A-Space>` by default).
---
--- - LSP kind highlighting ("Function", "Keyword", etc.). Requires Neovim>=0.11.
---   By default uses "lsp" category of |MiniIcons| (if enabled). Can be customized
---   via `config.lsp_completion.process_items` by adding field <kind_hlgroup>
---   (same meaning as in |complete-items|) to items.
---
--- What it doesn't do:
--- - Many configurable sources.
--- - Automatic mapping of `<CR>`, `<Tab>`, etc., as those tend to have highly
---   variable user expectations. See 'Helpful mappings' for suggestions.
---
--- # Dependencies ~
---
--- Suggested dependencies (provide extra functionality, will work without them):
---
--- - Enabled |MiniIcons| module to highlight LSP kind (requires Neovim>=0.11).
---   If absent, |MiniCompletion.default_process_items()| does not add highlighting.
---   Also take a look at |MiniIcons.tweak_lsp_kind()|.
--- - Enabled |MiniSnippets| module for better snippet handling (much recommended).
---   If absent and custom snippet insert is not configured, |vim.snippet.expand()|
---   is used on Neovim>=0.10 (nothing extra is done on earlier versions).
---   See |MiniCompletion.default_snippet_insert()|.
---
--- # Setup ~
---
--- This module needs a setup with `require('mini.completion').setup({})`
--- (replace `{}` with your `config` table). It will create global Lua table
--- `MiniCompletion` which you can use for scripting or manually (with
--- `:lua MiniCompletion.*`).
---
--- See |MiniCompletion.config| for `config` structure and default values.
---
--- You can override runtime config settings locally to buffer inside
--- `vim.b.minicompletion_config` which should have same structure as
--- `MiniCompletion.config`. See |mini.nvim-buffer-local-config| for more details.
---
--- # Snippets ~
---
--- As per LSP specification, some completion items can be supplied in the form of
--- snippet - a template with both pre-defined text and places (called "tabstops")
--- for user to interactively change/add text during snippet session.
---
--- In 'mini.completion' items that will insert snippet have "S" symbol shown in
--- the popup (as part of `menu` in |complete-items|). To actually insert a snippet:
--- - Select an item via <C-n> / <C-p>. This will insert item's label (usually not
---   full snippet) first to reduce visual flicker. The full snippet text will be
---   shown in info window if LSP server doesn't provide its own info for an item.
--- - Press <C-y> (|complete_CTRL-Y|) or attempt inserting a non-keyword character
---   (like <CR>; new character will be removed). It will clear text from previous
---   step, set cursor, and call `lsp_completion.snippet_insert` with snippet text.
--- - Press <C-e> (|complete_CTRL-E|) to cancel snippet insert and properly end
---   completion.
---
--- See |MiniCompletion.default_snippet_insert()| for overview of how to work with
--- inserted snippets.
---
--- Notes:
--- - To stop LSP server from suggesting snippets, disable (set to `false`) the
---   following capability during LSP server start:
---   `textDocument.completion.completionItem.snippetSupport`.
--- - If snippet body doesn't contain tabstops, `lsp_completion.snippet_insert`
---   is not called and text is inserted as is.
---
--- # Notes ~
---
--- - More appropriate (albeit slightly advanced) LSP completion setup is to set
---   it not on every |BufEnter| event (default), but on every attach of LSP
---   client. To do that:
---     - Use in initial config:
---     `lsp_completion = { source_func = 'omnifunc', auto_setup = false }`.
---     - In `on_attach()` of every LSP client set 'omnifunc' option to exactly
---       `v:lua.MiniCompletion.completefunc_lsp`.
---
--- - The `additionalTextEdits` data can come from LSP server only in response for
---   "textDocument/resolve". For these servers select completion item and wait
---   for `config.delay.info` time plus server response time to process the request.
---
--- - Uses `vim.lsp.protocol.CompletionItemKind` map in LSP step to show a readable
---   version of item's kind. Modify it directly to change what is displayed.
---   If you have |mini.icons| enabled, take a look at |MiniIcons.tweak_lsp_kind()|.
---
--- - If you have trouble using custom (overridden) |vim.ui.input|, disable
---   'mini.completion' for input buffer (usually based on its 'filetype').
---
--- # Comparisons ~
---
--- - 'nvim-cmp':
---     - More complex design which allows multiple sources each in form of
---       separate plugin. `MiniCompletion` has two built in: LSP and fallback.
---     - Supports snippet expansion.
---     - Doesn't have customizable delays for basic actions.
---     - Doesn't allow fallback action.
---     - Doesn't provide signature help.
---
--- # Helpful mappings ~
---
--- To use `<Tab>` and `<S-Tab>` for navigation through completion list, make
--- these mappings: >lua
---
---   local imap_expr = function(lhs, rhs)
---     vim.keymap.set('i', lhs, rhs, { expr = true })
---   end
---   imap_expr('<Tab>',   [[pumvisible() ? "\<C-n>" : "\<Tab>"]])
---   imap_expr('<S-Tab>', [[pumvisible() ? "\<C-p>" : "\<S-Tab>"]])
--- <
--- To get more consistent behavior of `<CR>`, you can use this template in
--- your 'init.lua' to make customized mapping: >lua
---
---   local keycode = vim.keycode or function(x)
---     return vim.api.nvim_replace_termcodes(x, true, true, true)
---   end
---   local keys = {
---     ['cr']        = keycode('<CR>'),
---     ['ctrl-y']    = keycode('<C-y>'),
---     ['ctrl-y_cr'] = keycode('<C-y><CR>'),
---   }
---
---   _G.cr_action = function()
---     if vim.fn.pumvisible() ~= 0 then
---       -- If popup is visible, confirm selected item or add new line otherwise
---       local item_selected = vim.fn.complete_info()['selected'] ~= -1
---       return item_selected and keys['ctrl-y'] or keys['ctrl-y_cr']
---     else
---       -- If popup is not visible, use plain `<CR>`. You might want to customize
---       -- according to other plugins. For example, to use 'mini.pairs', replace
---       -- next line with `return require('mini.pairs').cr()`
---       return keys['cr']
---     end
---   end
---
---   vim.keymap.set('i', '<CR>', 'v:lua._G.cr_action()', { expr = true })
--- <
--- # Highlight groups ~
---
--- * `MiniCompletionActiveParameter` - signature active parameter.
---   By default displayed as plain underline.
---
--- To change any highlight group, modify it directly with |:highlight|.
---
--- # Disabling ~
---
--- To disable, set `vim.g.minicompletion_disable` (globally) or
--- `vim.b.minicompletion_disable` (for a buffer) to `true`. Considering high
--- number of different scenarios and customization intentions, writing exact
--- rules for disabling module's functionality is left to user. See
--- |mini.nvim-disabling-recipes| for common recipes.

-- Overall implementation design:
-- - Completion:
--     - On `InsertCharPre` event try to start auto completion. If needed,
--       start timer which after delay will start completion process. Stop this
--       timer if it is not needed.
--     - When timer is activated, first execute LSP source (if set up and there
--       is an active LSP client) by calling built-in complete function
--       (`completefunc` or `omnifunc`) which tries LSP completion by
--       asynchronously sending LSP 'textDocument/completion' request to all
--       LSP clients. When all are done, execute callback which processes
--       results, stores them in LSP cache and reruns built-in complete
--       function which produces completion popup.
--     - If previous step didn't result into any completion, execute (in Insert
--       mode and if no popup) fallback action.
-- - Documentation:
--     - On `CompleteChanged` start auto info with similar to completion timer
--       pattern.
--     - If timer is activated, try these sources of item info:
--         - 'info' field of completion item (see `:h complete-items`).
--         - 'documentation' field of LSP's previously returned result.
--         - 'documentation' field in result of asynchronous
--           'completeItem/resolve' LSP request.
--     - If info doesn't consist only from whitespace, show floating window
--       with its content. Its dimensions and position are computed based on
--       current state of Neovim's data and content itself (which will be
--       displayed wrapped with `linebreak` option).
-- - Signature help (similar to item info):
--     - On `CursorMovedI` start auto signature (if there is any active LSP
--       client) with similar to completion timer pattern. Better event might
--       be `InsertCharPre` but there are issues with 'autopair-type' plugins.
--     - Check if character left to cursor is appropriate (')' or LSP's
--       signature help trigger characters). If not, do nothing.
--     - If timer is activated, send 'textDocument/signatureHelp' request to
--       all LSP clients. On callback, process their results. Window is opened
--       if not already with the same text (its characteristics are computed
--       similar to item info). For every LSP client it shows only active
--       signature (in case there are many). If LSP response has data about
--       active parameter, it is highlighted with
--       `MiniCompletionActiveParameter` highlight group.

-- Module definition ==========================================================
local MiniCompletion = {}
local H = {}

--- Module setup
---
---@param config table|nil Module config table. See |MiniCompletion.config|.
---
---@usage >lua
---   require('mini.completion').setup() -- use default config
---   -- OR
---   require('mini.completion').setup({}) -- replace {} with your config table
--- <
MiniCompletion.setup = function(config)
  -- Export module
  _G.MiniCompletion = MiniCompletion

  -- Setup config
  config = H.setup_config(config)

  -- Apply config
  H.apply_config(config)

  -- Define behavior
  H.create_autocommands(config)

  -- Create default highlighting
  H.create_default_hl()
end

--- Module config
---
--- Default values:
---@eval return MiniDoc.afterlines_to_code(MiniDoc.current.eval_section)
MiniCompletion.config = {
  -- Delay (debounce type, in ms) between certain Neovim event and action.
  -- This can be used to (virtually) disable certain automatic actions by
  -- setting very high delay time (like 10^7).
  delay = { completion = 100, info = 100, signature = 50 },

  -- Configuration for action windows:
  -- - `height` and `width` are maximum dimensions.
  -- - `border` defines border (as in `nvim_open_win()`).
  window = {
    info = { height = 25, width = 80, border = 'single' },
    signature = { height = 25, width = 80, border = 'single' },
  },

  -- Way of how module does LSP completion
  lsp_completion = {
    -- `source_func` should be one of 'completefunc' or 'omnifunc'.
    source_func = 'completefunc',

    -- `auto_setup` should be boolean indicating if LSP completion is set up
    -- on every `BufEnter` event.
    auto_setup = true,

    -- A function which takes LSP 'textDocument/completion' response items
    -- and word to complete. Output should be a table of the same nature as
    -- input items. Common use case is custom filter/sort.
    -- Default: `default_process_items`
    process_items = nil,

    -- A function which takes a snippet as string and inserts it at cursor.
    -- Default: `default_snippet_insert` which tries to use 'mini.snippets'
    -- and falls back to `vim.snippet.expand` (on Neovim>=0.10).
    snippet_insert = nil,
  },

  -- Fallback action as function/string. Executed in Insert mode.
  -- To use built-in completion (`:h ins-completion`), set its mapping as
  -- string. Example: set '<C-x><C-l>' for 'whole lines' completion.
  fallback_action = '<C-n>',

  -- Module mappings. Use `''` (empty string) to disable one. Some of them
  -- might conflict with system mappings.
  mappings = {
    -- Force two-step/fallback completions
    force_twostep = '<C-Space>',
    force_fallback = '<A-Space>',

    -- Scroll info/signature window down/up. When overriding, check for
    -- conflicts with built-in keys for popup menu (like `<C-u>`/`<C-o>`
    -- for 'completefunc'/'omnifunc' source function; or `<C-n>`/`<C-p>`).
    scroll_down = '<C-f>',
    scroll_up = '<C-b>',
  },

  -- Whether to set Vim's settings for better experience (modifies
  -- `shortmess` and `completeopt`)
  set_vim_settings = true,
}
--minidoc_afterlines_end

-- Module functionality =======================================================
--- Run two-stage completion
---
---@param fallback boolean|nil Whether to use fallback completion. Default: `true`.
---@param force boolean|nil Whether to force update of completion popup.
---   Default: `true`.
MiniCompletion.complete_twostage = function(fallback, force)
  if H.is_disabled() then return end
  if fallback == nil then fallback = true end
  if force == nil then force = true end

  H.stop_completion()
  H.completion.fallback, H.completion.force = fallback, force
  H.trigger_twostep()
end

--- Run fallback completion
MiniCompletion.complete_fallback = function()
  if H.is_disabled() then return end

  H.stop_completion()
  H.completion.fallback, H.completion.force = true, true
  H.trigger_fallback()
end

--- Scroll in info/signature window
---
--- Designed to be used in |:map-<expr>|.
--- Scrolling is done as if |CTRL-F| and |CTRL-B| is pressed inside target window.
--- Used in default `config.mappings.scroll_xxx` mappings.
---
---@param direction string One of `"down"` or `"up"`.
---
---@return boolean Whether scroll is scheduled to be done.
MiniCompletion.scroll = function(direction)
  if not (direction == 'down' or direction == 'up') then H.error('`direction` should be one of "up" or "down"') end
  local win_id = H.is_valid_win(H.info.win_id) and H.info.win_id
    or (H.is_valid_win(H.signature.win_id) and H.signature.win_id or nil)
  if win_id == nil then return false end

  -- Schedule execution as scrolling is not allowed in expression mappings
  local key = direction == 'down' and '\6' or '\2'
  vim.schedule(function()
    if not H.is_valid_win(win_id) then return end
    vim.api.nvim_win_call(win_id, function() vim.cmd('noautocmd normal! ' .. key) end)
  end)
  return true
end

--- Stop actions
---
--- This stops currently active (because of module delay or LSP answer delay)
--- actions.
---
--- Designed to be used with |autocmd|. No need to use it directly, everything
--- is setup in |MiniCompletion.setup|.
---
---@param actions table|nil Array containing any of 'completion', 'info', or
---   'signature' string. Default: array containing all of them.
MiniCompletion.stop = function(actions)
  actions = actions or { 'completion', 'info', 'signature' }
  for _, n in pairs(actions) do
    H.stop_actions[n]()
  end
end

--- Module's |complete-function|
---
--- This is the main function which enables two-stage completion. It should be
--- set as one of |completefunc| or |omnifunc|.
---
--- No need to use it directly, everything is setup in |MiniCompletion.setup|.
MiniCompletion.completefunc_lsp = function(findstart, base)
  -- Early return
  if not H.has_lsp_clients('completionProvider') or H.completion.lsp.status == 'sent' then
    return findstart == 1 and -3 or {}
  end

  -- NOTE: having code for request inside this function enables its use
  -- directly with `<C-x><...>` and as a reaction to `<BS>`.
  if H.completion.lsp.status ~= 'received' then
    -- NOTE: it is CRUCIAL to make LSP request on the first call to
    -- 'complete-function' (as in Vim's help). This is due to the fact that
    -- cursor line and position are different on the first and second calls to
    -- 'complete-function'. For example, when calling this function at the end
    -- of the line '  he', cursor position on the first call will be
    -- (<linenum>, 4) and line will be '  he' but on the second call -
    -- (<linenum>, 2) and '  ' (because 2 is a column of completion start).
    --
    -- This request is not executed on second call because it returns `-3` on
    -- first call (which means cancel and leave completion mode).
    H.make_completion_request()

    -- End completion and wait for LSP callback to re-trigger this
    return findstart == 1 and -3 or {}
  else
    if findstart == 1 then
      H.completion.start_pos = H.get_completion_start(H.completion.lsp.result)
      return H.completion.start_pos[2]
    end

    local process_items, is_incomplete = H.get_config().lsp_completion.process_items, false
    process_items = process_items or MiniCompletion.default_process_items
    local words = H.process_lsp_response(H.completion.lsp.result, function(response, client_id)
      is_incomplete = is_incomplete or response.isIncomplete
      -- Response can be `CompletionList` with 'items' field or `CompletionItem[]`
      local items = H.table_get(response, { 'items' }) or response
      if type(items) ~= 'table' then return {} end
      items = process_items(items, base)
      return H.lsp_completion_response_items_to_complete_items(items, client_id)
    end)

    H.completion.lsp.status = is_incomplete and 'done-isincomplete' or 'done'

    -- Maybe trigger fallback action
    if vim.tbl_isempty(words) and H.completion.fallback then return H.trigger_fallback() end

    -- Track from which source is current popup
    H.completion.source = 'lsp'
    return words
  end
end

--- Default processing of LSP items
---
--- Steps:
--- - Filter out items not starting with `base`.
--- - Sort by LSP specification.
--- - If |MiniIcons| is enabled, add <kind_hlgroup> based on the "lsp" category.
---
---@param items table Array of items from LSP response.
---@param base string Base for which completion is done. See |complete-functions|.
---
---@return table Array of processed items from LSP response.
MiniCompletion.default_process_items = function(items, base)
  local res = vim.tbl_filter(
    function(item) return vim.startswith(item.filterText or H.get_completion_word(item), base) end,
    items
  )

  res = vim.deepcopy(res)
  table.sort(res, function(a, b) return (a.sortText or a.label) < (b.sortText or b.label) end)

  -- Possibly add "kind" highlighting
  if _G.MiniIcons ~= nil then
    local add_kind_hlgroup = H.make_add_kind_hlgroup()
    for _, item in ipairs(res) do
      add_kind_hlgroup(item)
    end
  end

  return res
end

--- Default snippet insert
---
--- Order of preference:
--- - Use |MiniSnippets| if set up (i.e. there is `require('mini.snippets').setup()`).
--- - Use |vim.snippet.expand()| on Neovim>=0.10
--- - Add snippet text at cursor as is.
---
--- After snippet is inserted, user is expected to navigate/jump between dedicated
--- places (tabstops) to adjust inserted text as needed:
--- - |MiniSnippets| by default uses <C-l> / <C-h> to jump to next/previous tabstop.
---   Can be adjusted in `mappings` of |MiniSnippets.config|.
--- - |vim.snippet| on Neovim=0.10 requires manually created mappings for jumping
---   between tabstops (see |vim.snippet.jump()|). Neovim>=0.11 sets them up
---   automatically to <Tab> / <S-Tab> (if not overridden by user).
---
--- End session by navigating all the way to the last tabstop. In 'mini.snippets':
--- - Also make any text edit or exit Insert mode to end the session. This allows
---   smoother navigation to previous tabstops in case of a lately spotted typo.
--- - Press `<C-c>` to force session stop.
---
---@param snippet string Snippet body to insert at cursor.
---
---@seealso |MiniSnippets-session| if 'mini.snippets' is set up.
--- |vim.snippet| for Neovim's built-in snippet engine.
MiniCompletion.default_snippet_insert = function(snippet)
  if _G.MiniSnippets then
    local insert = MiniSnippets.config.expand.insert or MiniSnippets.default_insert
    return insert({ body = snippet })
  end
  if vim.fn.has('nvim-0.10') == 1 then return vim.snippet.expand(snippet) end

  local pos, lines = vim.api.nvim_win_get_cursor(0), vim.split(snippet, '\n')
  vim.api.nvim_buf_set_text(0, pos[1] - 1, pos[2], pos[1] - 1, pos[2], lines)
  local n = #lines
  local new_pos = n == 1 and { pos[1], pos[2] + lines[n]:len() } or { pos[1] + n - 1, lines[n]:len() }
  vim.api.nvim_win_set_cursor(0, new_pos)
end

-- Helper data ================================================================
-- Module default config
H.default_config = vim.deepcopy(MiniCompletion.config)

-- Track Insert mode changes
H.text_changed_id = 0

-- Namespace for highlighting
H.ns_id = vim.api.nvim_create_namespace('MiniCompletion')

-- Commonly used key sequences
H.keys = {
  completefunc = vim.api.nvim_replace_termcodes('<C-x><C-u>', true, false, true),
  omnifunc = vim.api.nvim_replace_termcodes('<C-x><C-o>', true, false, true),
  ctrl_n = vim.api.nvim_replace_termcodes('<C-g><C-g><C-n>', true, false, true),
}

-- Caches for different actions -----------------------------------------------
-- Field `lsp` is a table describing state of all used LSP requests. It has the
-- following structure:
-- - id: identifier (consecutive numbers).
-- - status: one of 'sent', 'received', 'done', 'done-isincomplete', 'canceled'
-- - result: result of request.
-- - cancel_fun: function which cancels current request.

-- Cache for completion
H.completion = {
  fallback = true,
  force = false,
  source = nil,
  text_changed_id = 0,
  timer = vim.loop.new_timer(),
  lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
  start_pos = {},
}

-- Cache for completion item info
H.info = {
  bufnr = nil,
  event = nil,
  id = 0,
  timer = vim.loop.new_timer(),
  win_id = nil,
  lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}

-- Cache for signature help
H.signature = {
  bufnr = nil,
  text = nil,
  timer = vim.loop.new_timer(),
  win_id = nil,
  lsp = { id = 0, status = nil, result = nil, cancel_fun = nil },
}

-- Helper functionality =======================================================
-- Settings -------------------------------------------------------------------
H.setup_config = function(config)
  H.check_type('config', config, 'table', true)
  config = vim.tbl_deep_extend('force', vim.deepcopy(H.default_config), config or {})

  H.check_type('delay', config.delay, 'table')
  H.check_type('window', config.window, 'table')
  H.check_type('lsp_completion', config.lsp_completion, 'table')
  if not (type(config.fallback_action) == 'function' or type(config.fallback_action) == 'string') then
    H.error('`fallback_action` should be function or string, not ' .. type(config.fallback_action))
  end
  H.check_type('mappings', config.mappings, 'table')
  H.check_type('set_vim_settings', config.set_vim_settings, 'boolean')

  H.check_type('delay.completion', config.delay.completion, 'number')
  H.check_type('delay.info', config.delay.info, 'number')
  H.check_type('delay.signature', config.delay.signature, 'number')

  H.check_type('window.info', config.window.info, 'table')
  H.check_type('window.signature', config.window.signature, 'table')

  if not (config.lsp_completion.source_func == 'completefunc' or config.lsp_completion.source_func == 'omnifunc') then
    H.error('`lsp_completion.source_func` should be one of "completefunc" or "omnifunc"')
  end
  H.check_type('lsp_completion.auto_setup', config.lsp_completion.auto_setup, 'boolean')
  H.check_type('lsp_completion.process_items', config.lsp_completion.process_items, 'callable')

  H.check_type('mappings.force_twostep', config.mappings.force_twostep, 'string')
  H.check_type('mappings.force_fallback', config.mappings.force_fallback, 'string')
  H.check_type('mappings.scroll_down', config.mappings.scroll_down, 'string')
  H.check_type('mappings.scroll_up', config.mappings.scroll_up, 'string')

  local is_string_or_array = function(x) return type(x) == 'string' or H.islist(x) end
  H.check_type('window.info.height', config.window.info.height, 'number')
  H.check_type('window.info.width', config.window.info.width, 'number')
  if not is_string_or_array(config.window.info.border) then
    H.error('`config.window.info.border` should be either string or array, not ' .. type(config.window.info.border))
  end
  H.check_type('window.signature.height', config.window.signature.height, 'number')
  H.check_type('window.signature.width', config.window.signature.width, 'number')
  if not is_string_or_array(config.window.signature.border) then
    H.error(
      '`config.window.signature.border` should be either string or array, not ' .. type(config.window.signature.border)
    )
  end

  return config
end

H.apply_config = function(config)
  MiniCompletion.config = config

  H.map('i', config.mappings.force_twostep, MiniCompletion.complete_twostage, { desc = 'Complete with two-stage' })
  H.map('i', config.mappings.force_fallback, MiniCompletion.complete_fallback, { desc = 'Complete with fallback' })

  local map_scroll = function(lhs, direction)
    local rhs = function() return MiniCompletion.scroll(direction) and '' or lhs end
    H.map('i', lhs, rhs, { expr = true, desc = 'Scroll info/signature ' .. direction })
  end
  map_scroll(config.mappings.scroll_down, 'down')
  map_scroll(config.mappings.scroll_up, 'up')

  if config.set_vim_settings then
    -- Don't give ins-completion-menu messages
    vim.opt.shortmess:append('c')
    if vim.fn.has('nvim-0.9') == 1 then vim.opt.shortmess:append('C') end

    -- More common completion behavior
    vim.o.completeopt = 'menuone,noselect'
  end
end

H.create_autocommands = function(config)
  local gr = vim.api.nvim_create_augroup('MiniCompletion', {})

  local au = function(event, pattern, callback, desc)
    vim.api.nvim_create_autocmd(event, { group = gr, pattern = pattern, callback = callback, desc = desc })
  end

  au('InsertCharPre', '*', H.auto_completion, 'Auto show completion')
  au('CompleteChanged', '*', H.auto_info, 'Auto show info')
  au('CursorMovedI', '*', H.auto_signature, 'Auto show signature')
  au('ModeChanged', 'i*:[^i]*', function() MiniCompletion.stop() end, 'Stop completion')
  au('CompleteDonePre', '*', H.on_completedonepre, 'On CompleteDonePre')
  au('TextChangedI', '*', H.on_text_changed_i, 'On TextChangedI')
  au('TextChangedP', '*', H.on_text_changed_p, 'On TextChangedP')

  if config.lsp_completion.auto_setup then
    local source_func = config.lsp_completion.source_func
    local callback = function() vim.bo[source_func] = 'v:lua.MiniCompletion.completefunc_lsp' end
    au('BufEnter', '*', callback, 'Set completion function')
  end

  au('ColorScheme', '*', H.create_default_hl, 'Ensure colors')
  au('FileType', 'TelescopePrompt', function() vim.b.minicompletion_disable = true end, 'Disable locally')
end

H.create_default_hl = function()
  vim.api.nvim_set_hl(0, 'MiniCompletionActiveParameter', { default = true, underline = true })
end

H.is_disabled = function() return vim.g.minicompletion_disable == true or vim.b.minicompletion_disable == true end

H.get_config = function(config)
  return vim.tbl_deep_extend('force', MiniCompletion.config, vim.b.minicompletion_config or {}, config or {})
end

-- Autocommands ---------------------------------------------------------------
H.auto_completion = function()
  if H.is_disabled() then return end

  H.completion.timer:stop()

  local is_incomplete = H.completion.lsp.status == 'done-isincomplete'
  local force = H.is_lsp_trigger(vim.v.char, 'completion') or is_incomplete
  if force then
    -- If character is LSP trigger, force fresh LSP completion later
    -- Check LSP trigger before checking for pumvisible because it should be
    -- forced even if there are visible candidates
    H.stop_completion(false)
  elseif H.pumvisible() then
    -- Do nothing if popup is visible. `H.pumvisible()` might be `true` even if
    -- there is no popup. It is common when manually typing candidate followed
    -- by an LSP trigger (like ".").
    -- Keep completion source as it is needed all time when popup is visible.
    return H.stop_completion(true)
  elseif not H.is_char_keyword(vim.v.char) then
    -- Stop everything if inserted character is not appropriate. Check this
    -- after popup check to allow completion candidates to have bad characters.
    return H.stop_completion(false)
  end

  -- Start non-forced completion with fallback or forced LSP source for trigger
  H.completion.fallback, H.completion.force = not force, force

  -- Cache id of Insert mode "text changed" event for a later tracking (reduces
  -- false positive delayed triggers). The intention is to trigger completion
  -- after the delay only if text wasn't changed during waiting. Using only
  -- `InsertCharPre` is not enough though, as not every Insert mode change
  -- triggers `InsertCharPre` event (notable example - hitting `<CR>`).
  -- Also, using `+ 1` here because it is a `Pre` event and needs to cache
  -- after inserting character.
  H.completion.text_changed_id = H.text_changed_id + 1

  -- If completion was requested after 'lsp' source exhausted itself (there
  -- were matches on typing start, but they disappeared during filtering), call
  -- fallback immediately.
  if H.completion.source == 'lsp' then return H.trigger_fallback() end

  -- Debounce delay improves experience (can type fast without many popups)
  -- Request immediately if improving incomplete suggestions (less flickering)
  if is_incomplete then return H.trigger_twostep() end
  H.completion.timer:start(H.get_config().delay.completion, 0, vim.schedule_wrap(H.trigger_twostep))
end

H.auto_info = function()
  if H.is_disabled() then return end

  H.info.timer:stop()

  -- Defer execution because of textlock during `CompleteChanged` event
  -- Don't stop timer when closing info window because it is needed
  vim.schedule(function() H.close_action_window(H.info, true) end)

  -- Stop current LSP request that tries to get not current data
  H.cancel_lsp({ H.info })

  -- Update metadata before leaving to register a `CompleteChanged` event
  H.info.event = vim.v.event
  H.info.id = H.info.id + 1

  -- Don't even try to show info if nothing is selected in popup
  if vim.tbl_isempty(H.info.event.completed_item) then return end

  H.info.timer:start(H.get_config().delay.info, 0, vim.schedule_wrap(H.show_info_window))
end

H.auto_signature = function()
  if H.is_disabled() then return end

  H.signature.timer:stop()
  if not H.has_lsp_clients('signatureHelpProvider') then return end

  local left_char = H.get_left_char()
  local char_is_trigger = left_char == ')' or H.is_lsp_trigger(left_char, 'signature')
  if not char_is_trigger then return end

  H.signature.timer:start(H.get_config().delay.signature, 0, vim.schedule_wrap(H.show_signature_window))
end

H.on_completedonepre = function()
  -- Do nothing if it is triggered inside `trigger_lsp()` as a result of
  -- emulating 'completefunc'/'omnifunc' keys. This can happen if popup is
  -- visible and pressing keys first hides it with 'CompleteDonePre' event.
  if H.completion.lsp.status == 'received' then return end

  -- Do extra actions for LSP completion items
  local lsp_data = H.table_get(vim.v.completed_item, { 'user_data', 'nvim', 'lsp' })
  if lsp_data ~= nil then H.make_lsp_extra_actions(lsp_data) end

  -- Stop processes
  MiniCompletion.stop({ 'completion', 'info' })
end

H.on_text_changed_i = function()
  -- Track Insert mode changes
  H.text_changed_id = H.text_changed_id + 1

  -- Stop 'info' processes in case no completion event is triggered but popup
  -- is not visible. See https://github.com/neovim/neovim/issues/15077
  H.stop_info()
end

H.on_text_changed_p = function()
  -- Track Insert mode changes
  H.text_changed_id = H.text_changed_id + 1
end

-- Completion triggers --------------------------------------------------------
H.trigger_twostep = function()
  -- Trigger only in Insert mode and if text didn't change after trigger
  -- request, unless completion is forced
  -- NOTE: check for `text_changed_id` equality is still not 100% solution as
  -- there are cases when, for example, `<CR>` is hit just before this check.
  -- Because of asynchronous id update and this function call (called after
  -- delay), these still match.
  local allow_trigger = (vim.fn.mode() == 'i')
    and (H.completion.force or (H.completion.text_changed_id == H.text_changed_id))
  if not allow_trigger then return end

  if H.has_lsp_clients('completionProvider') and H.has_lsp_completion() then
    H.trigger_lsp()
  elseif H.completion.fallback then
    H.trigger_fallback()
  end
end

H.trigger_lsp = function()
  -- Check for popup visibility is needed to reduce flickering.
  -- Possible issue timeline (with 100ms delay with set up LSP):
  -- 0ms: Key is pressed.
  -- 100ms: LSP is triggered from first key press.
  -- 110ms: Another key is pressed.
  -- 200ms: LSP callback is processed, triggers complete-function which
  --   processes "received" LSP request.
  -- 201ms: LSP request is processed, completion is (should be almost
  --   immediately) provided, request is marked as "done".
  -- 210ms: LSP is triggered from second key press. As previous request is
  --   "done", it will once make whole LSP request. Having check for visible
  --   popup should prevent here the call to complete-function.

  -- Do not trigger if not needed and/or allowed
  if vim.fn.mode() ~= 'i' or (H.pumvisible() and not H.completion.force) then return end

  -- Overall idea: first make LSP request and re-trigger this same function
  -- inside its callback to take the "received" route. This reduces flickering
  -- in case popup is visible (like for `isIncomplete` and trigger characters)
  -- as pressing 'completefunc'/'omnifunc' keys first hides completion menu.
  -- There are still minor visual defects: typing new character reduces number
  -- of matched items which can visually shrink popup while later increase it
  -- again after LSP response is received. This is usually fine (especially
  -- with not huge 'pumheight').
  if H.completion.lsp.status ~= 'received' then return H.make_completion_request() end
  local keys = H.keys[H.get_config().lsp_completion.source_func]
  vim.api.nvim_feedkeys(keys, 'n', false)
end

H.trigger_fallback = function()
  -- Fallback only in Insert mode when no popup is visible
  local has_popup = H.pumvisible() and not H.completion.force
  if has_popup or vim.fn.mode() ~= 'i' then return end

  -- Track from which source is current popup
  H.completion.source = 'fallback'

  -- Execute fallback action
  local fallback_action = H.get_config().fallback_action or H.default_fallback_action
  fallback_action = fallback_action == '<C-n>' and H.default_fallback_action or fallback_action
  if vim.is_callable(fallback_action) then return fallback_action() end
  if type(fallback_action) ~= 'string' then return end

  -- Having `<C-g><C-g>` also (for some mysterious reason) helps to avoid
  -- some weird behavior. For example, if `keys = '<C-x><C-l>'` then Neovim
  -- starts new line when there is no suggestions.
  local keys = string.format('<C-g><C-g>%s', fallback_action)
  local trigger_keys = vim.api.nvim_replace_termcodes(keys, true, false, true)
  vim.api.nvim_feedkeys(trigger_keys, 'n', false)
end

H.default_fallback_action = function() vim.api.nvim_feedkeys(H.keys.ctrl_n, 'n', false) end

-- Stop actions ---------------------------------------------------------------
H.stop_completion = function(keep_source)
  H.completion.timer:stop()
  H.cancel_lsp({ H.completion })
  H.completion.fallback, H.completion.force = true, false
  if H.completion.lsp.status == 'done-isincomplete' then H.completion.lsp.status = 'done' end
  if not keep_source then H.completion.source = nil end
end

H.stop_info = function()
  -- Id update is needed to notify that all previous work is not current
  H.info.id = H.info.id + 1
  H.info.timer:stop()
  H.cancel_lsp({ H.info })
  H.close_action_window(H.info)
end

H.stop_signature = function()
  H.signature.text = nil
  H.signature.timer:stop()
  H.cancel_lsp({ H.signature })
  H.close_action_window(H.signature)
end

H.stop_actions = {
  completion = H.stop_completion,
  info = H.stop_info,
  signature = H.stop_signature,
}

-- LSP ------------------------------------------------------------------------
---@param capability string|table|nil Server capability (possibly nested
---   supplied via table) to check.
---
---@return boolean Whether at least one LSP client supports `capability`.
---@private
H.has_lsp_clients = function(capability)
  local clients = H.get_buf_lsp_clients()
  if vim.tbl_isempty(clients) then return false end
  if not capability then return true end

  for _, c in pairs(clients) do
    local has_capability = H.table_get(c.server_capabilities, capability)
    if has_capability then return true end
  end
  return false
end

H.has_lsp_completion = function()
  local source_func = H.get_config().lsp_completion.source_func
  local func = vim.bo[source_func]
  return func == 'v:lua.MiniCompletion.completefunc_lsp'
end

H.is_lsp_trigger = function(char, type)
  local triggers
  local providers = { completion = 'completionProvider', signature = 'signatureHelpProvider' }

  for _, client in pairs(H.get_buf_lsp_clients()) do
    triggers = H.table_get(client, { 'server_capabilities', providers[type], 'triggerCharacters' })
    if vim.tbl_contains(triggers or {}, char) then return true end
  end
  return false
end

H.cancel_lsp = function(caches)
  caches = caches or { H.completion, H.info, H.signature }
  for _, c in pairs(caches) do
    if vim.tbl_contains({ 'sent', 'received' }, c.lsp.status) then
      if c.lsp.cancel_fun then c.lsp.cancel_fun() end
      c.lsp.status = 'canceled'
    end

    c.lsp.result, c.lsp.cancel_fun = nil, nil
  end
end

H.process_lsp_response = function(request_result, processor)
  if not request_result then return {} end

  local res = {}
  for client_id, item in pairs(request_result) do
    if not item.err and item.result then vim.list_extend(res, processor(item.result, client_id) or {}) end
  end

  return res
end

H.is_lsp_current = function(cache, id) return cache.lsp.id == id and cache.lsp.status == 'sent' end

-- Completion -----------------------------------------------------------------
H.make_completion_request = function()
  local current_id = H.completion.lsp.id + 1
  H.completion.lsp.id = current_id
  H.completion.lsp.status = 'sent'

  local buf_id, params = vim.api.nvim_get_current_buf(), H.make_position_params()
  -- NOTE: use `buf_request_all()` (instead of `buf_request()`) to easily
  -- handle possible fallback and to have all completion suggestions be later
  -- filtered with one `base`. Anyway, the most common situation is with one
  -- attached LSP client.
  local cancel_fun = vim.lsp.buf_request_all(buf_id, 'textDocument/completion', params, function(result)
    if not H.is_lsp_current(H.completion, current_id) then return end

    H.completion.lsp.status = 'received'
    H.completion.lsp.result = result

    -- Trigger LSP completion to use completefunc/omnifunc route
    H.trigger_lsp()
  end)

  -- Cache cancel function to disable requests when they are not needed
  H.completion.lsp.cancel_fun = cancel_fun
end

-- Source:
-- https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#textDocument_completion
H.lsp_completion_response_items_to_complete_items = function(items, client_id)
  if vim.tbl_count(items) == 0 then return {} end

  local res, item_kinds = {}, vim.lsp.protocol.CompletionItemKind
  local snippet_kind = vim.lsp.protocol.CompletionItemKind.Snippet
  local snippet_inserttextformat = vim.lsp.protocol.InsertTextFormat.Snippet
  for _, item in pairs(items) do
    local word = H.get_completion_word(item)

    local is_snippet_kind = item.kind == snippet_kind
    local is_snippet_format = item.insertTextFormat == snippet_inserttextformat
    -- Treat item as snippet only if it has tabstops. This is important to make
    -- "implicit" expand work with LSP servers that report even regular words
    -- as `InsertTextFormat.Snippet` (like `gopls`).
    local needs_snippet_insert = (is_snippet_kind or is_snippet_format)
      and (word:find('[^\\]%${?%d') ~= nil or word:find('^%${?%d') ~= nil)

    local details = item.labelDetails or {}
    local snippet_clue = needs_snippet_insert and 'S' or ''
    local label_detail = (details.detail or '') .. (details.description or '')
    label_detail = snippet_clue .. ((snippet_clue ~= '' and label_detail ~= '') and ' ' or '') .. label_detail

    local lsp_data = { completion_item = item, client_id = client_id, needs_snippet_insert = needs_snippet_insert }
    table.insert(res, {
      -- Show less for snippet items (usually less confusion)
      word = needs_snippet_insert and item.label or word,
      abbr = item.label,
      kind = item_kinds[item.kind] or 'Unknown',
      kind_hlgroup = item.kind_hlgroup,
      menu = label_detail,
      -- NOTE: info will be attempted to resolve, use snippet text as fallback
      info = needs_snippet_insert and word or nil,
      icase = 1,
      dup = 1,
      empty = 1,
      user_data = { nvim = { lsp = lsp_data } },
    })
  end
  return res
end

H.make_add_kind_hlgroup = function()
  -- Account for possible effect of `MiniIcons.tweak_lsp_kind()` which modifies
  -- only array part of `CompletionItemKind` but not "map" part
  if H.kind_map == nil then
    -- Cache kind map so as to not recompute it each time (as it will be called
    -- in performance sensitive context). Assumes `tweak_lsp_kind()` is called
    -- right after `require('mini.icons').setup()`.
    H.kind_map = {}
    for k, v in pairs(vim.lsp.protocol.CompletionItemKind) do
      if type(k) == 'string' and type(v) == 'number' then H.kind_map[v] = k end
    end
  end

  return function(item)
    local _, hl, is_default = _G.MiniIcons.get('lsp', H.kind_map[item.kind] or 'Unknown')
    item.kind_hlgroup = not is_default and hl or nil
  end
end

H.get_completion_word = function(item)
  return H.table_get(item, { 'textEdit', 'newText' }) or item.insertText or item.label or ''
end

H.make_lsp_extra_actions = function(lsp_data)
  -- Prefer resolved item over the one from 'textDocument/completion'
  local resolved = (H.info.lsp.result or {})[lsp_data.client_id]
  local item = (resolved == nil or resolved.err) and lsp_data.completion_item or resolved.result

  if item.additionalTextEdits == nil and not lsp_data.needs_snippet_insert then return end
  local snippet = lsp_data.needs_snippet_insert and H.get_completion_word(item) or nil

  -- Make extra actions not only after an explicit `<C-y>` (accept completed
  -- item), but also after implicit non-keyword character. This needs:
  -- - Keeping track of newly added non-keyword character and cursor move (like
  --   after 'mini.pairs') for a later undo. Do it via using expanding extmark.
  -- - Delay actual execution to operate *after* characters are inserted (as it
  --   is not immediate). Otherwise those characters will get "inserted" after
  --   snippet is inserted and its session is active.
  local cur = vim.api.nvim_win_get_cursor(0)
  local extmark_opts = { end_row = cur[1] - 1, end_col = cur[2], right_gravity = false, end_right_gravity = true }
  local track_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur[1] - 1, cur[2], extmark_opts)

  vim.schedule(function()
    -- Do nothing if user exited Insert mode
    if vim.fn.mode() ~= 'i' then return end

    -- Undo possible non-keyword character(s) and cursor move. Do this before
    -- text edits to have more proper state (as it was at the time edits were
    -- created by server), but only if there is snippet (keep new characters
    -- for *only* text edits).
    if snippet ~= nil then
      local ok, new = pcall(vim.api.nvim_buf_get_extmark_by_id, 0, H.ns_id, track_id, { details = true })
      if ok then vim.api.nvim_buf_set_text(0, new[1], new[2], new[3].end_row, new[3].end_col, {}) end
      pcall(vim.api.nvim_win_set_cursor, 0, cur)
    end

    -- Try to apply additional text edits
    H.apply_additional_text_edits(item.additionalTextEdits, lsp_data.client_id)

    -- Expand snippet: remove inserted word and instead insert snippet
    if snippet == nil then return end
    local from, to = H.completion.start_pos, vim.api.nvim_win_get_cursor(0)
    pcall(vim.api.nvim_buf_set_text, 0, from[1] - 1, from[2], to[1] - 1, to[2], { '' })
    local insert = H.get_config().lsp_completion.snippet_insert or MiniCompletion.default_snippet_insert
    insert(snippet)
  end)
end

H.apply_additional_text_edits = function(edits, client_id)
  -- Code originally inspired by https://github.com/neovim/neovim/issues/12310
  if edits == nil then return end
  client_id = client_id or 0

  -- Prepare extmarks to track relevant positions after text edits
  local start_pos = H.completion.start_pos
  local start_extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, start_pos[1] - 1, start_pos[2], {})

  local cur_pos = vim.api.nvim_win_get_cursor(0)
  -- - Keep track of start-cursor range as not "expanding"
  local cursor_extmark_opts = { right_gravity = false }
  local cursor_extmark_id = vim.api.nvim_buf_set_extmark(0, H.ns_id, cur_pos[1] - 1, cur_pos[2], cursor_extmark_opts)

  -- Do text edits
  local offset_encoding = vim.lsp.get_client_by_id(client_id).offset_encoding
  vim.lsp.util.apply_text_edits(edits, vim.api.nvim_get_current_buf(), offset_encoding)

  -- Restore relevant positions
  local start_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, start_extmark_id, {})
  H.completion.start_pos = { start_data[1] + 1, start_data[2] }
  pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, start_extmark_id)

  local cursor_data = vim.api.nvim_buf_get_extmark_by_id(0, H.ns_id, cursor_extmark_id, {})
  pcall(vim.api.nvim_win_set_cursor, 0, { cursor_data[1] + 1, cursor_data[2] })
  pcall(vim.api.nvim_buf_del_extmark, 0, H.ns_id, cursor_extmark_id)
end

-- Completion item info -------------------------------------------------------
H.show_info_window = function()
  local event = H.info.event
  if not event then return end

  -- Get info lines to show
  local lines = H.info_window_lines(H.info.id)
  if lines == nil or H.is_whitespace(lines) then return end

  -- Ensure permanent buffer with "markdown" highlighting to display info
  H.ensure_buffer(H.info, 'MiniCompletion:completion-item-info')
  H.ensure_highlight(H.info, 'markdown')
  vim.api.nvim_buf_set_lines(H.info.bufnr, 0, -1, false, lines)

  -- Compute floating window options
  local opts = H.info_window_options()
  -- Adjust to hide top/bottom code block delimiters (as they are concealed)
  local top_is_codeblock_start = lines[1]:find('^```%S*$')
  if top_is_codeblock_start then opts.height = opts.height - 1 end
  if lines[#lines]:find('^```$') then opts.height = opts.height - 1 end

  -- Adjust section separator with better visual alternative
  lines = vim.tbl_map(function(l) return l:gsub('^%-%-%-%-*$', string.rep('─', opts.width)) end, lines)
  vim.api.nvim_buf_set_lines(H.info.bufnr, 0, -1, false, lines)

  -- Defer execution because of textlock during `CompleteChanged` event
  vim.schedule(function()
    -- Ensure that window doesn't open when it shouldn't be
    if not (H.pumvisible() and vim.fn.mode() == 'i') then return end
    H.open_action_window(H.info, opts)
    local win_id = H.info.win_id
    if not H.is_valid_win(win_id) then return end

    -- Hide helper syntax elements (like ``` code blocks, etc.)
    vim.wo[H.info.win_id].conceallevel = 3

    -- Scroll past first line if it is a start of a code block
    if top_is_codeblock_start then vim.api.nvim_win_call(win_id, function() vim.fn.winrestview({ topline = 2 }) end) end
  end)
end

H.info_window_lines = function(info_id)
  local completed_item = H.table_get(H.info, { 'event', 'completed_item' }) or {}
  local lsp_data = H.table_get(completed_item, { 'user_data', 'nvim', 'lsp' })
  local info = completed_item.info or ''

  -- If popup is not from LSP, try using 'info' field of completion item
  if lsp_data == nil then return vim.split(info, '\n') end

  -- Try to get documentation from LSP's latest resolved info
  if H.info.lsp.status == 'received' then
    local lines = H.process_lsp_response(H.info.lsp.result, function(x) return H.normalize_item_doc(x, info) end)
    H.info.lsp.status = 'done'
    return lines
  end

  -- If server doesn't support resolving completion item, reuse first response
  local client = vim.lsp.get_client_by_id(lsp_data.client_id) or {}
  local can_resolve = H.table_get(client.server_capabilities, { 'completionProvider', 'resolveProvider' })
  if not can_resolve then return H.normalize_item_doc(lsp_data.completion_item, info) end

  -- Finally, request to resolve current completion to add more documentation
  local bufnr = vim.api.nvim_get_current_buf()
  local current_id = H.info.lsp.id + 1
  H.info.lsp.id = current_id
  H.info.lsp.status = 'sent'

  local cancel_fun = vim.lsp.buf_request_all(bufnr, 'completionItem/resolve', lsp_data.completion_item, function(result)
    -- Don't do anything if there is other LSP request in action
    if not H.is_lsp_current(H.info, current_id) then return end

    H.info.lsp.status = 'received'

    -- Don't do anything if completion item was changed
    if H.info.id ~= info_id then return end

    H.info.lsp.result = result
    H.show_info_window()
  end)

  H.info.lsp.cancel_fun = cancel_fun
end

H.info_window_options = function()
  local win_config = H.get_config().window.info

  -- Compute dimensions based on lines to be displayed
  local lines = vim.api.nvim_buf_get_lines(H.info.bufnr, 0, -1, false)
  local info_height, info_width = H.floating_dimensions(lines, win_config.height, win_config.width)

  -- Compute position
  local event = H.info.event
  local left_to_pum = event.col - 1
  local right_to_pum = event.col + event.width + (event.scrollbar and 1 or 0)

  local border_offset = win_config.border == 'none' and 0 or 2
  local space_left = left_to_pum - border_offset
  local space_right = vim.o.columns - right_to_pum - border_offset

  -- Decide side at which info window will be displayed
  local anchor, col, space
  if info_width <= space_right or space_left <= space_right then
    anchor, col, space = 'NW', right_to_pum, space_right
  else
    anchor, col, space = 'NE', left_to_pum, space_left
  end

  -- Possibly adjust floating window dimensions to fit screen
  if space < info_width then
    info_height, info_width = H.floating_dimensions(lines, win_config.height, space)
  end

  local title = vim.fn.has('nvim-0.9') == 1 and H.fit_to_width(' Info ', info_width) or nil
  return {
    relative = 'editor',
    anchor = anchor,
    row = event.row,
    col = col,
    width = info_width,
    height = info_height,
    focusable = false,
    style = 'minimal',
    border = win_config.border,
    title = title,
  }
end

-- Signature help -------------------------------------------------------------
H.show_signature_window = function()
  -- If there is no received LSP result, make request and exit
  if H.signature.lsp.status ~= 'received' then
    local current_id = H.signature.lsp.id + 1
    H.signature.lsp.id = current_id
    H.signature.lsp.status = 'sent'

    local bufnr = vim.api.nvim_get_current_buf()
    local params = H.make_position_params()

    local cancel_fun = vim.lsp.buf_request_all(bufnr, 'textDocument/signatureHelp', params, function(result)
      if not H.is_lsp_current(H.signature, current_id) then return end

      H.signature.lsp.status = 'received'
      H.signature.lsp.result = result

      -- Trigger `show_signature` again to take 'received' route
      H.show_signature_window()
    end)

    -- Cache cancel function to disable requests when they are not needed
    H.signature.lsp.cancel_fun = cancel_fun

    return
  end

  -- Make lines to show in floating window
  local lines, hl_ranges = H.signature_window_lines()
  H.signature.lsp.status = 'done'

  -- Close window and exit if there is nothing to show
  if not lines or H.is_whitespace(lines) then
    H.close_action_window(H.signature)
    return
  end

  -- Ensure permanent buffer with current highlighting to display signature
  H.ensure_buffer(H.signature, 'MiniCompletion:signature-help')
  H.ensure_highlight(H.signature, vim.bo.filetype)
  vim.api.nvim_buf_set_lines(H.signature.bufnr, 0, -1, false, lines)

  -- Add highlighting of active parameter
  local buf_id = H.signature.bufnr
  for i, hl_range in ipairs(hl_ranges) do
    if not vim.tbl_isempty(hl_range) and hl_range.first and hl_range.last then
      local first, last = hl_range.first, hl_range.last
      vim.api.nvim_buf_add_highlight(buf_id, H.ns_id, 'MiniCompletionActiveParameter', i - 1, first, last)
    end
  end

  -- If window is already opened and displays the same text, don't reopen it
  local cur_text = table.concat(lines, '\n')
  if H.signature.win_id and cur_text == H.signature.text then return end

  -- Cache lines for later checks if window should be reopened
  H.signature.text = cur_text

  -- Ensure window is closed
  H.close_action_window(H.signature)

  -- Compute floating window options
  local opts = H.signature_window_opts()

  -- Ensure that window doesn't open when it shouldn't
  if vim.fn.mode() == 'i' then H.open_action_window(H.signature, opts) end
end

H.signature_window_lines = function()
  local signature_data = H.process_lsp_response(H.signature.lsp.result, H.process_signature_response)
  -- Each line is a single-line active signature string from one attached LSP
  -- client. Each highlight range is a table which indicates (if not empty)
  -- what parameter to highlight for every LSP client's signature string.
  local lines, hl_ranges = {}, {}
  for _, t in pairs(signature_data) do
    -- `t` is allowed to be an empty table (in which case nothing is added) or
    -- a table with two entries. This ensures that `hl_range`'s integer index
    -- points to an actual line in future buffer.
    table.insert(lines, t.label)
    table.insert(hl_ranges, t.hl_range)
  end

  return lines, hl_ranges
end

H.process_signature_response = function(response)
  if not response.signatures or vim.tbl_isempty(response.signatures) then return {} end

  -- Get active signature (based on textDocument/signatureHelp specification)
  local signature_id = response.activeSignature or 0
  -- This is according to specification: "If ... value lies outside ...
  -- defaults to zero"
  local n_signatures = vim.tbl_count(response.signatures or {})
  if signature_id < 0 or signature_id >= n_signatures then signature_id = 0 end
  local signature = response.signatures[signature_id + 1]

  -- Get displayed signature label
  local signature_label = signature.label:gsub('\n', ' ')

  -- Get start and end of active parameter (for highlighting)
  local hl_range = {}
  local n_params = vim.tbl_count(signature.parameters or {})
  local has_params = signature.parameters and n_params > 0

  -- Take values in this order because data inside signature takes priority
  local parameter_id = signature.activeParameter or response.activeParameter or 0
  local param_id_inrange = 0 <= parameter_id and parameter_id < n_params

  -- Computing active parameter only when parameter id is inside bounds is not
  -- strictly based on specification, as currently (v3.16) it says to treat
  -- out-of-bounds value as first parameter. However, some clients seem to use
  -- those values to indicate that nothing needs to be highlighted.
  -- Sources:
  -- https://github.com/microsoft/pyright/pull/1876
  -- https://github.com/microsoft/language-server-protocol/issues/1271
  if has_params and param_id_inrange then
    local param_label = signature.parameters[parameter_id + 1].label

    -- Compute highlight range based on type of supplied parameter label: can
    -- be string label which should be a part of signature label or direct start
    -- (inclusive) and end (exclusive) range values
    local first, last = nil, nil
    if type(param_label) == 'string' then
      first, last = signature_label:find(vim.pesc(param_label))
      -- Make zero-indexed and end-exclusive
      if first then first = first - 1 end
    elseif type(param_label) == 'table' then
      first, last = unpack(param_label)
    end
    if first then hl_range = { first = first, last = last } end
  end

  -- Return nested table because this will be a second argument of
  -- `vim.list_extend()` and the whole inner table is a target value here.
  return { { label = signature_label, hl_range = hl_range } }
end

H.signature_window_opts = function()
  local win_config = H.get_config().window.signature
  local lines = vim.api.nvim_buf_get_lines(H.signature.bufnr, 0, -1, false)
  local height, width = H.floating_dimensions(lines, win_config.height, win_config.width)

  -- Compute position
  local win_line = vim.fn.winline()
  local border_offset = win_config.border == 'none' and 0 or 2
  local space_above = win_line - 1 - border_offset
  local space_below = vim.api.nvim_win_get_height(0) - win_line - border_offset

  local anchor, row, space
  if height <= space_above or space_below <= space_above then
    anchor, row, space = 'SW', 0, space_above
  else
    anchor, row, space = 'NW', 1, space_below
  end

  -- Possibly adjust floating window dimensions to fit screen
  if space < height then
    height, width = H.floating_dimensions(lines, space, win_config.width)
  end

  -- Get zero-indexed current cursor position
  local bufpos = vim.api.nvim_win_get_cursor(0)
  bufpos[1] = bufpos[1] - 1

  local title = vim.fn.has('nvim-0.9') == 1 and H.fit_to_width(' Signature ', width) or nil
  return {
    relative = 'win',
    bufpos = bufpos,
    anchor = anchor,
    row = row,
    col = 0,
    width = width,
    height = height,
    focusable = false,
    style = 'minimal',
    border = win_config.border,
    title = title,
  }
end

-- Helpers for floating windows -----------------------------------------------
H.ensure_buffer = function(cache, name)
  if H.is_valid_buf(cache.bufnr) then return end

  local buf_id = vim.api.nvim_create_buf(false, true)
  cache.bufnr = buf_id
  vim.api.nvim_buf_set_name(buf_id, name)
  vim.bo[buf_id].buftype = 'nofile'
end

H.ensure_highlight = function(cache, filetype)
  if cache.hl_filetype == filetype then return end
  cache.hl_filetype = filetype
  local buf_id = cache.bufnr

  local has_lang, lang = pcall(vim.treesitter.language.get_lang, filetype)
  lang = has_lang and lang or filetype
  -- TODO: Remove `opts.error` after compatibility with Neovim=0.11 is dropped
  local has_parser, parser = pcall(vim.treesitter.get_parser, buf_id, lang, { error = false })
  has_parser = has_parser and parser ~= nil
  if has_parser then has_parser = pcall(vim.treesitter.start, buf_id, lang) end
  if not has_parser then vim.bo[buf_id].syntax = filetype end
end

-- Returns tuple of height and width
H.floating_dimensions = function(lines, max_height, max_width)
  max_height, max_width = math.max(max_height, 1), math.max(max_width, 1)

  -- Simulate how lines will look in window with `wrap` and `linebreak`.
  -- This is not 100% accurate (mostly because of concealed characters and
  -- multibyte manifest into empty space at bottom), but does the job
  local lines_wrap = {}
  for _, l in pairs(lines) do
    vim.list_extend(lines_wrap, H.wrap_line(l, max_width))
  end
  -- Height is a number of wrapped lines truncated to maximum height
  local height = math.min(#lines_wrap, max_height)

  -- Width is a maximum width of the first `height` wrapped lines truncated to
  -- maximum width
  local width = 0
  local l_width
  for i, l in ipairs(lines_wrap) do
    -- Use `strdisplaywidth()` to account for 'non-UTF8' characters
    l_width = vim.fn.strdisplaywidth(l)
    if i <= height and width < l_width then width = l_width end
  end
  -- It should already be less that that because of wrapping, so this is "just
  -- in case"
  width = math.min(width, max_width)

  return math.max(height, 1), math.max(width, 1)
end

H.open_action_window = function(cache, opts)
  local win_id = vim.api.nvim_open_win(cache.bufnr, false, opts)
  vim.wo[win_id].breakindent = false
  vim.wo[win_id].foldenable = false
  vim.wo[win_id].foldmethod = 'manual'
  vim.wo[win_id].linebreak = true
  vim.wo[win_id].wrap = true
  cache.win_id = win_id
end

H.close_action_window = function(cache, keep_timer)
  if not keep_timer then cache.timer:stop() end

  if H.is_valid_win(cache.win_id) then vim.api.nvim_win_close(cache.win_id, true) end
  cache.win_id = nil

  -- For some reason 'buftype' might be reset. Ensure that buffer is scratch.
  if H.is_valid_buf(cache.bufnr) then vim.bo[cache.bufnr].buftype = 'nofile' end
end

-- Utilities ------------------------------------------------------------------
H.error = function(msg) error('(mini.completion) ' .. msg, 0) end

H.check_type = function(name, val, ref, allow_nil)
  if type(val) == ref or (ref == 'callable' and vim.is_callable(val)) or (allow_nil and val == nil) then return end
  H.error(string.format('`%s` should be %s, not %s', name, ref, type(val)))
end

H.is_valid_buf = function(buf_id) return type(buf_id) == 'number' and vim.api.nvim_buf_is_valid(buf_id) end

H.is_valid_win = function(win_id) return type(win_id) == 'number' and vim.api.nvim_win_is_valid(win_id) end

H.is_char_keyword = function(char)
  -- Using Vim's `match()` and `keyword` enables respecting Cyrillic letters
  return vim.fn.match(char, '[[:keyword:]]') >= 0
end

-- NOTE: Might return `true` even if there is no visible completion popup, but
-- built-in completion is still "active" (`<BS>` will show previous completion
-- immediately).
H.pumvisible = function() return vim.fn.pumvisible() > 0 end

H.get_completion_start = function(lsp_result)
  -- Prefer completion start from LSP response(s)
  for _, response_data in pairs(lsp_result or {}) do
    local server_start = H.get_completion_start_server(response_data)
    if server_start ~= nil then return server_start end
  end

  -- Fall back to start position of latest keyword
  local pos = vim.api.nvim_win_get_cursor(0)
  local line = vim.api.nvim_get_current_line()
  return { pos[1], vim.fn.match(line:sub(1, pos[2]), '\\k*$') }
end

H.get_completion_start_server = function(response_data, line_num)
  if response_data.err or type(response_data.result) ~= 'table' then return end
  local items = response_data.result.items or response_data.result
  for _, item in pairs(items) do
    if type(item.textEdit) == 'table' then
      -- NOTE: As per LSP spec, `textEdit` can be either `TextEdit` or `InsertReplaceEdit`
      local range = type(item.textEdit.range) == 'table' and item.textEdit.range or item.textEdit.insert
      -- NOTE: Return immediately, ignoring possibly several conflicting starts
      return { range.start.line + 1, range.start.character }
    end
  end
end

H.is_whitespace = function(s)
  if type(s) == 'string' then return s:find('^%s*$') end
  if type(s) == 'table' then
    for _, val in pairs(s) do
      if not H.is_whitespace(val) then return false end
    end
    return true
  end
  return false
end

H.fit_to_width = function(text, width)
  local t_width = vim.fn.strchars(text)
  return t_width <= width and text or ('…' .. vim.fn.strcharpart(text, t_width - width + 1, width - 1))
end

-- Simulate splitting single line `l` like how it would look inside window with
-- `wrap` and `linebreak` set to `true`
H.wrap_line = function(l, width)
  local res = {}

  local success, width_id = true, nil
  -- Use `strdisplaywidth()` to account for multibyte characters
  while success and vim.fn.strdisplaywidth(l) > width do
    -- Simulate wrap by looking at breaking character from end of current break
    -- Use `pcall()` to handle complicated multibyte characters (like Chinese)
    -- for which even `strdisplaywidth()` seems to return incorrect values.
    success, width_id = pcall(vim.str_byteindex, l, width)

    if success then
      local break_match = vim.fn.match(l:sub(1, width_id):reverse(), '[- \t.,;:!?]')
      -- If no breaking character found, wrap at whole width
      local break_id = width_id - (break_match < 0 and 0 or break_match)
      table.insert(res, l:sub(1, break_id))
      l = l:sub(break_id + 1)
    end
  end
  table.insert(res, l)

  return res
end

H.table_get = function(t, id)
  if type(id) ~= 'table' then return H.table_get(t, { id }) end
  local success, res = true, t
  for _, i in ipairs(id) do
    --stylua: ignore start
    success, res = pcall(function() return res[i] end)
    if not success or res == nil then return end
    --stylua: ignore end
  end
  return res
end

H.get_left_char = function()
  local line = vim.api.nvim_get_current_line()
  local col = vim.api.nvim_win_get_cursor(0)[2]

  return string.sub(line, col, col)
end

H.map = function(mode, lhs, rhs, opts)
  if lhs == '' then return end
  opts = vim.tbl_deep_extend('force', { silent = true }, opts or {})
  vim.keymap.set(mode, lhs, rhs, opts)
end

H.normalize_item_doc = function(completion_item, fallback_info)
  local detail, doc = completion_item.detail, completion_item.documentation
  -- Fall back to explicit info only of there is no data in completion item
  -- Assume that explicit info is a code that needs highlighting
  detail = (detail == nil and doc == nil) and fallback_info or detail
  if detail == nil and doc == nil then return {} end

  -- Extract string content. Treat markdown and plain kinds the same.
  -- Show both `detail` and `documentation` if the first provides new info.
  detail, doc = detail or '', (type(doc) == 'table' and doc.value or doc) or ''
  -- Wrap details in language's code block to (usually) improve highlighting
  -- This approach seems to work in 'hrsh7th/nvim-cmp'
  detail = (H.is_whitespace(detail) or doc:find(detail, 1, true) ~= nil) and '' or (H.wrap_in_codeblock(detail) .. '\n')
  local text = detail .. doc

  -- Ensure consistent line separators
  text = text:gsub('\r\n?', '\n')
  -- Remove trailing whitespace (converts blank lines to empty)
  text = text:gsub('[ \t]+\n', '\n'):gsub('[ \t]+$', '\n')
  -- Collapse multiple empty lines, remove top and bottom padding
  text = text:gsub('\n\n+', '\n\n'):gsub('^\n+', ''):gsub('\n+$', '')
  -- Remove padding around code blocks as they are concealed and appear empty
  text = text:gsub('\n*(\n```%S+\n)', '%1'):gsub('(\n```\n?)\n*', '%1')

  if text == '' and fallback_info ~= '' then text = H.wrap_in_codeblock(fallback_info) end
  return text == '' and {} or vim.split(text, '\n')
end

H.wrap_in_codeblock = function(x) return string.format('```%s\n%s\n```', vim.bo.filetype:match('^[^%.]*'), vim.trim(x)) end

-- TODO: Remove after compatibility with Neovim=0.9 is dropped
H.islist = vim.fn.has('nvim-0.10') == 1 and vim.islist or vim.tbl_islist

H.get_buf_lsp_clients = function() return vim.lsp.get_clients({ bufnr = 0 }) end
if vim.fn.has('nvim-0.10') == 0 then H.get_buf_lsp_clients = function() return vim.lsp.buf_get_clients() end end

-- TODO: Remove after compatibility with Neovim=0.10 is dropped
H.make_position_params = function() return vim.lsp.util.make_position_params() end
if vim.fn.has('nvim-0.11') == 1 then
  -- Use callable `params` to workaround mandatory non-nil `offset_encoding` in
  -- `vim.lsp.util.make_position_params()` on Neovim>=0.11
  H.make_position_params = function()
    return function(client, _) return vim.lsp.util.make_position_params(0, client.offset_encoding) end
  end
end

return MiniCompletion