--- mod-version:3
--
-- LSP client for lite-xl
-- @copyright Jefferson Gonzalez
-- @license MIT
--
-- Note: Annotations syntax documentation which is supported by
-- https://github.com/sumneko/lua-language-server can be read here:
-- https://emmylua.github.io/annotation.html

-- TODO Change the code to make it possible to use more than one LSP server
-- for a single file if possible and needed, for eg:
--   One lsp may not support goto definition but another one registered
--   for the current document filetype may do.

local core = require "core"
local common = require "core.common"
local config = require "core.config"
local command = require "core.command"
local style = require "core.style"
local keymap = require "core.keymap"
local translate = require "core.doc.translate"
local autocomplete = require "plugins.autocomplete"
local Doc = require "core.doc"
local DocView = require "core.docview"
local StatusView = require "core.statusview"
local RootView = require "core.rootview"
local LineWrapping
-- If the lsp plugin is loaded from users init.lua it will load linewrapping
-- even if it was disabled from the settings ui, so we queue this check since
-- there is no way to automatically load settings ui before the user module.
core.add_thread(function()
  if config.plugins.linewrapping or type(config.plugins.linewrapping) == "nil" then
    LineWrapping = require "plugins.linewrapping"
  end
end)

local json = require "plugins.lsp.json"
local util = require "plugins.lsp.util"
local listbox = require "plugins.lsp.listbox"
local diagnostics = require "plugins.lsp.diagnostics"
local Server = require "plugins.lsp.server"
local Timer = require "plugins.lsp.timer"
local SymbolResults = require "plugins.lsp.symbolresults"
local MessageBox = require "libraries.widget.messagebox"
local snippets_found, snippets = pcall(require, "plugins.snippets")

---@type lsp.helpdoc
local HelpDoc = require "plugins.lsp.helpdoc"

--
-- Plugin settings
--

---Configuration options for the LSP plugin.
---@class config.plugins.lsp
---Set to a file path to log all json
---@field log_file string
---Setting to true prettyfies json for more readability on the log
---but this setting will impact performance so only enable it when
---in need of easy to read json output when developing the plugin.
---@field prettify_json boolean
---Show a symbol hover information when mouse cursor is on top.
---@field mouse_hover boolean
---The amount of time in milliseconds before showing the tooltip.
---@field mouse_hover_delay integer
---Show diagnostic messages
---@field show_diagnostics boolean
---Amount of milliseconds to delay updating the inline diagnostics.
---@field diagnostics_delay number
---Wether to enable snippets processing.
---@field snippets boolean
---Stop servers that aren't needed by any of the open files
---@field stop_unneeded_servers boolean
---Send a server stderr output to lite log
---@field log_server_stderr boolean
---Force verbosity off even if a server is configured with verbosity on
---@field force_verbosity_off boolean
---Yield when reading from LSP which may give you better UI responsiveness
---when receiving large responses, but will affect LSP performance.
---@field more_yielding boolean
config.plugins.lsp = common.merge({
  mouse_hover = true,
  mouse_hover_delay = 300,
  show_diagnostics = true,
  diagnostics_delay = 500,
  snippets = true,
  stop_unneeded_servers = true,
  log_file = "",
  prettify_json = false,
  log_server_stderr = false,
  force_verbosity_off = false,
  more_yielding = false,
  autostart_server = true,
  -- The config specification used by the settings gui
  config_spec = {
    name = "Language Server Protocol",
    {
      label = "Mouse Hover",
      description = "Show a symbol hover information when mouse cursor is on top.",
      path = "mouse_hover",
      type = "TOGGLE",
      default = true
    },
    {
      label = "Mouse Hover Delay",
      description = "The amount of time in milliseconds before showing the tooltip.",
      path = "mouse_hover_delay",
      type = "NUMBER",
      default = 300,
      min = 50,
      max = 2000
    },
    {
      label = "Diagnostics",
      description = "Show inline diagnostic messages with lint+.",
      path = "show_diagnostics",
      type = "TOGGLE",
      default = false
    },
    {
      label = "Diagnostics Delay",
      description = "Amount of milliseconds to delay the update of inline diagnostics.",
      path = "diagnostics_delay",
      type = "NUMBER",
      default = 500,
      min = 100,
      max = 10000
    },
    {
      label = "Snippets",
      description = "Snippets processing using lsp_snippets, may need a restart.",
      path = "snippets",
      type = "TOGGLE",
      default = true
    },
    {
      label = "Autostart Server",
      description = "Automatically start server when opening a file",
      path = "autostart_server",
      type = "TOGGLE",
      default = true
    },
    {
      label = "Stop Servers",
      description = "Stop servers that aren't needed by any of the open files.",
      path = "stop_unneeded_servers",
      type = "TOGGLE",
      default = true
    },
    {
      label = "Log File",
      description = "Absolute path to a '.log' file for logging all json.",
      path = "log_file",
      type = "FILE",
      filters = {"%.log$"}
    },
    {
      label = "Prettify JSON",
      description = "Prettify json for more readability but impacts performance.",
      path = "prettify_json",
      type = "TOGGLE",
      default = false
    },
    {
      label = "Log Standard Error",
      description = "Send a server stderr output to lite log.",
      path = "log_server_stderr",
      type = "TOGGLE",
      default = false
    },
    {
      label = "Force Verbosity Off",
      description = "Turn verbosity off even if a server is configured with verbosity on.",
      path = "force_verbosity_off",
      type = "TOGGLE",
      default = false
    },
    {
      label = "More Yielding",
      description = "Yield when reading from LSP which may give you better UI responsiveness.",
      path = "more_yielding",
      type = "TOGGLE",
      default = false
    }
  }
}, config.plugins.lsp)


--
-- Main plugin functionality
--
local lsp = {}

---List of registered servers
---@type table<string, lsp.server.options>
lsp.servers = {}

---List of running servers
---@type table<string, lsp.server>
lsp.servers_running = {}

---Flag that indicates if last autocomplete request was a trigger
---to prevent requesting another autocompletion request until the
---autocomplete box is hidden since some lsp servers loose context
---and return wrong results (eg: lua-language-server)
---@type boolean
lsp.in_trigger = false

---Flag that indicates if the user typed something on the editor to try and
---call autocomplete only when neccesary.
---@type boolean
lsp.user_typed = false

---Used on the hover timer to display hover info
---@class lsp.hover_position
---@field doc core.doc | nil
---@field x number
---@field y number
---@field triggered boolean
---@field utf8_range table | nil
lsp.hover_position = {doc = nil, x = -1, y = -1, triggered = false, utf8_range = nil}

---@type lsp.timer
lsp.hover_timer = Timer(300, true)
lsp.hover_timer.on_timer = function()
  local doc, line, col = lsp.get_hovered_location(lsp.hover_position.x, lsp.hover_position.y)
  if not doc then return end
  lsp.hover_position.triggered = true
  lsp.hover_position.utf8_range = nil
  lsp.hover_position.doc = doc
  lsp.request_hover(doc, line, col)
end

--
-- Private functions
--

---Generate an lsp location object
---@param doc core.doc
---@param line integer
---@param col integer
local function get_buffer_position_params(doc, line, col)
  return {
    textDocument = {
      uri = util.touri(core.project_absolute_path(doc.filename)),
    },
    position = {
      line = line - 1,
      character = util.doc_utf8_to_utf16(doc, line, col) - 1
    }
  }
end

---Recursive function to generate a list of symbols ready
---to use for the lsp.request_document_symbols() action.
---@param list table<integer, table>
---@param parent? string
local function get_symbol_lists(list, parent)
  local symbols = {}
  local symbol_names = {}
  parent = parent or ""
  parent = #parent > 0 and (parent .. "/") or parent

  for _, symbol in pairs(list) do
    -- Include symbol kind to be able to filter by it
    local symbol_name = parent
      .. symbol.name
      .. "||" .. Server.get_symbol_kind(symbol.kind)

    table.insert(symbol_names, symbol_name)

    symbols[symbol_name] = { kind = symbol.kind }

    if symbol.location then
      symbols[symbol_name].location = symbol.location
    else
      if symbol.range then
        symbols[symbol_name].range = symbol.range
      end
      if symbol.uri then
        symbols[symbol_name].uri = symbol.uri
      end
    end

    if symbol.children and #symbol.children > 0 then
      local child_symbols, child_names = get_symbol_lists(
        symbol.children, parent .. symbol.name
      )

      for _, name in pairs(child_names) do
        table.insert(symbol_names, name)
        symbols[name] = child_symbols[name]
      end
    end
  end

  return symbols, symbol_names
end

local function log(server, message, ...)
  if server.verbose then
    core.log("["..server.name.."] " .. message, ...)
  else
    core.log_quiet("["..server.name.."] " .. message, ...)
  end
end

---Check if active view is a DocView and return it
---@return core.docview|nil
local function get_active_docview()
  local av = core.active_view
  if getmetatable(av) == DocView and av.doc and av.doc.filename then
    return av
  end
  return nil
end

