Browse Source

[Feature] Encapsulate interface logic (#1320)

* Provide a utility function for aligning text

* Replace lvim banner with one using only ASCII chars

* Use strings.format instead of .. operator

* Center text in the popup based on its dimentions

* Minor improvements

* Provide a popup factory function

* Add function documentation

* Improve text alignment

* Print marker only if provider list is not empty

* Format client capabilities as list

* Pretty format lsp client capabilities

* Add a metatable to popup.lua

* Improve rendering when no lsp is available

* Take cmdheight into acount when computing popup size and pos

Co-authored-by: kylo252 <59826753+kylo252@users.noreply.github.com>
Luc Sinet 3 năm trước cách đây
mục cha
commit
9b36872d88
3 tập tin đã thay đổi với 243 bổ sung137 xóa
  1. 102 137
      lua/core/info.lua
  2. 62 0
      lua/interface/popup.lua
  3. 79 0
      lua/interface/text.lua

+ 102 - 137
lua/core/info.lua

@@ -1,128 +1,63 @@
-local M = {}
-local indent = "  "
-
-M.banner = {
-  " ",
-  indent
-    .. "⠀⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀   ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀  ⠀⠀     ⠀⠀⠀   ⠀⠀ ⣺⡿⠀⠀⠀⠀⠀⠀⠀ ⠀⠀⠀",
-  indent
-    .. "⠀⣿⠇⠀⠀⠀⠀⠀⣤⡄⠀⠀⢠⣤⡄⠀.⣠⣤⣤⣤⡀⠀⠀⢀⣤⣤⣤⣤⡄⠀⠀⠀⣤⣄⣤⣤⣤⠀⠀ ⣿⣯  ⣿⡟⠀   ⣤⣤⠀⠀⠀⠀⣠⣤⣤⣤⣄⣤⣤",
-  indent
-    .. "⢠⣿⠀⠀⠀⠀⠀⠀⣿⠃⠀⠀⣸⣿⠁⠀⣿⣿⠉⠀⠈⣿⡇⠀⠀⠛⠋⠀⠀⢹⣿⠀⠀⠀⣿⠏⠀⠸⠿⠃⠀⣿⣿⠀⣰⡟⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⣿⡟⢸⣿⡇⢀⣿",
-  indent
-    .. "⣸⡇⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⣿⡟⠀⢠⣿⡇⠀⠀⢰⣿⡇⠀⣰⣾⠟⠛⠛⣻⡇⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⢻⣿⢰⣿⠀⠀⠀⠀⠀⠀⣾⡇⠀⠀⠀⢸⣿⠇⢸⣿⠀⢸⡏",
-  indent
-    .. "⣿⣧⣤⣤⣤⡄⠀⠘⣿⣤⣤⡤⣿⠇⠀⢸⣿⠁⠀⠀⣼⣿⠀⠀⢿⣿⣤⣤⠔⣿⠃⠀⠀⣾⡇⠀⠀⠀⠀⠀⠀⢸⣿⣿⠋⠀⠀⠀⢠⣤⣤⣿⣥⣤⡄⠀⣼⣿⠀⣸⡏⠀⣿⠃",
-  indent
-    .. "⠉⠉⠉⠉⠉⠁⠀⠀⠈⠉⠉⠀⠉⠀⠀⠈⠉⠀⠀⠀⠉⠉⠀⠀⠀⠉⠉⠁⠈⠉⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠁⠀⠉⠁⠀⠉⠁⠀⠉⠀",
-  "",
+local M = {
+  banner = {
+    "",
+    [[    __                          _    ___         ]],
+    [[   / /   __  ______  ____ _____| |  / (_)___ ___ ]],
+    [[  / /   / / / / __ \/ __ `/ ___/ | / / / __ `__ \]],
+    [[ / /___/ /_/ / / / / /_/ / /   | |/ / / / / / / /]],
+    [[/_____/\__,_/_/ /_/\__,_/_/    |___/_/_/ /_/ /_/ ]],
+  },
 }
 
+local fmt = string.format
+
 local function str_list(list)
-  return "[ " .. table.concat(list, ", ") .. " ]"
+  return fmt("[ %s ]", table.concat(list, ", "))
 end
 
 local function get_formatter_suggestion_msg(ft)
   local null_formatters = require "lsp.null-ls.formatters"
   local supported_formatters = null_formatters.list_available(ft)
-  return {
-    indent
-      .. "───────────────────────────────────────────────────────────────────",
-    "",
-    indent .. " HINT ",
-    "",
-    indent .. "* List of supported formatters: " .. str_list(supported_formatters),
-    indent .. "* Configured formatter needs to be installed and executable.",
-    indent .. "* Enable installed formatter(s) with following config in ~/.config/lvim/config.lua",
-    "",
-    indent .. "  lvim.lang." .. tostring(ft) .. [[.formatters = { { exe = ']] .. table.concat(
-      supported_formatters,
-      "│"
-    ) .. [[' } }]],
+  local section = {
+    " HINT ",
     "",
+    fmt("* List of supported formatters: %s", str_list(supported_formatters)),
   }
+
+  if not vim.tbl_isempty(supported_formatters) then
+    vim.list_extend(section, {
+      "* Configured formatter needs to be installed and executable.",
+      fmt("* Enable installed formatter(s) with following config in %s", USER_CONFIG_PATH),
+      "",
+      fmt("  lvim.lang.%s.formatters = { { exe = '%s' } }", ft, table.concat(supported_formatters, "│")),
+    })
+  end
+
+  return section
 end
 
 local function get_linter_suggestion_msg(ft)
   local null_linters = require "lsp.null-ls.linters"
   local supported_linters = null_linters.list_available(ft)
-  return {
-    indent
-      .. "───────────────────────────────────────────────────────────────────",
-    "",
-    indent .. " HINT ",
-    "",
-    indent .. "* List of supported linters: " .. str_list(supported_linters),
-    indent .. "* Configured linter needs to be installed and executable.",
-    indent .. "* Enable installed linter(s) with following config in ~/.config/lvim/config.lua",
-    "",
-    indent
-      .. "  lvim.lang."
-      .. tostring(ft)
-      .. [[.linters = { { exe = ']]
-      .. table.concat(supported_linters, "│")
-      .. [[' } }]],
+  local section = {
+    " HINT ",
     "",
+    fmt("* List of supported linters: %s", str_list(supported_linters)),
   }
-end
 
----creates an average size popup
----@param buf_lines a list of lines to print
----@param callback could be used to set syntax highlighting rules for example
----@return bufnr buffer number of the created buffer
----@return win_id window ID of the created popup
-function M.create_simple_popup(buf_lines, callback)
-  -- runtime/lua/vim/lsp/util.lua
-  local bufnr = vim.api.nvim_create_buf(false, true)
-  local height_percentage = 0.9
-  local width_percentage = 0.8
-  local row_start_percentage = (1 - height_percentage) / 2
-  local col_start_percentage = (1 - width_percentage) / 2
-  local opts = {}
-  opts.relative = "editor"
-  opts.height = math.min(math.ceil(vim.o.lines * height_percentage), #buf_lines)
-  opts.row = math.ceil(vim.o.lines * row_start_percentage)
-  opts.col = math.floor(vim.o.columns * col_start_percentage)
-  opts.width = math.floor(vim.o.columns * width_percentage)
-  opts.style = "minimal"
-  opts.border = "rounded"
-  --[[
-  opts.border = {
-    lvim.builtin.telescope.defaults.borderchars[5], -- "┌",
-    lvim.builtin.telescope.defaults.borderchars[3], -- "-",
-    lvim.builtin.telescope.defaults.borderchars[6], -- "┐",
-    lvim.builtin.telescope.defaults.borderchars[2], -- "|",
-    lvim.builtin.telescope.defaults.borderchars[7], -- "┘",
-    lvim.builtin.telescope.defaults.borderchars[3], -- "-",
-    lvim.builtin.telescope.defaults.borderchars[8], -- "└",
-    lvim.builtin.telescope.defaults.borderchars[4], -- "|",
-  }
-  --]]
-
-  local win_id = vim.api.nvim_open_win(bufnr, true, opts)
-
-  vim.api.nvim_win_set_buf(win_id, bufnr)
-  -- this needs to be window option!
-  vim.api.nvim_win_set_option(win_id, "number", false)
-  vim.cmd "setlocal nocursorcolumn"
-  vim.cmd "setlocal wrap"
-  -- set buffer options
-  vim.api.nvim_buf_set_option(bufnr, "filetype", "lspinfo")
-  vim.lsp.util.close_preview_autocmd({ "BufHidden", "BufLeave" }, win_id)
-  buf_lines = vim.lsp.util._trim(buf_lines, {})
-  vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, buf_lines)
-  vim.api.nvim_buf_set_option(bufnr, "modifiable", false)
-  if type(callback) == "function" then
-    callback()
+  if not vim.tbl_isempty(supported_linters) then
+    vim.list_extend(section, {
+      "* Configured linter needs to be installed and executable.",
+      fmt("* Enable installed linter(s) with following config in %s", USER_CONFIG_PATH),
+      "",
+      fmt("  lvim.lang.%s.linters = { { exe = '%s' } }", ft, table.concat(supported_linters, "│")),
+    })
   end
-  return bufnr, win_id
+
+  return section
 end
 
 local function tbl_set_highlight(terms, highlight_group)
-  if type(terms) ~= "table" then
-    return
-  end
-
   for _, v in pairs(terms) do
     vim.cmd('let m=matchadd("' .. highlight_group .. '", "' .. v .. "[ ,│']\")")
   end
@@ -136,67 +71,90 @@ function M.toggle_popup(ft)
   local client_name = ""
   local client_id = 0
   local document_formatting = false
-  local num_caps = 0
   if client ~= nil then
     is_client_active = not client.is_stopped()
     client_enabled_caps = require("lsp").get_ls_capabilities(client.id)
-    num_caps = vim.tbl_count(client_enabled_caps)
     client_name = client.name
     client_id = client.id
     document_formatting = client.resolved_capabilities.document_formatting
   end
 
-  local buf_lines = {}
-  vim.list_extend(buf_lines, M.banner)
-
   local header = {
-    indent .. "Detected filetype:     " .. tostring(ft),
-    indent .. "Treesitter active:     " .. tostring(next(vim.treesitter.highlighter.active) ~= nil),
-    "",
+    fmt("Detected filetype:      %s", ft),
+    fmt("Treesitter active:      %s", tostring(next(vim.treesitter.highlighter.active) ~= nil)),
   }
-  vim.list_extend(buf_lines, header)
 
+  local text = require "interface.text"
   local lsp_info = {
-    indent .. "Language Server Protocol (LSP) info",
-    indent .. "* Associated server:   " .. client_name,
-    indent .. "* Active:              " .. tostring(is_client_active) .. " (id: " .. tostring(client_id) .. ")",
-    indent .. "* Supports formatting: " .. tostring(document_formatting),
-    indent .. "* Capabilities list:   " .. table.concat(vim.list_slice(client_enabled_caps, 1, num_caps / 2), ", "),
-    indent .. indent .. indent .. table.concat(vim.list_slice(client_enabled_caps, ((num_caps / 2) + 1)), ", "),
-    "",
+    "Language Server Protocol (LSP) info",
+    fmt("* Associated server:    %s", client_name),
+    fmt("* Active:               %s (id: %d)", tostring(is_client_active), client_id),
+    fmt("* Supports formatting:  %s", tostring(document_formatting)),
   }
-  vim.list_extend(buf_lines, lsp_info)
-
+  if not vim.tbl_isempty(client_enabled_caps) then
+    local caps_text = "* Capabilities list:    "
+    local caps_text_len = caps_text:len()
+    local enabled_caps = text.format_table(client_enabled_caps, 3, " | ")
+    enabled_caps = text.shift_left(enabled_caps, caps_text_len)
+    enabled_caps[1] = fmt("%s%s", caps_text, enabled_caps[1]:sub(caps_text_len + 1))
+    vim.list_extend(lsp_info, enabled_caps)
+  end
   local null_ls = require "lsp.null-ls"
   local registered_providers = null_ls.list_supported_provider_names(ft)
+  local registered_count = vim.tbl_count(registered_providers)
   local null_ls_info = {
-    indent .. "Formatters and linters",
-    indent .. "* Configured providers: " .. table.concat(registered_providers, "  , ") .. "  ",
+    "Formatters and linters",
+    fmt(
+      "* Configured providers: %s%s",
+      table.concat(registered_providers, "  , "),
+      registered_count > 0 and "  " or ""
+    ),
   }
-  vim.list_extend(buf_lines, null_ls_info)
 
   local null_formatters = require "lsp.null-ls.formatters"
   local missing_formatters = null_formatters.list_unsupported_names(ft)
-  if vim.tbl_count(missing_formatters) > 0 then
-    local missing_formatters_status = {
-      indent .. "* Missing formatters:   " .. table.concat(missing_formatters, "  , ") .. "  ",
+  local missing_formatters_status = {}
+  if not vim.tbl_isempty(missing_formatters) then
+    missing_formatters_status = {
+      fmt("* Missing formatters:   %s", table.concat(missing_formatters, "  , ") .. "  "),
     }
-    vim.list_extend(buf_lines, missing_formatters_status)
   end
 
   local null_linters = require "lsp.null-ls.linters"
   local missing_linters = null_linters.list_unsupported_names(ft)
-  if vim.tbl_count(missing_linters) > 0 then
-    local missing_linters_status = {
-      indent .. "* Missing linters:      " .. table.concat(missing_linters, "  , ") .. "  ",
+  local missing_linters_status = {}
+  if not vim.tbl_isempty(missing_linters) then
+    missing_linters_status = {
+      fmt("* Missing linters:      %s", table.concat(missing_linters, "  , ") .. "  "),
     }
-    vim.list_extend(buf_lines, missing_linters_status)
   end
 
-  vim.list_extend(buf_lines, { "" })
-
-  vim.list_extend(buf_lines, get_formatter_suggestion_msg(ft))
-  vim.list_extend(buf_lines, get_linter_suggestion_msg(ft))
+  local content_provider = function(popup)
+    local content = {}
+
+    for _, section in ipairs {
+      M.banner,
+      { "" },
+      { "" },
+      header,
+      { "" },
+      lsp_info,
+      { "" },
+      null_ls_info,
+      missing_formatters_status,
+      missing_linters_status,
+      { "" },
+      { "" },
+      get_formatter_suggestion_msg(ft),
+      { "" },
+      { "" },
+      get_linter_suggestion_msg(ft),
+    } do
+      vim.list_extend(content, section)
+    end
+
+    return text.align(popup, content, 0.5)
+  end
 
   local function set_syntax_hl()
     vim.cmd [[highlight LvimInfoIdentifier gui=bold]]
@@ -214,6 +172,13 @@ function M.toggle_popup(ft)
     vim.cmd('let m=matchadd("LvimInfoIdentifier", "' .. client_name .. '")')
   end
 
-  return M.create_simple_popup(buf_lines, set_syntax_hl)
+  local Popup = require("interface.popup"):new {
+    win_opts = { number = false },
+    buf_opts = { modifiable = false, filetype = "lspinfo" },
+  }
+  Popup:display(content_provider)
+  set_syntax_hl()
+
+  return Popup
 end
 return M

+ 62 - 0
lua/interface/popup.lua

@@ -0,0 +1,62 @@
+local Popup = {}
+
+--- Create a new floating window
+-- @param config The configuration passed to vim.api.nvim_open_win
+-- @param win_opts The options registered with vim.api.nvim_win_set_option
+-- @param buf_opts The options registered with vim.api.nvim_buf_set_option
+-- @return A new popup
+function Popup:new(opts)
+  opts = opts or {}
+  opts.layout = opts.layout or {}
+  opts.win_opts = opts.win_opts or {}
+  opts.buf_opts = opts.buf_opts or {}
+
+  Popup.__index = Popup
+
+  local editor_layout = {
+    height = vim.o.lines - vim.o.cmdheight - 2, -- Add margin for status and buffer line
+    width = vim.o.columns,
+  }
+  local popup_layout = {
+    relative = "editor",
+    height = math.floor(editor_layout.height * 0.9),
+    width = math.floor(editor_layout.width * 0.8),
+    style = "minimal",
+    border = "rounded",
+  }
+  popup_layout.row = math.floor((editor_layout.height - popup_layout.height) / 2)
+  popup_layout.col = math.floor((editor_layout.width - popup_layout.width) / 2)
+
+  local obj = {
+    buffer = vim.api.nvim_create_buf(false, true),
+    layout = vim.tbl_deep_extend("force", popup_layout, opts.layout),
+    win_opts = opts.win_opts,
+    buf_opts = opts.buf_opts,
+  }
+
+  setmetatable(obj, Popup)
+
+  return obj
+end
+
+--- Display the popup with the provided content
+-- @param content_provider A function accepting the popup's layout and returning the content to display
+function Popup:display(content_provider)
+  self.win_id = vim.api.nvim_open_win(self.buffer, true, self.layout)
+  vim.lsp.util.close_preview_autocmd({ "BufHidden", "BufLeave" }, self.win_id)
+
+  local lines = content_provider(self.layout)
+  vim.api.nvim_buf_set_lines(self.bufnr, 0, -1, false, lines)
+
+  -- window options
+  for key, value in pairs(self.win_opts) do
+    vim.api.nvim_win_set_option(self.win_id, key, value)
+  end
+
+  -- buffer options
+  for key, value in pairs(self.buf_opts) do
+    vim.api.nvim_buf_set_option(self.buffer, key, value)
+  end
+end
+
+return Popup

+ 79 - 0
lua/interface/text.lua

@@ -0,0 +1,79 @@
+local M = {}
+
+local function max_len_line(lines)
+  local max_len = 0
+
+  for _, line in ipairs(lines) do
+    local line_len = line:len()
+    if line_len > max_len then
+      max_len = line_len
+    end
+  end
+
+  return max_len
+end
+
+--- Center align lines relatively to the parent container
+-- @param container The container where lines will be displayed
+-- @param lines The text to align
+-- @param alignment The alignment value, range: [0-1]
+function M.align(container, lines, alignment)
+  local max_len = max_len_line(lines)
+  local indent_amount = math.ceil(math.max(container.width - max_len, 0) * alignment)
+  return M.shift_left(lines, indent_amount)
+end
+
+--- Shift lines by a given amount
+-- @params lines The lines the shift
+-- @param amount The amount of spaces to add
+function M.shift_left(lines, amount)
+  local output = {}
+  local padding = string.rep(" ", amount)
+
+  for _, line in ipairs(lines) do
+    table.insert(output, padding .. line)
+  end
+
+  return output
+end
+
+--- Pretty format tables
+-- @param entries The table to format
+-- @param col_count The number of column to span the table on
+-- @param col_sep The separator between each colummn, default: " "
+function M.format_table(entries, col_count, col_sep)
+  col_sep = col_sep or " "
+
+  local col_rows = math.ceil(vim.tbl_count(entries) / col_count)
+  local cols = {}
+  local count = 0
+
+  for i, entry in ipairs(entries) do
+    if ((i - 1) % col_rows) == 0 then
+      table.insert(cols, {})
+      count = count + 1
+    end
+    table.insert(cols[count], entry)
+  end
+
+  local col_max_len = {}
+  for _, col in ipairs(cols) do
+    table.insert(col_max_len, max_len_line(col))
+  end
+
+  local output = {}
+  for i, col in ipairs(cols) do
+    for j, entry in ipairs(col) do
+      if not output[j] then
+        output[j] = entry
+      else
+        local padding = string.rep(" ", col_max_len[i - 1] - cols[i - 1][j]:len())
+        output[j] = output[j] .. padding .. col_sep .. entry
+      end
+    end
+  end
+
+  return output
+end
+
+return M