---Generates a code preview of a location
---@param location table
local function get_location_preview(location)
  local line1, col1 = util.toselection(
    location.range or location.targetRange
  )
  local filename = core.normalize_to_project_dir(
    util.tofilename(location.uri or location.targetUri)
  )
  local abs_filename = core.project_absolute_path(filename)

  local file = io.open(abs_filename)

  if not file then
    return "", filename .. ":" .. tostring(line1) .. ":" .. tostring(col1)
  end

  local preview = ""

  -- sometimes the lsp can send the location of a definition where the
  -- doc comments should be written but if no docs are written the line
  -- is empty and subsequent line is the one we are interested in.
  local line_count = 1
  for line in file:lines() do
    if line_count >= line1 then
      preview = line:gsub("^%s+", "")
        :gsub("%s+$", "")

      if preview ~= "" then
        break
      else
        -- change also the location table
        if location.range then
          location.range.start.line = location.range.start.line + 1
          location.range['end'].line = location.range['end'].line + 1
        elseif location.targetRange then
          location.targetRange.start.line = location.targetRange.start.line + 1
          location.targetRange['end'].line = location.targetRange['end'].line + 1
        end
      end
    end
    line_count = line_count + 1
  end
  file:close()

  local position = filename .. ":" .. tostring(line1) .. ":" .. tostring(col1)

  return preview, position
end

---Generate a list ready to use for the lsp.request_references() action.
---@param locations table
local function get_references_lists(locations)
  local references, reference_names = {}, {}

  for _, location in pairs(locations) do
    local preview, position = get_location_preview(location)
    local name = preview .. "||" .. position
    table.insert(reference_names, name)
    references[name] = location
  end

  return references, reference_names
end

---Apply an lsp textEdit to a document if possible.
---@param server lsp.server
---@param doc core.doc
---@param text_edit table
---@param is_snippet boolean
---@param update_cursor_position boolean
---@return boolean True on success
local function apply_edit(server, doc, text_edit, is_snippet, update_cursor_position)
  local range = nil

  if text_edit.range then
    range = text_edit.range
  elseif text_edit.insert then
    range = text_edit.insert
  elseif text_edit.replace then
    range = text_edit.replace
  end

  if not range then return false end

  local text = text_edit.newText
  local line1, col1, line2, col2
  local current_text = ""

  if
    not server.capabilities.positionEncoding
    or
    server.capabilities.positionEncoding == Server.position_encoding_kind.UTF16
  then
    line1, col1, line2, col2 = util.toselection(range, doc)
  else
    line1, col1, line2, col2 = util.toselection(range)
    core.error(
      "[LSP] Unsupported position encoding: ",
      server.capabilities.positionEncoding
    )
  end

  if lsp.in_trigger then
    local cline2, ccol2 = doc:get_selection()
    local cline1, ccol1 = doc:position_offset(line2, col2, translate.start_of_word)
    current_text = doc:get_text(cline1, ccol1, cline2, ccol2)
  end

  doc:remove(line1, col1, line2, col2+#current_text)

  if is_snippet and snippets_found and config.plugins.lsp.snippets then
    doc:set_selection(line1, col1, line1, col1)
    snippets.execute {format = 'lsp', template = text}
    return true
  end

  doc:insert(line1, col1, text)
  if update_cursor_position then
    doc:move_to_cursor(nil, #text)
  end

  return true
end

---Callback given to autocomplete plugin which is executed once for each
---element of the autocomplete box which is hovered with the idea of providing
---better description of the selected element by requesting the LSP server for
---detailed information/documentation.
---@param index integer
---@param item table
local function autocomplete_onhover(index, item)
  local completion_item = item.data.completion_item

  if item.data.server.verbose then
    item.data.server:log(
      "Resolve item: %s", util.jsonprettify(json.encode(completion_item))
    )
  end

  -- Only send resolve request if data field (which should contain
  -- the item id) is available.
  if completion_item.data then
    item.data.server:push_request('completionItem/resolve', {
      params = completion_item,
      callback = function(server, response)
        if response.result then
          local symbol = response.result
          if symbol.detail and #item.desc <= 0 then
            item.desc = symbol.detail
          end
          if symbol.documentation then
            if #item.desc > 0 then
              item.desc = item.desc .. "\n\n"
            end
            if
              type(symbol.documentation) == "table"
              and
              symbol.documentation.value
            then
              item.desc = item.desc .. symbol.documentation.value
              if
                symbol.documentation.kind
                and
                symbol.documentation.kind == "markdown"
              then
                item.desc = util.strip_markdown(item.desc)
              end
            else
              item.desc = item.desc .. symbol.documentation
            end
          end
          item.desc = item.desc:gsub("[%s\n]+$", "")
            :gsub("^[%s\n]+", "")
            :gsub("\n\n\n+", "\n\n")
          if symbol.additionalTextEdits then
            completion_item.additionalTextEdits = symbol.additionalTextEdits
          end

          if server.verbose then
            server:log(
              "Resolve response: %s", util.jsonprettify(json.encode(symbol))
            )
          end
        elseif server.verbose then
          server:log("Resolve returned empty response")
        end
      end
    })
  end
end

---Callback that handles insertion of an autocompletion item that has
---the information of insertion
---@param index integer
---@param item table
local function autocomplete_onselect(index, item)
  local completion = item.data.completion_item
  local dv = get_active_docview()
  local edit_applied = false
  if completion.textEdit then
    if dv then
      local is_snippet = completion.insertTextFormat
        and completion.insertTextFormat == Server.insert_text_format.Snippet
      edit_applied = apply_edit(item.data.server, dv.doc, completion.textEdit, is_snippet, true)
      if edit_applied then
        -- Retrigger code completion if last char is a trigger
        -- this is useful for example with clangd when autocompleting
        -- a #include, if user types < a list of paths will appear
        -- when selecting a path that ends with / as <AL/ the
        -- autocompletion will be retriggered to show a list of
        -- header files that belong to that directory.
        lsp.in_trigger = false
        local line, col = dv.doc:get_selection()
        local char = dv.doc:get_char(line, col-1)
        local char_prev = dv.doc:get_char(line, col-2)
        if char:match("%p") or (char == " " and char_prev:match("%p")) then
          if not util.table_empty(dv.doc.lsp_changes) then
            lsp.update_document(dv.doc, true)
          else
            lsp.request_completion(dv.doc, line, col, true)
          end
        end
      end
    end
  elseif
    dv and snippets_found and config.plugins.lsp.snippets
    and
    completion.insertText and completion.insertTextFormat
    and
    completion.insertTextFormat == Server.insert_text_format.Snippet
  then
    ---@type core.doc
    local doc = dv.doc
    if dv then
      local line2, col2 = doc:get_selection()
      local line1, col1 = doc:position_offset(line2, col2, translate.start_of_word)
      doc:set_selection(line1, col1, line2, col2)
      snippets.execute {format = 'lsp', template = completion.insertText}
      edit_applied = true
    end
  end
  if edit_applied and completion.additionalTextEdits and #completion.additionalTextEdits > 0 then
    -- TODO: do we need to sort this? Or is it expected to be already sorted?
    -- TODO: are the edit ranges considered as if the "main" textEdit was applied already?

    -- Apply the edits in reverse order, so that their ranges are not shifted
    -- around by previous edits
    for i=#completion.additionalTextEdits,1,-1 do
      local edit = completion.additionalTextEdits[i]
      apply_edit(item.data.server, dv.doc, edit, false, false)
    end
  end
  return edit_applied
end

--
-- Public functions
--

---Open a document location returned by LSP
---@param location table
function lsp.goto_location(location)
  local doc_view = core.root_view:open_doc(
    core.open_doc(
      common.home_expand(
        util.tofilename(location.uri or location.targetUri)
      )
    )
  )
  local line1, col1 = util.toselection(
    location.range or location.targetRange, doc_view.doc
  )
  doc_view.doc:set_selection(line1, col1, line1, col1)
end

lsp.get_location_preview = get_location_preview

---Register an LSP server to be launched on demand
---@param options lsp.server.options
function lsp.add_server(options)
  local required_fields = {
    "name", "language", "file_patterns", "command"
  }

  for _, field in pairs(required_fields) do
    if not options[field] then
      core.error(
        "[LSP] You need to provide a '%s' field for the server.",
        field
      )
      return false
    end
  end

  if snippets_found and config.plugins.lsp.snippets then
    options.snippets = true
  end

  if #options.command <= 0 then
    core.error("[LSP] Provide a command table list with the lsp command.")
    return false
  end

  -- On Windows using cmd.exe allows us to take advantage of its ability to run
  -- the correct executable, as well as running scripts.
  if PLATFORM == "Windows" and not options.windows_skip_cmd then
    local escaped_commands = { }
    if type(options.command) == "string" then
      options.command = { options.command }
    end
    -- We need to escape `"` as `"""`
    for _, v in ipairs(options.command) do
      table.insert(escaped_commands, '"' .. string.gsub(v, '"', '"""') .. '"')
    end
    -- The result should be something like `cmd.exe /C ""first" "second" "third""`
    options.command = 'cmd.exe /C "' .. table.concat(escaped_commands, " ") .. '"'
  end

  if config.plugins.lsp.force_verbosity_off then
    options.verbose = false
  end

  lsp.servers[options.name] = options

  return true
end

---Get valid running lsp servers for a given filename
---@param filename string
---@param initialized boolean
---@return table active_servers
function lsp.get_active_servers(filename, initialized)
  local servers = {}
  for name, server in pairs(lsp.servers) do
    if common.match_pattern(filename, server.file_patterns) then
      if lsp.servers_running[name] then
        local add_server = true
        if
          initialized
          and
          (
            not lsp.servers_running[name].initialized
            or
            not lsp.servers_running[name].capabilities
          )
        then
          add_server = false
        end
        if add_server then
          table.insert(servers, name)
        end
      end
    end
  end
  return servers
end

-- Used on lsp.get_workspace_settings()
local cached_workspace_settings = {}
local cached_workspace_settings_timestamp = 0

---Get table of configuration settings in the following way:
---1. Scan the USERDIR for .lite_lsp.lua or .lite_lsp.json (in that order)
---2. Merge server.settings
---4. Scan workspace if set also for .lite_lsp.lua/json and merge them or
---3. Scan server.path also for .lite_lsp.lua/json and merge them
---Note: settings are cached for 5 seconds for faster retrieval
---      on repetitive calls to this function.
---@param server lsp.server
---@param workspace? string
---@return table
function lsp.get_workspace_settings(server, workspace)
  -- Search settings on the following directories, subsequent settings
  -- overwrite the previous ones
  local paths = { USERDIR }
  local cached_index = USERDIR
  local settings = {}

  if not workspace and server.path then
    table.insert(paths, server.path)
    cached_index = cached_index .. tostring(server.path)
  elseif workspace then
    table.insert(paths, workspace)
    cached_index = cached_index .. tostring(workspace)
  end

  if
    cached_workspace_settings_timestamp > os.time()
    and
    cached_workspace_settings[cached_index]
  then
    return cached_workspace_settings[cached_index]
  else
    local position = 1
    for _, path in pairs(paths) do
      if path then
        local settings_new = nil
        path = path:gsub("\\+$", ""):gsub("/+$", "")
        if util.file_exists(path .. "/.lite_lsp.lua") then
          local settings_lua = dofile(path .. "/.lite_lsp.lua")
          if type(settings_lua) == "table" then
            settings_new = settings_lua
          end
        elseif util.file_exists(path .. "/.lite_lsp.json") then
          local file = io.open(path .. "/.lite_lsp.json", "r")
          if file then
            local settings_json = file:read("*a")
            settings_new = json.decode(settings_json)
            file:close()
          end
        end

        -- overwrite global settings by those specified in the server if any
        if position == 1 and server.settings then
          if settings_new then
            settings_new = util.deep_merge(settings_new, server.settings)
          else
            settings_new = server.settings
          end
        end

        -- overwrite previous settings with new ones
        if settings_new then
          settings = util.deep_merge(settings, settings_new)
        end
      end

      position = position + 1
    end

    -- store settings on cache for 5 seconds for fast repeated calls
    cached_workspace_settings[cached_index] = settings
    cached_workspace_settings_timestamp = os.time() + 5
  end

  return settings
end

-- TODO Update workspace folders of already running lsp servers if required
--- Start all applicable lsp servers for a given file.
--- @param filename string
--- @param project_directory string
function lsp.start_server(filename, project_directory)
  for name, server in pairs(lsp.servers) do
    if common.match_pattern(filename, server.file_patterns) then
      if not lsp.servers_running[name] then
        core.log("[LSP]: Starting " .. name)
        ---@type boolean, lsp.server
        local success, client = pcall(function() return Server(server) end)
        if not success then
          core.error("[LSP]: Unable to start %s:\nCommand: %s\nError: %s", name, common.serialize(server.command), client)
          goto continue
        end
        client.yield_on_reads = config.plugins.lsp.more_yielding

        lsp.servers_running[name] = client

        -- We overwrite the default log function to log messages on lite
        function client:log(message, ...)
          core.log_quiet(
            "[LSP/%s]: " .. message .. "\n",
            self.name,
            ...
          )
        end

        function client:on_shutdown()
          local sname = self.name
          core.log(
            "[LSP]: %s was shutdown, revise your configuration",
            sname
          )
          local last_shutdown = lsp.servers_running[sname].last_shutdown or 0
          lsp.servers_running = util.table_remove_key(
            lsp.servers_running,
            sname
          )
          if system.get_time() - last_shutdown >= 5 then
            lsp.start_servers()
            if lsp.servers_running[sname] then
              lsp.servers_running[sname].last_shutdown = system.get_time()
              core.log(
                "[LSP]: %s automatically restarted",
                sname
              )
            end
          end
        end

        -- Respond to workspace/configuration request
        client:add_request_listener(
          "workspace/configuration",
          function(server, request)
            local settings_default = lsp.get_workspace_settings(server)

            local settings_list = {}
            for _, item in pairs(request.params.items) do
              local value = nil
              if item.section then
                -- No workspace was specified so we return from default settings
                if not item.scopeUri then
                  value = util.table_get_field(settings_default, item.section)
                -- A workspace was specified so we return from that workspace
                else
                  local settings_workspace = lsp.get_workspace_settings(
                    server, util.tofilename(item.scopeUri)
                  )
                  value = util.table_get_field(settings_workspace, item.section)
                end

                if not value then
                  server:log("Asking for '%s' config but not set", item.section)
                else
                  server:log("Asking for '%s' config", item.section)
                end
              end

              table.insert(settings_list, value or json.null)
            end
            server:push_response(request.method, request.id, settings_list)
          end
        )

        -- Respond to window/showDocument request
        client:add_request_listener(
          "window/showDocument",
          function(server, request)
            if request.params.external then
              MessageBox.info(
                server.name .. " LSP Server",
                "Wants to externally open:\n'" .. request.params.uri .. "'",
                function(_, button_id)
                  if button_id == 1 then
                    util.open_external(request.params.uri)
                  end
                end,
                MessageBox.BUTTONS_YES_NO
              )
            else
              local document = util.tofilename(request.params.uri)
              ---@type core.docview
              local doc_view = core.root_view:open_doc(
                core.open_doc(common.home_expand(document))
              )
              if request.params.selection then
                local line1, col1, line2, col2 = util.toselection(
                  request.params.selection, doc_view.doc
                )
                doc_view.doc:set_selection(line1, col1, line2, col2)
              end
              if request.params.takeFocus then
                system.raise_window()
              end
            end

            server:push_response(request.method, request.id, {success=true})
          end
        )

        -- Display server messages on lite UI
        client:add_message_listener(
          "window/logMessage",
          function(server, params)
            if core.log then
              log(server, "%s", params.message)
            end
          end
        )

        -- Register/unregister diagnostic messages
        client:add_message_listener(
          "textDocument/publishDiagnostics",
          function(server, params)
            local abs_filename = util.tofilename(params.uri)
            local filename = core.normalize_to_project_dir(abs_filename)

            if server.verbose then
              core.log_quiet(
                "["..server.name.."] %s diagnostics for:  %s",
                filename,
                params.diagnostics and #params.diagnostics or 0
              )
            end

            if params.diagnostics and #params.diagnostics > 0 then
              local added = diagnostics.add(filename, params.diagnostics)

              if
                added and diagnostics.lintplus_found
                and
                config.plugins.lsp.show_diagnostics
                and
                util.doc_is_open(abs_filename)
              then
                -- we delay rendering of diagnostics for 2 seconds to prevent
                -- the constant reporting of errors while typing.
                diagnostics.lintplus_populate_delayed(filename)
              end
            else
              diagnostics.clear(filename)
              diagnostics.lintplus_clear_messages(filename)
            end
          end
        )

        -- Register/unregister diagnostic messages
        client:add_message_listener(
          "window/showMessage",
          function(server, params)
            local log_func = "log_quiet"
            if params.type == Server.message_type.Error then
              log_func = "error"
            elseif params.type == Server.message_type.Warning then
              log_func = "warn"
            elseif params.type == Server.message_type.Info then
              log_func = "log"
            elseif params.type == Server.message_type.Debug then
              log_func = "log_quiet"
            end
            core[log_func]("["..server.name.."] message: %s", params.message)
          end
        )

        -- Send settings table after initialization if available.
        client:add_event_listener("initialized", function(server)
          if config.plugins.lsp.force_verbosity_off then
            core.log_quiet("["..server.name.."] " .. "Initialized")
          else
            log(server, "Initialized")
          end
          local settings = lsp.get_workspace_settings(server)
          if not util.table_empty(settings) then
            server:push_notification("workspace/didChangeConfiguration", {
              params = {settings = settings}
            })
          end

          -- Send open document request if needed
          for _, docu in ipairs(core.docs) do
            if docu.filename then
              if common.match_pattern(docu.filename, server.file_patterns) then
                lsp.open_document(docu)
              end
            end
          end
        end)

        -- Start the server initialization process
        client:initialize(project_directory, "Lite XL", VERSION)
      end
    end
    ::continue::
  end
end

---Stops all running servers.
function lsp.stop_servers()
  for name, _ in pairs(lsp.servers) do
    if lsp.servers_running[name] then
       lsp.servers_running[name]:exit()
       core.log("[LSP] stopped %s", name)
       lsp.servers_running = util.table_remove_key(lsp.servers_running, name)
    end
  end
end

---Start only the needed servers by current opened documents.
function lsp.start_servers()
  for _, doc in ipairs(core.docs) do
    if doc.filename then
      lsp.start_server(doc.filename, core.project_dir)
    end
  end
end

---Returns the hovered doc and the hovered position.
---Returns nil if no doc with an LSP activated is under the provided coordinates.
---@param x number
---@param y number
---@return core.doc|nil doc
---@return integer|nil line
---@return integer|nil col
function lsp.get_hovered_location(x, y)
  local n = core.root_view.root_node:get_child_overlapping_point(x, y)
  if not n then return end
  local av = n.active_view
  if not av:extends(DocView) then return end
  if av and av.doc.lsp_open then
    ---@type core.doc
    local doc = av.doc
    local line, col = av:resolve_screen_position(x, y)
    local last_x = av:get_col_x_offset(line, #av.doc.lines[line])
    local lx, ly = av:get_line_screen_position(line)
    if x > last_x + lx or y > ly + av:get_line_height() then return end
    return doc, line, col
  end
end

---Send notification to applicable LSP servers that a document was opened
---@param doc core.doc
function lsp.open_document(doc)
  -- in some rare ocassions this function may return nil when the
  -- user closed lite-xl with files opened, removed the files from system
  -- and opens lite-xl again which loads the non existent files.
  local doc_path = core.project_absolute_path(doc.filename)
  local file_info = system.get_file_info(doc_path)
  if not file_info then
    core.error("[LSP] could not open: %s", tostring(doc.filename))
    return
  end

  local active_servers = lsp.get_active_servers(doc.filename, true)

  if #active_servers > 0 then
    doc.disable_symbols = true -- disable symbol parsing on autocomplete plugin
    for _, name in pairs(active_servers) do
      local server = lsp.servers_running[name]
      if server.capabilities.textDocumentSync.openClose then
        if server.exit_timer then
          server.exit_timer:stop()
          server.exit_timer = nil
        end
        if file_info.size / 1024 <= 50 then
          -- file size is in range so push the notification as usual.
          server:push_notification('textDocument/didOpen', {
            params = {
              textDocument = {
                uri = util.touri(doc_path),
                languageId = server:get_language_id(doc),
                version = doc.clean_change_id,
                text = table.concat(doc.lines)
              }
            },
            callback = function() doc.lsp_open = true end
          })
        else
          -- big files too slow for json encoder, also sending a huge file
          -- without yielding would stall the ui, and some lsp servers have
          -- issues with receiving big files in a single chunk.
          local text = table.concat(doc.lines)
            :gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
            :gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
            :gsub('\f', '\\f')

          server:push_raw("textDocument/didOpen", {
            raw_data = '{\n'
            .. '"jsonrpc": "2.0",\n'
            .. '"method": "textDocument/didOpen",\n'
            .. '"params": {\n'
            .. '"textDocument": {\n'
            .. '"uri": "'..util.touri(doc_path)..'",\n'
            .. '"languageId": "'..server:get_language_id(doc)..'",\n'
            .. '"version": '..doc.clean_change_id..',\n'
            .. '"text": "'..text..'"\n'
            .. '}\n'
            .. '}\n'
            .. '}\n',
            callback = function(server)
              doc.lsp_open = true
              log(server, "Big file '%s' ready for completion!", doc.filename)
            end
          })

          log(server, "Processing big file '%s'...", doc.filename)
        end
      else
        doc.lsp_open = true
      end
    end
  end
end

--- Send notification to applicable LSP servers that a document was saved
---@param doc core.doc
function lsp.save_document(doc)
  if not doc.lsp_open then return end

  local active_servers = lsp.get_active_servers(doc.filename, true)
  if #active_servers > 0 then
    for _, name in pairs(active_servers) do
      local server = lsp.servers_running[name]
      local save = server.capabilities.textDocumentSync.save
      if save then
        -- Send document content only if required by lsp server
        if save.includeText then
          -- If save should include file content then raw is faster for
          -- huge files that would take too much to encode.
          local text = table.concat(doc.lines)
            :gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
            :gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
            :gsub('\f', '\\f')

          server:push_raw("textDocument/didSave", {
            raw_data = '{\n'
            .. '"jsonrpc": "2.0",\n'
            .. '"method": "textDocument/didSave",\n'
            .. '"params": {\n'
            .. '"textDocument": {\n'
            .. '"uri": "'..util.touri(core.project_absolute_path(doc.filename))..'"\n'
            .. '},\n'
            .. '"text": "'..text..'"\n'
            .. '}\n'
            .. '}\n'
          })
        else
          server:push_notification('textDocument/didSave', {
            params = {
              textDocument = {
                uri = util.touri(core.project_absolute_path(doc.filename))
              }
            }
          })
        end
      end
    end
  end
end

--- Send notification to applicable LSP servers that a document was closed
---@param doc core.doc
function lsp.close_document(doc)
  if not doc.lsp_open then return end

  local active_servers = lsp.get_active_servers(doc.filename, true)
  if #active_servers > 0 then
    for _, name in pairs(active_servers) do
      local server = lsp.servers_running[name]
      if server.capabilities.textDocumentSync.openClose then
        server:push_notification('textDocument/didClose', {
          params = {
            textDocument = {
              uri = util.touri(core.project_absolute_path(doc.filename)),
              languageId = server:get_language_id(doc),
              version = doc.clean_change_id
            }
          }
        })
      end
    end
  end
end

--- Helper for lsp.update_document
---@param doc core.doc
local function request_signature_completion(doc)
  local line1, col1, line2, col2 = doc:get_selection()

  if line1 == line2 and col1 == col2 then
    -- First try to display a function signatures and if not possible
    -- do normal code autocomplete
    lsp.request_signature(
      doc,
      line1,
      col1,
      false,
      lsp.request_completion
    )
  end
end

---Send document updates to applicable running LSP servers.
---@param doc core.doc
---@param request_completion? boolean
function lsp.update_document(doc, request_completion)
  if not doc.lsp_open or not doc.lsp_changes or util.table_empty(doc.lsp_changes) then
    return
  end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if not doc.lsp_changes[server] or #doc.lsp_changes[server] <= 0 then
      goto continue
    end
    local sync_kind = server.capabilities.textDocumentSync.change
    if
      sync_kind ~= Server.text_document_sync_kind.None
      and
      server:can_push() -- ensure we don't loose incremental changes
    then
      local completion_callback = nil
      if request_completion then
        completion_callback = function() request_signature_completion(doc) end
      end

      if
        sync_kind == Server.text_document_sync_kind.Full
        and
        not server.incremental_changes
      then
        -- If sync should be done by sending full file content then lets do
        -- it raw which is faster for big files.
        local text = table.concat(doc.lines)
          :gsub('\\', '\\\\'):gsub("\n", "\\n"):gsub("\r", "\\r")
          :gsub("\t", "\\t"):gsub('"', '\\"'):gsub('\b', '\\b')
          :gsub('\f', '\\f')

        server:push_raw("textDocument/didChange", {
          overwrite = true,
          raw_data = '{\n'
          .. '"jsonrpc": "2.0",\n'
          .. '"method": "textDocument/didChange",\n'
          .. '"params": {\n'
          .. '"textDocument": {\n'
          .. '"uri": "'..util.touri(core.project_absolute_path(doc.filename))..'",\n'
          .. '"version": '..doc.lsp_version .. "\n"
          .. '},\n'
          .. '"contentChanges": [\n'
          .. '{"text": "'..text..'"}\n'
          .. "]\n"
          .. '}\n'
          .. '}\n',
          callback = function()
            doc.lsp_changes[server] = nil
            if completion_callback then
              completion_callback()
            end
          end
        })
      else
        lsp.servers_running[name]:push_notification('textDocument/didChange', {
          overwrite = true,
          params = {
            textDocument = {
              uri = util.touri(core.project_absolute_path(doc.filename)),
              version = doc.lsp_version,
            },
            contentChanges = doc.lsp_changes[server]
          },
          callback = function()
            doc.lsp_changes[server] = nil
            if completion_callback then
              completion_callback()
            end
          end
        })
      end
    end
    ::continue::
  end
end

--- Enable or disable diagnostic messages
function lsp.toggle_diagnostics()
  config.plugins.lsp.show_diagnostics = not config.plugins.lsp.show_diagnostics

  if not config.plugins.lsp.show_diagnostics then
    diagnostics.lintplus_clear_messages()
    core.log("[LSP] Diagnostics disabled")
  else
    diagnostics.lintplus_populate()
    core.log("[LSP] Diagnostics enabled")
  end
end

--- Send to applicable LSP servers a request for code completion
function lsp.request_completion(doc, line, col, forced)
  if lsp.in_trigger or not doc.lsp_open then
    return
  end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if server.capabilities.completionProvider then
      local capabilities = lsp.servers_running[name].capabilities
      local char = doc:get_char(line, col-1)
      local trigger_char = false

      local request = get_buffer_position_params(doc, line, col)

      -- without providing context some language servers like the
      -- lua-language-server behave poorly and return garbage.
      if
        capabilities.completionProvider.triggerCharacters
        and
        #capabilities.completionProvider.triggerCharacters > 0
        and
        char:match("%p")
        and
        util.intable(char, capabilities.completionProvider.triggerCharacters)
      then
        request.context = {
          triggerKind = Server.completion_trigger_Kind.TriggerCharacter,
          triggerCharacter = char
        }
        trigger_char = true;
      end

      if
        not trigger_char
        and
        not autocomplete.can_complete()
        and
        not forced
      then
        return false
      end

      server:push_request('textDocument/completion', {
        params = request,
        overwrite = true,
        callback = function(server, response)
          lsp.user_typed = false

          -- don't autocomplete if caret position changed
          local cline, cchar = doc:get_selection()
          if cline ~= line or cchar ~= col then
            return
          end

          if server.verbose then
            server:log(
              "Completion response received."
            )
          end

          if not response.result then
            return
          end

          local result = response.result
          local complete_result = true
          if result.isIncomplete then
            if server.verbose then
              core.log_quiet(
                "["..server.name.."] " .. "Completion list incomplete"
              )
            end
            complete_result = false
          end

          if not result.items or #result.items <= 0 then
            -- Workaround for some lsp servers that don't return results
            -- in the items property but instead on the results it self
            if #result > 0 then
              local items = result
              result = {items = items}
            else
              return
            end
          end

          local symbols = {
            name = lsp.servers_running[name].name,
            files = lsp.servers_running[name].file_patterns,
            items = {}
          }

          local symbol_count = 1
          for _, symbol in ipairs(result.items) do
            local label = symbol.label
              or (
                symbol.textEdit
                and symbol.textEdit.newText
                or symbol.insertText
              )

            local info = server.get_completion_item_kind(symbol.kind)

            local desc = symbol.detail or ""

            -- TODO: maybe we should give priority to insertText above
            if
              symbol.label and
              symbol.insertText and
              #symbol.label > #symbol.insertText
            then
              label = symbol.insertText
              if symbol.label ~= label then
                desc = symbol.label
              end
              if symbol.detail then
                desc = desc .. ": " .. symbol.detail
              end
            end

            if desc ~= "" then
              desc = desc .. "\n"
            end

            if
              type(symbol.documentation) == "table"
              and
              symbol.documentation.value
            then
              desc = desc .. "\n" .. symbol.documentation.value
              if
                symbol.documentation.kind
                and
                symbol.documentation.kind == "markdown"
              then
                desc = util.strip_markdown(desc)
                if symbol_count % 10 == 0 then
                  coroutine.yield()
                end
              end
            elseif symbol.documentation then
              desc = desc .. "\n" .. symbol.documentation
            end

            desc = desc:gsub("[%s\n]+$", "")
              :gsub("\n\n\n+", "\n\n")

            symbols.items[label] = {
              info = info,
              desc = desc,
              data = {
                server = server, completion_item = symbol
              },
              onselect = autocomplete_onselect
            }

            if
              server.capabilities.completionProvider.resolveProvider
              and
              not symbol.documentation
            then
              symbols.items[label].onhover = autocomplete_onhover
            end

            symbol_count = symbol_count + 1
          end

          if trigger_char and complete_result then
            lsp.in_trigger = true
            autocomplete.complete(symbols, function()
              lsp.in_trigger = false
            end)
          else
            autocomplete.complete(symbols)
          end
        end
      })
    end
  end
end

--- Send to applicable LSP servers a request for info about a function
--- signatures and display them on a tooltip.
function lsp.request_signature(doc, line, col, forced, fallback)
  if not doc.lsp_open then return end

  local char = doc:get_char(line, col-1)
  local prev_char = doc:get_char(line, col-2) -- to support ', '
  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if
      server.capabilities.signatureHelpProvider
      and
      (
        forced
        or
        (
          server.capabilities.signatureHelpProvider.triggerCharacters
          and
          #server.capabilities.signatureHelpProvider.triggerCharacters > 0
          and
          (
            util.intable(
              char, server.capabilities.signatureHelpProvider.triggerCharacters
            )
            or
            util.intable(
              prev_char,
              server.capabilities.signatureHelpProvider.triggerCharacters
            )
          )
        )
      )
    then
      server:push_request('textDocument/signatureHelp', {
        params = get_buffer_position_params(doc, line, col),
        overwrite = true,
        callback = function(server, response)
          -- don't show signature if caret position changed
          local cline, cchar = doc:get_selection()
          if cline ~= line or cchar ~= col then
            return
          end

          if
            response.result
            and
            response.result.signatures
            and
            #response.result.signatures > 0
          then
            autocomplete.close()
            listbox.show_signatures(response.result)
            lsp.user_typed  = false
          elseif fallback then
            fallback(doc, line, col)
          end
        end
      })
      break
    elseif fallback then
      fallback(doc, line, col)
    end
  end
end

---Returns the "selection" for the token that includes the provided position.
---@param doc core.doc
---@param line integer
---@param col integer
---@return integer line1
---@return integer col2
---@return integer line2
---@return integer col2
local function get_token_range(doc, line, col)
  local col1 = 0
  for _, _, text in doc.highlighter:each_token(line) do
    local text_len = #text
    local col2 = col1 + text_len
    if col2 >= col then
      return line, col1 + 1, line, col2 + 1
    end
    col1 = col2
  end
  return line, col, line, col+1
end

---@type core.node
local help_active_node = nil
---@type core.node
local help_bottom_node = nil
--- Sends a request to applicable LSP servers for information about the
--- symbol where the cursor is placed and shows it on a tooltip.
function lsp.request_hover(doc, line, col, in_tab)
  if not doc.lsp_open then return end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if server.capabilities.hoverProvider then
      server:push_request('textDocument/hover', {
        params = get_buffer_position_params(doc, line, col),
        callback = function(server, response)
          if response.result and response.result.contents then
            local range = response.result.range
            local line1, col1, line2, col2
            if range then
              line1, col1, line2, col2 = util.toselection(range, doc)
            else
              line1, col1, line2, col2 = get_token_range(doc, line, col)
            end
            lsp.hover_position.utf8_range = { line1 = line1, col1 = col1,
                                              line2 = line2, col2 = col2 }

            local content = response.result.contents
            local kind = nil
            local text = ""
            if type(content) == "table" then
              if content.value then
                text = content.value
                if content.kind then kind = content.kind end
              else
                local texts = {}
                for _, element in pairs(content) do
                  if type(element) == "string" then
                    table.insert(texts, element)
                  elseif type(element) == "table" and element.value then
                    table.insert(texts, element.value)
                    if not kind and element.kind then kind = element.kind end
                  end
                end
                text = table.concat(texts, "\n\n")
              end
            else -- content should be a string
              text = content
            end
            if text and #text > 0 then
              text = text:gsub("^[\n%s]+", ""):gsub("[\n%s]+$", "")
              if not in_tab then
                if kind == "markdown" then text = util.strip_markdown(text) end
                listbox.show_text(
                  text,
                  { line = line, col = col }
                )
              else
                local line1, col1 = translate.start_of_word(doc, line, col)
                local line2, col2 = translate.end_of_word(doc, line1, col1)
                local title = doc:get_text(line1, col1, line2, col2):gsub("%s*", "")
                title = "Help:" .. title .. ".md"
                ---@type lsp.helpdoc
                local helpdoc = HelpDoc(title, title)
                helpdoc:set_text(text)
                local helpview = DocView(helpdoc)
                helpview.context = "application"
                helpview.wrapping_enabled = true
                if LineWrapping then
                  LineWrapping.update_docview_breaks(helpview)
                end
                if
                  not help_bottom_node
                  or
                  (
                    #help_bottom_node.views == 1
                    and
                    not help_active_node:get_node_for_view(help_bottom_node.views[1])
                  )
                then
                  help_active_node = core.root_view:get_active_node_default()
                  help_bottom_node = help_active_node:split("down", helpview)
                else
                  help_bottom_node:add_view(helpview)
                end
              end
            end
          end
        end
      })
      break
    end
  end
end

--- Sends a request to applicable LSP servers for a symbol references
function lsp.request_references(doc, line, col)
  if not doc.lsp_open then return end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if server.capabilities.hoverProvider then
      local request_params = get_buffer_position_params(doc, line, col)
      request_params.context = {includeDeclaration = true}
      server:push_request('textDocument/references', {
        params = request_params,
        callback = function(server, response)
          if response.result and #response.result > 0 then
            local references, reference_names = get_references_lists(response.result)
            core.command_view:enter("Filter References", {
              submit = function(text, item)
                if item then
                  local reference = references[item.name]
                    lsp.goto_location(reference)
                end
              end,
              suggest = function(text)
                local res = common.fuzzy_match(reference_names, text)
                for i, name in ipairs(res) do
                  local reference_info = util.split(name, "||")
                  res[i] = {
                    text = reference_info[1],
                    info = reference_info[2],
                    name = name
                  }
                end
                return res
              end
            })
          else
            core.log("[LSP] No references found.")
          end
        end
      })
      break
    end
    break
  end
end

---Sends a request to applicable LSP servers to retrieve the
---hierarchy of calls for the given function under the cursor.
function lsp.request_call_hierarchy(doc, line, col)
  if not doc.lsp_open then return end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if server.capabilities.callHierarchyProvider then
      server:push_request('textDocument/prepareCallHierarchy', {
        params = get_buffer_position_params(doc, line, col),
        callback = function(server, response)
          if response.result and #response.result > 0 then
            -- TODO: Finish implement call hierarchy functionality
            return
          end
        end
      })
      return
    end
  end

  core.log("[LSP] Call hierarchy not supported.")
end

---Sends a request to applicable LSP servers to rename a symbol.
---@param doc core.doc
---@param line integer
---@param col integer
---@param new_name string
function lsp.request_symbol_rename(doc, line, col, new_name)
  if not doc.lsp_open then return end

  local servers_found = false
  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    servers_found = true
    local server = lsp.servers_running[name]
    if server.capabilities.renameProvider then
      local request_params = get_buffer_position_params(doc, line, col)
      request_params.newName = new_name
      server:push_request('textDocument/rename', {
        params = request_params,
        callback = function(server, response)
          if response.result and #response.result.changes then
            for file_uri, changes in pairs(response.result.changes) do
              core.log(file_uri .. " " .. #changes)
              -- TODO: Finish implement textDocument/rename
            end
          end

          core.log("%s", json.prettify(json.encode(response)))
        end
      })
      return
    end
  end

  if not servers_found then
    core.log("[LSP] " .. "No server ready or running")
  else
    core.log("[LSP] " .. "Symbols rename not supported")
  end
end

---Sends a request to applicable LSP servers to search for symbol on workspace.
---@param doc core.doc
---@param symbol string
function lsp.request_workspace_symbol(doc, symbol)
  if not doc.lsp_open then return end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]
    if server.capabilities.workspaceSymbolProvider then
      local rs = SymbolResults(symbol)
      core.root_view:get_active_node_default():add_view(rs)
      server:push_request('workspace/symbol', {
        params = {
          query = symbol,
          -- TODO: implement status notifications but seems not supported
          -- by tested lsp servers so far.
          -- workDoneToken = "some-identifier",
          -- partialResultToken = "some-other-identifier"
        },
        callback = function(server, response)
          if response.result and #response.result > 0 then
            for index, result in ipairs(response.result) do
              rs:add_result(result)
              if index % 100 == 0 then
                coroutine.yield()
                rs.list:resize_to_parent()
              end
            end
            rs.list:resize_to_parent()
          end
          rs:stop_searching()
        end
      })
      break
    end
    break
  end
end

--- Request a list of symbols for the given document for easy document
-- navigation and displays them using core.command_view:enter()
function lsp.request_document_symbols(doc)
  if not doc.lsp_open then return end

  local servers_found = false
  local symbols_retrieved = false
  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    servers_found = true
    local server = lsp.servers_running[name]
    if server.capabilities.documentSymbolProvider then
      log(server, "Retrieving document symbols...")
      server:push_request('textDocument/documentSymbol', {
        params = {
          textDocument = {
            uri = util.touri(core.project_absolute_path(doc.filename)),
          }
        },
        callback = function(server, response)
          if response.result and response.result and #response.result > 0 then
            local symbols, symbol_names = get_symbol_lists(response.result)
            core.command_view:enter("Find Symbol", {
              submit = function(text, item)
                if item then
                  local symbol = symbols[item.name]
                  -- The lsp may return a location object with range
                  -- and uri inside of it or just range as part of
                  -- the symbol it self.
                  symbol = symbol.location and symbol.location or symbol
                  if not symbol.uri then
                    local line1, col1 = util.toselection(symbol.range, doc)
                    doc:set_selection(line1, col1, line1, col1)
                  else
                    lsp.goto_location(symbol)
                  end
                end
              end,
              suggest = function(text)
                local res = common.fuzzy_match(symbol_names, text)
                for i, name in ipairs(res) do
                  res[i] = {
                    text = util.split(name, "||")[1],
                    info = Server.get_symbol_kind(symbols[name].kind),
                    name = name
                  }
                end
                return res
              end
            })
          end
        end
      })
      symbols_retrieved = true
      break
    end
  end

  if not servers_found then
    core.log("[LSP] " .. "No server running")
  elseif not symbols_retrieved then
    core.log("[LSP] " .. "Document symbols not supported")
  end
end

--- Format current document if supported by one of the running lsp servers.
function lsp.request_document_format(doc)
  if not doc.lsp_open then return end

  local servers_found = false
  local format_executed = false
  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    servers_found = true
    local server = lsp.servers_running[name]
    if server.capabilities.documentFormattingProvider then
      local trim_trailing_whitespace = false
      local trim_newlines = false
      if type(config.plugins.trimwhitespace) == "table"
         and config.plugins.trimwhitespace.enabled
      then
        trim_trailing_whitespace = true
        trim_newlines = config.plugins.trimwhitespace.trim_empty_end_lines
      elseif config.plugins.trimwhitespace then -- Plugin enabled with true
        trim_trailing_whitespace = true
        trim_newlines = true
      end
      local indent_type, indent_size, indent_confirmed = doc:get_indent_info()
      if not indent_confirmed then
        indent_type, indent_size = config.tab_type, config.indent_size
      end
      server:push_request('textDocument/formatting', {
        params = {
          textDocument = {
            uri = util.touri(core.project_absolute_path(doc.filename)),
          },
          options = {
            tabSize = indent_size,
            insertSpaces = indent_type == "soft",
            trimTrailingWhitespace = trim_trailing_whitespace,
            insertFinalNewline = false,
            trimFinalNewlines = trim_newlines
          }
        },
        callback = function(server, response)
          if response.error and response.error.message then
            log(server, "Error formatting: " .. response.error.message)
          elseif response.result and #response.result > 0 then
            -- Apply edits in reverse, as the ranges don't consider
            -- the intermediate states.
            -- Consider the TextEdits as already sorted.
            -- If there are servers that don't sort their TextEdits,
            -- we'll add sorting code.
            for i=#response.result,1,-1 do
              apply_edit(server, doc, response.result[i], false, false)
            end
            log(server, "Formatted document")
          else
            log(server, "Formatting not required")
          end
        end
      })
      format_executed = true
      break
    end
  end

  if not servers_found then
    core.log("[LSP] " .. "No server running")
  elseif not format_executed then
    core.log("[LSP] " .. "Formatting not supported")
  end
end

function lsp.view_document_diagnostics(doc)
  local diagnostic_messages = diagnostics.get(core.project_absolute_path(doc.filename))
  if not diagnostic_messages or #diagnostic_messages <= 0 then
    core.log("[LSP] %s", "No diagnostic messages found.")
    return
  end

  local diagnostic_labels = { "Error", "Warning", "Info", "Hint" }

  local indexes, captions = {}, {}
  for index, diagnostic in pairs(diagnostic_messages) do
    local line1, col1 = util.toselection(diagnostic.range)
    local label = diagnostic_labels[diagnostic.severity or diagnostics.severity.ERROR]
      .. ": " .. diagnostic.message .. " "
      .. tostring(line1) .. ":" .. tostring(col1)
    captions[index] = label
    indexes[label] = index
  end

  core.command_view:enter("Filter Diagnostics", {
    submit = function(text, item)
      if item then
        local diagnostic = diagnostic_messages[item.index]
        local line1, col1 = util.toselection(diagnostic.range, doc)
        doc:set_selection(line1, col1, line1, col1)
      end
    end,
    suggest = function(text)
      local res = common.fuzzy_match(captions, text)
      for i, name in ipairs(res) do
        local diagnostic = diagnostic_messages[indexes[name]]
        local line1, col1 = util.toselection(diagnostic.range)
        res[i] = {
          text = diagnostics.lintplus_kinds[diagnostic.severity or diagnostics.severity.ERROR]
            .. ": " .. diagnostic.message,
          info = tostring(line1) .. ":" .. tostring(col1),
          index = indexes[name]
        }
      end
      return res
    end
  })
end

function lsp.view_all_diagnostics()
  if diagnostics.count <= 0 then
    core.log("[LSP] %s", "No diagnostic messages found.")
    return
  end

  local captions = {}
  for _, diagnostic in ipairs(diagnostics.list) do
    table.insert(
      captions,
      core.normalize_to_project_dir(diagnostic.filename)
    )
  end

  core.command_view:enter("Filter Files", {
    submit = function(text, item)
      if item then
        core.root_view:open_doc(
          core.open_doc(
            common.home_expand(
              text
            )
          )
        )
      end
    end,
    suggest = function(text)
      local res = common.fuzzy_match(captions, text, true)
      for i, name in ipairs(res) do
        local diagnostics_count = diagnostics.get_messages_count(
          core.project_absolute_path(name)
        )
        res[i] = {
          text = name,
          info = "Messages: " .. diagnostics_count
        }
      end
      return res
    end
  })
end

--- Jumps to the definition or implementation of the symbol where the cursor
-- is placed if the LSP server supports it
function lsp.goto_symbol(doc, line, col, implementation)
  if not doc.lsp_open then return end

  for _, name in pairs(lsp.get_active_servers(doc.filename, true)) do
    local server = lsp.servers_running[name]

    local method = ""
    if not implementation then
      if server.capabilities.definitionProvider then
        method = method .. "definition"
      elseif server.capabilities.declarationProvider then
        method = method .. "declaration"
      elseif server.capabilities.typeDefinitionProvider then
        method = method .. "typeDefinition"
      else
        log(server, "Goto definition not supported")
        return
      end
    else
      if server.capabilities.implementationProvider then
        method = method .. "implementation"
      else
        log(server, "Goto implementation not supported")
        return
      end
    end

    -- Send document updates first
    lsp.update_document(doc)

    server:push_request("textDocument/" .. method, {
      params = get_buffer_position_params(doc, line, col),
      callback = function(server, response)
        local location = response.result

        if not location or not location.uri and #location == 0 then
          core.log("[LSP] No %s found.", method)
          return
        end

        if not location.uri and #location > 1 then
          listbox.clear()
          for _, loc in pairs(location) do
            local preview, position = get_location_preview(loc)
            listbox.append {
              text = preview,
              info = position,
              location = loc
            }
          end
          listbox.show_list(nil, function(doc, item)
            lsp.goto_location(item.location)
          end)
        else
          if not location.uri then
            location = location[1]
          end
          lsp.goto_location(location)
        end
      end
    })
  end
end

--
-- Thread to process server requests and responses
-- without blocking entirely the editor.
--
core.add_thread(function()
  while true do
    local servers_running = false
    for _,server in pairs(lsp.servers_running) do
      -- Send raw data to server which is usually big and slow in a
      -- non blocking way by creating a coroutine just for it.
      if #server.raw_list > 0 then
        local raw_send = coroutine.create(function()
          server:process_raw()
        end)
        coroutine.resume(raw_send)
        while coroutine.status(raw_send) ~= "dead" do
          -- while sending raw request we only read from lsp to not
          -- conflict with the written raw data so remember no calls
          -- here to: server:process_client_responses()
          -- or server:process_notifications()
          server:process_errors(config.plugins.lsp.log_server_stderr)
          server:process_responses()
          coroutine.yield()
          coroutine.resume(raw_send)
        end
      end

      if not config.plugins.lsp.more_yielding then
        server:process_notifications()
        server:process_requests()
        server:process_responses()
        server:process_client_responses()
      else
        server:process_notifications()
        coroutine.yield()
        server:process_requests()
        coroutine.yield()
        server:process_responses()
        server:process_client_responses()
        coroutine.yield()
      end

      server:process_errors(config.plugins.lsp.log_server_stderr)

      servers_running = true
    end

    if servers_running then
      local wait = 0.01
      if config.plugins.lsp.more_yielding then wait = 0 end
      coroutine.yield(wait)
    else
      coroutine.yield(2)
    end
  end
end)

--
-- Events patching
--
local doc_load = Doc.load
local doc_save = Doc.save
local doc_on_close = Doc.on_close
local doc_raw_insert = Doc.raw_insert
local doc_raw_remove = Doc.raw_remove
local root_view_on_text_input = RootView.on_text_input
local root_view_on_mouse_moved = RootView.on_mouse_moved

function Doc:load(...)
  local res = doc_load(self, ...)
  -- skip new files
  if self.filename and config.plugins.lsp.autostart_server then
    diagnostics.lintplus_init_doc(self)
    core.add_thread(function()
      lsp.start_server(self.filename, core.project_dir)
      lsp.open_document(self)
    end)
  end
  return res
end

function Doc:save(...)
  local old_filename = self.filename
  local res = doc_save(self, ...)
  if old_filename ~= self.filename then
    -- seems to be a new document so we send open notification
    diagnostics.lintplus_init_doc(self)
    core.add_thread(function()
      lsp.open_document(self)
    end)
  else
    core.add_thread(function()
      lsp.update_document(self)
      lsp.save_document(self)
    end)
  end
  return res
end

function Doc:on_close()
  doc_on_close(self)

  -- skip new files
  if not self.filename then return end
  core.add_thread(function()
    lsp.close_document(self)
  end)

  if not config.plugins.lsp.stop_unneeded_servers then
    return
  end

  -- Check if any running lsp servers is not needed anymore and stop it
  for name, server in pairs(lsp.servers_running) do
    local doc_found = false
    for _, docu in ipairs(core.docs) do
      if docu.filename then
        if common.match_pattern(docu.filename, server.file_patterns) then
          doc_found = true
          break
        end
      end
    end

    if not doc_found and not server.exit_timer then
      local t = Timer(server.quit_timeout * 1000, true)
      t.on_timer = function()
        server:exit()
        core.log("[LSP] stopped %s", name)
        lsp.servers_running = util.table_remove_key(lsp.servers_running, name)
      end
      t:start()
      server.exit_timer = t
    end
  end
end

local function add_change(self, text, line1, col1, line2, col2)
  if not self.lsp_changes then
    self.lsp_changes = {}
    self.lsp_version = 0
  end

  local change = { range = {}, text = text}
  change.range["start"] = {line = line1-1, character = col1-1}
  change.range["end"] = {line = line2-1, character = col2-1}

  for _, name in pairs(lsp.get_active_servers(self.filename, true)) do
    local server = lsp.servers_running[name]
    if not self.lsp_changes[server] then
      self.lsp_changes[server] = {}
    end
    table.insert(self.lsp_changes[server], change)
  end

  -- TODO: this should not be needed but changing documents rapidly causes this
  if type(self.lsp_version) ~= 'nil' then
    self.lsp_version = self.lsp_version + 1
  else
    self.lsp_version = 1
  end
end

function Doc:raw_insert(line, col, text, undo_stack, time)
  doc_raw_insert(self, line, col, text, undo_stack, time)

  -- skip new files
  if not self.filename then return end

  col = util.doc_utf8_to_utf16(self, line, col)

  if self.lsp_open then
    add_change(self, text, line, col, line, col)
    lsp.update_document(self)
  elseif #lsp.get_active_servers(self.filename, true) > 0 then
    add_change(self, text, line, col, line, col)
  end
end

function Doc:raw_remove(line1, col1, line2, col2, undo_stack, time)
  local lcol1 = util.doc_utf8_to_utf16(self, line1, col1)
  local lcol2 = util.doc_utf8_to_utf16(self, line2, col2)

  doc_raw_remove(self, line1, col1, line2, col2, undo_stack, time)

  -- skip new files
  if not self.filename then return end

  if self.lsp_open then
    add_change(self, "", line1, lcol1, line2, lcol2)
    lsp.update_document(self)
  elseif #lsp.get_active_servers(self.filename, true) > 0 then
    add_change(self, "", line1, lcol1, line2, lcol2)
  end
end

function RootView:on_text_input(text)
  root_view_on_text_input(self, text)

  -- this part should actually trigger after Doc:raw_insert and Doc:raw_remove
  -- so it is safe to trigger autocompletion from here.
  local av = get_active_docview()

  if av then
    lsp.user_typed = true
    lsp.update_document(av.doc, true)
  end
end

function RootView:on_mouse_moved(x, y, dx, dy)
  root_view_on_mouse_moved(self, x, y, dx, dy)

  if not config.plugins.lsp.mouse_hover then return end

  lsp.hover_position.x = x
  lsp.hover_position.y = y
  if lsp.hover_position.triggered then
    local doc, line, col = lsp.get_hovered_location(x, y)
    if doc == lsp.hover_position.doc and lsp.hover_position.utf8_range then
      local utf8_range = lsp.hover_position.utf8_range
      local line1, col1, line2, col2 = utf8_range.line1, utf8_range.col1,
                                       utf8_range.line2, utf8_range.col2
      if (line > line1 or (line == line1 and col >= col1)) and
         (line < line2 or (line == line2 and col <= col2)) then
        return
      end
    end
    listbox.hide()
    lsp.hover_position.triggered = false
  end
  lsp.hover_timer:set_interval(config.plugins.lsp.mouse_hover_delay)
  lsp.hover_timer:restart()
end

--
-- Add status view item to show document diagnostics count
--
core.status_view:add_item({
  predicate = function()
    local dv = get_active_docview()
    if dv then
      local filename = core.project_absolute_path(dv.doc.filename)
      local diagnostic_messages = diagnostics.get(filename)
      if diagnostic_messages and #diagnostic_messages > 0 then
        return true
      end
    end
    return false
  end,
  name = "lsp:diagnostics",
  alignment = StatusView.Item.RIGHT,
  get_item = function()
    local dv = get_active_docview()
    if dv then
      local filename = core.project_absolute_path(dv.doc.filename)
      local diagnostic_messages = diagnostics.get(filename)

      if diagnostic_messages and #diagnostic_messages > 0 then
        return {
          style.warn,
          style.icon_font, "!",
          style.font, " " .. tostring(#diagnostic_messages)
        }
      end
    end

    return {}
  end,
  command = "lsp:view-document-diagnostics",
  position = 1,
  tooltip = "LSP Diagnostics",
  separator = core.status_view.separator2
})

--
-- Register autocomplete icons
--
if autocomplete.add_icon then
  local autocomplete_icons = {
    { name = "Text",          color = "keyword",  icon = '' }, -- U+F77E
    { name = "Method",        color = "function", icon = '' }, -- U+F6A6
    { name = "Function",      color = "function", icon = '' }, -- U+F794
    { name = "Constructor",   color = "literal",  icon = '' }, -- U+F423
    { name = "Field",         color = "keyword2", icon = 'ﰠ' }, -- U+FC20
    { name = "Variable",      color = "keyword2", icon = '' }, -- U+F52A
    { name = "Class",         color = "literal",  icon = 'ﴯ' }, -- U+FD2F
    { name = "Interface",     color = "literal",  icon = '' }, -- U+F0E8
    { name = "Module",        color = "literal",  icon = '' }, -- U+F487
    { name = "Property",      color = "keyword2", icon = 'ﰠ' }, -- U+FC20
    { name = "Unit",          color = "number",   icon = '塞' }, -- U+F96C
    { name = "Value",         color = "string",   icon = '' }, -- U+F89F
    { name = "Enum",          color = "keyword2", icon = '' }, -- U+F15D
    { name = "Keyword",       color = "keyword",  icon = '' }, -- U+F80A
    { name = "Snippet",       color = "keyword",  icon = '' }, -- U+F44F
    { name = "Color",         color = "string",   icon = '' }, -- U+F8D7
    { name = "File",          color = "string",   icon = '' }, -- U+F718
    { name = "Reference",     color = "string",   icon = '' }, -- U+F706
    { name = "Folder",        color = "string",   icon = '' }, -- U+F74A
    { name = "EnumMember",    color = "number",   icon = '' }, -- U+F15D
    { name = "Constant",      color = "number",   icon = '' }, -- U+F8FE
    { name = "Struct",        color = "keyword2", icon = 'פּ' }, -- U+FB44
    { name = "Event",         color = "keyword",  icon = '' }, -- U+F0E7
    { name = "Operator",      color = "operator", icon = '' }, -- U+F694
    { name = "Unknown",       color = "keyword",  icon = '' }, -- U+F128
    { name = "TypeParameter", color = "literal",  icon = '' }  -- U+EA92
  }

  -- We add the font here to let it automatically scale by the scale plugin
  style.syntax_fonts["lsp_symbols"] = renderer.font.load(
    USERDIR .. "/plugins/lsp/fonts/symbols.ttf",
    15 * SCALE
  )

  for _, icon in ipairs(autocomplete_icons) do
    autocomplete.add_icon(
      icon.name, icon.icon, style.syntax_fonts["lsp_symbols"], icon.color
    )
  end
end

--
-- Commands
--
command.add(
  function()
    local dv = get_active_docview()
    return dv ~= nil and dv.doc.lsp_open, dv and dv.doc or nil
  end, {

  ["lsp:complete"] = function(doc)
    local line1, col1, line2, col2 = doc:get_selection()
    if line1 == line2 and col1 == col2 then
      lsp.request_completion(doc, line1, col1, true)
    end
  end,

  ["lsp:goto-definition"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.goto_symbol(doc, line1, col1)
    end
  end,

  ["lsp:goto-implementation"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.goto_symbol(doc, line1, col1, true)
    end
  end,

  ["lsp:show-signature"] = function(doc)
    local line1, col1, line2, col2 = doc:get_selection()
    if line1 == line2 and col1 == col2 then
      lsp.request_signature(doc, line1, col1, true)
    end
  end,

  ["lsp:show-symbol-info"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.request_hover(doc, line1, col1)
    end
  end,

  ["lsp:show-symbol-info-in-tab"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.request_hover(doc, line1, col1, true)
    end
  end,

  ["lsp:view-call-hierarchy"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.request_call_hierarchy(doc, line1, col1)
    end
  end,

  ["lsp:view-document-symbols"] = function(doc)
    lsp.request_document_symbols(doc)
  end,

  ["lsp:format-document"] = function(doc)
    lsp.request_document_format(doc)
  end,

  ["lsp:view-document-diagnostics"] = function(doc)
    lsp.view_document_diagnostics(doc)
  end,

  ["lsp:rename-symbol"] = function(doc)
    local symbol = doc:get_text(doc:get_selection())
    local line1, col1, line2 = doc:get_selection()
    if #symbol > 0 and line1 == line2 then
      core.command_view:enter("New Symbol Name", {
        text = symbol,
        submit = function(new_name)
          lsp.request_symbol_rename(doc, line1, col1, new_name)
        end
      })
    else
      core.log("Please select a symbol on the document to rename.")
    end
  end,

  ["lsp:find-references"] = function(doc)
    local line1, col1, line2 = doc:get_selection()
    if line1 == line2 then
      lsp.request_references(doc, line1, col1)
    end
  end
})

command.add(nil, {
  ["lsp:view-all-diagnostics"] = function()
    lsp.view_all_diagnostics()
  end,

  ["lsp:find-workspace-symbol"] = function()
    local dv = get_active_docview()
    local doc = dv and dv.doc or nil
    local symbol = doc and doc:get_text(doc:get_selection()) or ""
    core.command_view:enter("Find Workspace Symbol", {
      text = symbol,
      submit = function(query)
        lsp.request_workspace_symbol(doc, query)
      end
    })
  end,

  ["lsp:toggle-diagnostics"] = function()
    if not diagnostics.lintplus_found then
      core.error("[LSP] Please install lintplus for diagnostics rendering.")
      return
    end
    lsp.toggle_diagnostics()
  end,

  ["lsp:stop-servers"] = function()
    lsp.stop_servers()
  end,

  ["lsp:start-servers"] = function()
    lsp.start_servers()
  end,

  ["lsp:restart-servers"] = function()
    lsp.stop_servers()
    lsp.start_servers()
  end
})

--
-- Default Keybindings
--
keymap.add {
  ["ctrl+space"]        = "lsp:complete",
  ["ctrl+shift+space"]  = "lsp:show-signature",
  ["alt+a"]             = "lsp:show-symbol-info",
  ["alt+shift+a"]       = "lsp:show-symbol-info-in-tab",
  ["alt+d"]             = "lsp:goto-definition",
  ["alt+shift+d"]       = "lsp:goto-implementation",
  ["alt+s"]             = "lsp:view-document-symbols",
  ["alt+shift+s"]       = "lsp:find-workspace-symbol",
  ["alt+f"]             = "lsp:find-references",
  ["alt+shift+f"]       = "lsp:format-document",
  ["alt+e"]             = "lsp:view-document-diagnostics",
  ["ctrl+alt+e"]        = "lsp:view-all-diagnostics",
  ["alt+shift+e"]       = "lsp:toggle-diagnostics",
  ["alt+c"]             = "lsp:view-call-hierarchy",
  ["alt+r"]             = "lsp:rename-symbol",
}

--
-- Register context menu items
--
local function lsp_predicate(_, _, also_in_symbol)
  local dv = get_active_docview()
  if dv then
    local doc = dv.doc

    if #lsp.get_active_servers(doc.filename, true) < 1 then
      return false
    elseif not also_in_symbol then
      return true
    end

    -- Make sure the cursor is place near a document symbol (word)
    local linem, colm = doc:get_selection()
    local linel, coll = doc:position_offset(linem, colm, translate.start_of_word)
    local liner, colr = doc:position_offset(linem, colm, translate.end_of_word)

    local word_left = doc:get_text(linel, coll, linem, colm)
    local word_right = doc:get_text(linem, colm, liner, colr)

    if #word_left > 0 or #word_right > 0 then
      return true
    end
  end
  return false
end

local function lsp_predicate_symbols()
  return lsp_predicate(nil, nil, true)
end

local menu_found, menu = pcall(require, "plugins.contextmenu")
if menu_found then
  menu:register(lsp_predicate_symbols, {
    menu.DIVIDER,
    { text = "Show Symbol Info",        command = "lsp:show-symbol-info" },
    { text = "Show Symbol Info in Tab", command = "lsp:show-symbol-info-in-tab" },
    { text = "Goto Definition",         command = "lsp:goto-definition" },
    { text = "Goto Implementation",     command = "lsp:goto-implementation" },
    { text = "Find References",         command = "lsp:find-references" }
  })

  menu:register(lsp_predicate, {
    menu.DIVIDER,
    { text = "Document Symbols",       command = "lsp:view-document-symbols" },
    { text = "Document Diagnostics",   command = "lsp:view-document-diagnostics" },
    { text = "Toggle Diagnostics",     command = "lsp:toggle-diagnostics" },
    { text = "Format Document",        command = "lsp:format-document" },
  })

  local menu_show = menu.show
  function menu:show(...)
    lsp.hover_timer:stop()
    lsp.hover_timer:reset()
    listbox.hide()
    lsp.hover_position.triggered = false
    menu_show(self, ...)
  end
end


return lsp

