diff options
Diffstat (limited to 'lua/lvim')
55 files changed, 5531 insertions, 0 deletions
| diff --git a/lua/lvim/bootstrap.lua b/lua/lvim/bootstrap.lua new file mode 100644 index 00000000..46c044ae --- /dev/null +++ b/lua/lvim/bootstrap.lua @@ -0,0 +1,196 @@ +local M = {} + +package.loaded["lvim.utils.hooks"] = nil +local _, hooks = pcall(require, "lvim.utils.hooks") + +---Join path segments that were passed as input +---@return string +function _G.join_paths(...) +  local uv = vim.loop +  local path_sep = uv.os_uname().version:match "Windows" and "\\" or "/" +  local result = table.concat({ ... }, path_sep) +  return result +end + +---Get the full path to `$LUNARVIM_RUNTIME_DIR` +---@return string +function _G.get_runtime_dir() +  local lvim_runtime_dir = os.getenv "LUNARVIM_RUNTIME_DIR" +  if not lvim_runtime_dir then +    -- when nvim is used directly +    return vim.fn.stdpath "config" +  end +  return lvim_runtime_dir +end + +---Get the full path to `$LUNARVIM_CONFIG_DIR` +---@return string +function _G.get_config_dir() +  local lvim_config_dir = os.getenv "LUNARVIM_CONFIG_DIR" +  if not lvim_config_dir then +    return vim.fn.stdpath "config" +  end +  return lvim_config_dir +end + +---Get the full path to `$LUNARVIM_CACHE_DIR` +---@return string +function _G.get_cache_dir() +  local lvim_cache_dir = os.getenv "LUNARVIM_CACHE_DIR" +  if not lvim_cache_dir then +    return vim.fn.stdpath "cache" +  end +  return lvim_cache_dir +end + +---Get the full path to the currently installed lunarvim repo +---@return string +local function get_install_path() +  local lvim_runtime_dir = os.getenv "LUNARVIM_RUNTIME_DIR" +  if not lvim_runtime_dir then +    -- when nvim is used directly +    return vim.fn.stdpath "config" +  end +  return join_paths(lvim_runtime_dir, "lvim") +end + +---Get currently installed version of LunarVim +---@param type string can be "short" +---@return string +function _G.get_version(type) +  type = type or "" +  local lvim_full_ver = vim.fn.system("git -C " .. get_install_path() .. " describe --tags") + +  if string.match(lvim_full_ver, "%d") == nil then +    return nil +  end +  if type == "short" then +    return vim.fn.split(lvim_full_ver, "-")[1] +  else +    return string.sub(lvim_full_ver, 1, #lvim_full_ver - 1) +  end +end + +---Initialize the `&runtimepath` variables and prepare for startup +---@return table +function M:init() +  self.runtime_dir = get_runtime_dir() +  self.config_dir = get_config_dir() +  self.cache_path = get_cache_dir() +  self.install_path = get_install_path() + +  self.pack_dir = join_paths(self.runtime_dir, "site", "pack") +  self.packer_install_dir = join_paths(self.runtime_dir, "site", "pack", "packer", "start", "packer.nvim") +  self.packer_cache_path = join_paths(self.config_dir, "plugin", "packer_compiled.lua") + +  if os.getenv "LUNARVIM_RUNTIME_DIR" then +    vim.opt.rtp:remove(join_paths(vim.fn.stdpath "data", "site")) +    vim.opt.rtp:remove(join_paths(vim.fn.stdpath "data", "site", "after")) +    vim.opt.rtp:prepend(join_paths(self.runtime_dir, "site")) +    vim.opt.rtp:append(join_paths(self.runtime_dir, "site", "after")) + +    vim.opt.rtp:remove(vim.fn.stdpath "config") +    vim.opt.rtp:remove(join_paths(vim.fn.stdpath "config", "after")) +    vim.opt.rtp:prepend(self.config_dir) +    vim.opt.rtp:append(join_paths(self.config_dir, "after")) +    -- TODO: we need something like this: vim.opt.packpath = vim.opt.rtp + +    vim.cmd [[let &packpath = &runtimepath]] +    vim.cmd("set spellfile=" .. join_paths(self.config_dir, "spell", "en.utf-8.add")) +  end + +  vim.fn.mkdir(get_cache_dir(), "p") + +  -- FIXME: currently unreliable in unit-tests +  if not os.getenv "LVIM_TEST_ENV" then +    require("lvim.impatient").setup { +      path = vim.fn.stdpath "cache" .. "/lvim_cache", +      enable_profiling = true, +    } +  end + +  require("lvim.config"):init { +    config_dir = self.config_dir, +  } +  local config = require "lvim.config" +  config:init { +    user_config = join_paths(self.config_dir, "config.lua"), +  } + +  require("lvim.plugin-loader"):init { +    package_root = self.pack_dir, +    install_path = self.packer_install_dir, +  } + +  return self +end + +---Update LunarVim +---pulls the latest changes from github and, resets the startup cache +function M:update() +  hooks.run_pre_update() +  M:update_repo() +  hooks.run_post_update() +end + +local function git_cmd(subcmd) +  local Job = require "plenary.job" +  local Log = require "lvim.core.log" +  local args = { "-C", get_install_path() } +  vim.list_extend(args, subcmd) + +  local stderr = {} +  local stdout, ret = Job +    :new({ +      command = "git", +      args = args, +      cwd = get_install_path(), +      on_stderr = function(_, data) +        table.insert(stderr, data) +      end, +    }) +    :sync() + +  if not vim.tbl_isempty(stderr) then +    Log:debug(stderr) +  end + +  if not vim.tbl_isempty(stdout) then +    Log:debug(stdout) +  end + +  return ret +end + +---pulls the latest changes from github +function M:update_repo() +  local Log = require "lvim.core.log" +  local sub_commands = { +    fetch = { "fetch" }, +    diff = { "diff", "--quiet", "@{upstream}" }, +    merge = { "merge", "--ff-only", "--progress" }, +  } +  Log:info "Checking for updates" + +  local ret = git_cmd(sub_commands.fetch) +  if ret ~= 0 then +    Log:error "Update failed! Check the log for further information" +    return +  end + +  ret = git_cmd(sub_commands.diff) + +  if ret == 0 then +    Log:info "LunarVim is already up-to-date" +    return +  end + +  ret = git_cmd(sub_commands.merge) + +  if ret ~= 0 then +    Log:error "Update failed! Please pull the changes manually instead." +    return +  end +end + +return M diff --git a/lua/lvim/config/defaults.lua b/lua/lvim/config/defaults.lua new file mode 100644 index 00000000..ffbb2e1b --- /dev/null +++ b/lua/lvim/config/defaults.lua @@ -0,0 +1,32 @@ +return { +  leader = "space", +  colorscheme = "onedarker", +  line_wrap_cursor_movement = true, +  transparent_window = false, +  format_on_save = true, +  keys = {}, + +  builtin = {}, + +  log = { +    ---@usage can be { "trace", "debug", "info", "warn", "error", "fatal" }, +    level = "warn", +    viewer = { +      ---@usage this will fallback on "less +F" if not found +      cmd = "lnav", +      layout_config = { +        ---@usage direction = 'vertical' | 'horizontal' | 'window' | 'float', +        direction = "horizontal", +        open_mapping = "", +        size = 40, +        float_opts = {}, +      }, +    }, +  }, +  plugins = { +    -- use config.lua for this not put here +  }, + +  autocommands = {}, +  lang = {}, +} diff --git a/lua/lvim/config/init.lua b/lua/lvim/config/init.lua new file mode 100644 index 00000000..d7877f1e --- /dev/null +++ b/lua/lvim/config/init.lua @@ -0,0 +1,202 @@ +local utils = require "lvim.utils" +local Log = require "lvim.core.log" + +local M = {} + +local user_config_dir = get_config_dir() +local user_config_file = utils.join_paths(user_config_dir, "config.lua") + +-- Fallback config.lua to lv-config.lua +if not utils.is_file(user_config_file) then +  local lv_config = utils.join_paths(user_config_dir, "lv-config.lua") +  Log:warn(string.format("[%s] not found, falling back to [%s]", user_config_file, lv_config)) +  user_config_file = lv_config +end + +function M:get_user_config_path() +  return user_config_file +end + +--- Initialize lvim default configuration +-- Define lvim global variable +function M:init() +  if vim.tbl_isempty(lvim or {}) then +    lvim = require "lvim.config.defaults" +    local home_dir = vim.loop.os_homedir() +    lvim.vsnip_dir = utils.join_paths(home_dir, ".config", "snippets") +    lvim.database = { save_location = utils.join_paths(home_dir, ".config", "lunarvim_db"), auto_execute = 1 } +  end + +  local builtins = require "lvim.core.builtins" +  builtins.config { user_config_file = user_config_file } + +  local settings = require "lvim.config.settings" +  settings.load_options() + +  local lvim_lsp_config = require "lvim.lsp.config" +  lvim.lsp = vim.deepcopy(lvim_lsp_config) + +  local supported_languages = { +    "asm", +    "bash", +    "beancount", +    "bibtex", +    "bicep", +    "c", +    "c_sharp", +    "clojure", +    "cmake", +    "comment", +    "commonlisp", +    "cpp", +    "crystal", +    "cs", +    "css", +    "cuda", +    "d", +    "dart", +    "dockerfile", +    "dot", +    "elixir", +    "elm", +    "emmet", +    "erlang", +    "fennel", +    "fish", +    "fortran", +    "gdscript", +    "glimmer", +    "go", +    "gomod", +    "graphql", +    "haskell", +    "hcl", +    "heex", +    "html", +    "java", +    "javascript", +    "javascriptreact", +    "jsdoc", +    "json", +    "json5", +    "jsonc", +    "julia", +    "kotlin", +    "latex", +    "ledger", +    "less", +    "lua", +    "markdown", +    "nginx", +    "nix", +    "ocaml", +    "ocaml_interface", +    "perl", +    "php", +    "pioasm", +    "ps1", +    "puppet", +    "python", +    "ql", +    "query", +    "r", +    "regex", +    "rst", +    "ruby", +    "rust", +    "scala", +    "scss", +    "sh", +    "solidity", +    "sparql", +    "sql", +    "supercollider", +    "surface", +    "svelte", +    "swift", +    "tailwindcss", +    "terraform", +    "tex", +    "tlaplus", +    "toml", +    "tsx", +    "turtle", +    "typescript", +    "typescriptreact", +    "verilog", +    "vim", +    "vue", +    "yaml", +    "yang", +    "zig", +  } + +  require("lvim.lsp.manager").init_defaults(supported_languages) +end + +local function deprecation_notice() +  local in_headless = #vim.api.nvim_list_uis() == 0 +  if in_headless then +    return +  end + +  for lang, entry in pairs(lvim.lang) do +    local deprecated_config = entry["lvim.lsp"] or {} +    if not vim.tbl_isempty(deprecated_config) then +      local msg = string.format( +        "Deprecation notice: [lvim.lang.%s.lsp] setting is no longer supported. See https://github.com/LunarVim/LunarVim#breaking-changes", +        lang +      ) +      vim.schedule(function() +        vim.notify(msg, vim.log.levels.WARN) +      end) +    end +  end +end + +--- Override the configuration with a user provided one +-- @param config_path The path to the configuration overrides +function M:load(config_path) +  config_path = config_path or self.get_user_config_path() +  local ok, _ = pcall(dofile, config_path) +  if not ok then +    Log:warn("Invalid configuration: " .. config_path) +  end + +  deprecation_notice() + +  local autocmds = require "lvim.core.autocmds" +  autocmds.define_augroups(lvim.autocommands) + +  local settings = require "lvim.config.settings" +  settings.load_commands() +end + +--- Override the configuration with a user provided one +-- @param config_path The path to the configuration overrides +function M:reload() +  local lvim_modules = {} +  for module, _ in pairs(package.loaded) do +    if module:match "lvim" then +      package.loaded.module = nil +      table.insert(lvim_modules, module) +    end +  end + +  M:init() +  M:load() + +  require("lvim.keymappings").setup() -- this should be done before loading the plugins +  local plugins = require "lvim.plugins" +  utils.toggle_autoformat() +  local plugin_loader = require "lvim.plugin-loader" +  plugin_loader:cache_reset() +  plugin_loader:load { plugins, lvim.plugins } +  vim.cmd ":PackerInstall" +  vim.cmd ":PackerCompile" +  -- vim.cmd ":PackerClean" +  require("lvim.lsp").setup() +  Log:info "Reloaded configuration" +end + +return M diff --git a/lua/lvim/config/settings.lua b/lua/lvim/config/settings.lua new file mode 100644 index 00000000..b86e1a18 --- /dev/null +++ b/lua/lvim/config/settings.lua @@ -0,0 +1,76 @@ +local M = {} +local utils = require "lvim.utils" +M.load_options = function() +  local default_options = { +    backup = false, -- creates a backup file +    clipboard = "unnamedplus", -- allows neovim to access the system clipboard +    cmdheight = 2, -- more space in the neovim command line for displaying messages +    colorcolumn = "99999", -- fixes indentline for now +    completeopt = { "menuone", "noselect" }, +    conceallevel = 0, -- so that `` is visible in markdown files +    fileencoding = "utf-8", -- the encoding written to a file +    foldmethod = "manual", -- folding, set to "expr" for treesitter based folding +    foldexpr = "", -- set to "nvim_treesitter#foldexpr()" for treesitter based folding +    guifont = "monospace:h17", -- the font used in graphical neovim applications +    hidden = true, -- required to keep multiple buffers and open multiple buffers +    hlsearch = true, -- highlight all matches on previous search pattern +    ignorecase = true, -- ignore case in search patterns +    mouse = "a", -- allow the mouse to be used in neovim +    pumheight = 10, -- pop up menu height +    showmode = false, -- we don't need to see things like -- INSERT -- anymore +    showtabline = 2, -- always show tabs +    smartcase = true, -- smart case +    smartindent = true, -- make indenting smarter again +    splitbelow = true, -- force all horizontal splits to go below current window +    splitright = true, -- force all vertical splits to go to the right of current window +    swapfile = false, -- creates a swapfile +    termguicolors = true, -- set term gui colors (most terminals support this) +    timeoutlen = 100, -- time to wait for a mapped sequence to complete (in milliseconds) +    title = true, -- set the title of window to the value of the titlestring +    -- opt.titlestring = "%<%F%=%l/%L - nvim" -- what the title of the window will be set to +    undodir = utils.join_paths(get_cache_dir(), "undo"), -- set an undo directory +    undofile = true, -- enable persistent undo +    updatetime = 300, -- faster completion +    writebackup = false, -- if a file is being edited by another program (or was written to file while editing with another program), it is not allowed to be edited +    expandtab = true, -- convert tabs to spaces +    shiftwidth = 2, -- the number of spaces inserted for each indentation +    tabstop = 2, -- insert 2 spaces for a tab +    cursorline = true, -- highlight the current line +    number = true, -- set numbered lines +    relativenumber = false, -- set relative numbered lines +    numberwidth = 4, -- set number column width to 2 {default 4} +    signcolumn = "yes", -- always show the sign column, otherwise it would shift the text each time +    wrap = false, -- display lines as one long line +    spell = false, +    spelllang = "en", +    scrolloff = 8, -- is one of my fav +    sidescrolloff = 8, +  } + +  ---  SETTINGS  --- + +  vim.opt.shortmess:append "c" + +  for k, v in pairs(default_options) do +    vim.opt[k] = v +  end +end + +M.load_commands = function() +  local cmd = vim.cmd +  if lvim.line_wrap_cursor_movement then +    cmd "set whichwrap+=<,>,[,],h,l" +  end + +  if lvim.transparent_window then +    cmd "au ColorScheme * hi Normal ctermbg=none guibg=none" +    cmd "au ColorScheme * hi SignColumn ctermbg=none guibg=none" +    cmd "au ColorScheme * hi NormalNC ctermbg=none guibg=none" +    cmd "au ColorScheme * hi MsgArea ctermbg=none guibg=none" +    cmd "au ColorScheme * hi TelescopeBorder ctermbg=none guibg=none" +    cmd "au ColorScheme * hi NvimTreeNormal ctermbg=none guibg=none" +    cmd "let &fcs='eob: '" +  end +end + +return M diff --git a/lua/lvim/core/autocmds.lua b/lua/lvim/core/autocmds.lua new file mode 100644 index 00000000..4f29f0e1 --- /dev/null +++ b/lua/lvim/core/autocmds.lua @@ -0,0 +1,129 @@ +local autocommands = {} +local user_config_file = require("lvim.config"):get_user_config_path() + +lvim.autocommands = { +  _general_settings = { +    { +      "Filetype", +      "*", +      "lua require('lvim.utils.ft').do_filetype(vim.fn.expand(\"<amatch>\"))", +    }, +    { +      "FileType", +      "qf", +      "nnoremap <silent> <buffer> q :q<CR>", +    }, +    { +      "FileType", +      "lsp-installer", +      "nnoremap <silent> <buffer> q :q<CR>", +    }, +    { +      "TextYankPost", +      "*", +      "lua require('vim.highlight').on_yank({higroup = 'Search', timeout = 200})", +    }, +    { +      "BufWinEnter", +      "*", +      "setlocal formatoptions-=c formatoptions-=r formatoptions-=o", +    }, +    { +      "BufWinEnter", +      "dashboard", +      "setlocal cursorline signcolumn=yes cursorcolumn number", +    }, +    { +      "BufRead", +      "*", +      "setlocal formatoptions-=c formatoptions-=r formatoptions-=o", +    }, +    { +      "BufNewFile", +      "*", +      "setlocal formatoptions-=c formatoptions-=r formatoptions-=o", +    }, +    { "BufWritePost", user_config_file, "lua require('lvim.config'):reload()" }, +    { +      "FileType", +      "qf", +      "set nobuflisted", +    }, +    -- { "VimLeavePre", "*", "set title set titleold=" }, +  }, +  _filetypechanges = { +    { "BufWinEnter", ".tf", "setlocal filetype=terraform" }, +    { "BufRead", "*.tf", "setlocal filetype=terraform" }, +    { "BufNewFile", "*.tf", "setlocal filetype=terraform" }, +    { "BufWinEnter", ".zsh", "setlocal filetype=sh" }, +    { "BufRead", "*.zsh", "setlocal filetype=sh" }, +    { "BufNewFile", "*.zsh", "setlocal filetype=sh" }, +  }, +  -- _solidity = { +  --     {'BufWinEnter', '.sol', 'setlocal filetype=solidity'}, {'BufRead', '*.sol', 'setlocal filetype=solidity'}, +  --     {'BufNewFile', '*.sol', 'setlocal filetype=solidity'} +  -- }, +  -- _gemini = { +  --     {'BufWinEnter', '.gmi', 'setlocal filetype=markdown'}, {'BufRead', '*.gmi', 'setlocal filetype=markdown'}, +  --     {'BufNewFile', '*.gmi', 'setlocal filetype=markdown'} +  -- }, +  _git = { +    { "FileType", "gitcommit", "setlocal wrap" }, +    { "FileType", "gitcommit", "setlocal spell" }, +  }, +  _markdown = { +    { "FileType", "markdown", "setlocal wrap" }, +    { "FileType", "markdown", "setlocal spell" }, +  }, +  _buffer_bindings = { +    { "FileType", "floaterm", "nnoremap <silent> <buffer> q :q<CR>" }, +  }, +  _auto_resize = { +    -- will cause split windows to be resized evenly if main window is resized +    { "VimResized", "*", "wincmd =" }, +  }, +  _packer_compile = { +    -- will run PackerCompile after writing plugins.lua +    { "BufWritePost", "plugins.lua", "PackerCompile" }, +  }, +  _general_lsp = { +    { "FileType", "lspinfo", "nnoremap <silent> <buffer> q :q<CR>" }, +  }, + +  -- _fterm_lazygit = { +  --   -- will cause esc key to exit lazy git +  --   {"TermEnter", "*", "call LazyGitNativation()"} +  -- }, +  -- _mode_switching = { +  --   -- will switch between absolute and relative line numbers depending on mode +  --   {'InsertEnter', '*', 'if &relativenumber | let g:ms_relativenumberoff = 1 | setlocal number norelativenumber | endif'}, +  --   {'InsertLeave', '*', 'if exists("g:ms_relativenumberoff") | setlocal relativenumber | endif'}, +  --   {'InsertEnter', '*', 'if &cursorline | let g:ms_cursorlineoff = 1 | setlocal nocursorline | endif'}, +  --   {'InsertLeave', '*', 'if exists("g:ms_cursorlineoff") | setlocal cursorline | endif'}, +  -- }, +  custom_groups = {}, +} + +function autocommands.define_augroups(definitions) -- {{{1 +  -- Create autocommand groups based on the passed definitions +  -- +  -- The key will be the name of the group, and each definition +  -- within the group should have: +  --    1. Trigger +  --    2. Pattern +  --    3. Text +  -- just like how they would normally be defined from Vim itself +  for group_name, definition in pairs(definitions) do +    vim.cmd("augroup " .. group_name) +    vim.cmd "autocmd!" + +    for _, def in pairs(definition) do +      local command = table.concat(vim.tbl_flatten { "autocmd", def }, " ") +      vim.cmd(command) +    end + +    vim.cmd "augroup END" +  end +end + +return autocommands diff --git a/lua/lvim/core/autopairs.lua b/lua/lvim/core/autopairs.lua new file mode 100644 index 00000000..eb080fb1 --- /dev/null +++ b/lua/lvim/core/autopairs.lua @@ -0,0 +1,81 @@ +local M = {} + +function M.config() +  lvim.builtin.autopairs = { +    active = true, +    on_config_done = nil, +    ---@usage auto insert after select function or method item +    map_complete = true, +    ---@usage  -- modifies the function or method delimiter by filetypes +    map_char = { +      all = "(", +      tex = "{", +    }, +    ---@usage check treesitter +    check_ts = true, +    ts_config = { +      lua = { "string" }, +      javascript = { "template_string" }, +      java = false, +    }, +  } +end + +M.setup = function() +  local autopairs = require "nvim-autopairs" +  local Rule = require "nvim-autopairs.rule" +  local cond = require "nvim-autopairs.conds" + +  autopairs.setup { +    check_ts = lvim.builtin.autopairs.check_ts, +    ts_config = lvim.builtin.autopairs.ts_config, +  } + +  -- vim.g.completion_confirm_key = "" + +  autopairs.add_rule(Rule("$$", "$$", "tex")) +  autopairs.add_rules { +    Rule("$", "$", { "tex", "latex" }) -- don't add a pair if the next character is % +      :with_pair(cond.not_after_regex_check "%%") -- don't add a pair if  the previous character is xxx +      :with_pair(cond.not_before_regex_check("xxx", 3)) -- don't move right when repeat character +      :with_move(cond.none()) -- don't delete if the next character is xx +      :with_del(cond.not_after_regex_check "xx") -- disable  add newline when press <cr> +      :with_cr(cond.none()), +  } +  autopairs.add_rules { +    Rule("$$", "$$", "tex"):with_pair(function(opts) +      print(vim.inspect(opts)) +      if opts.line == "aa $$" then +        -- don't add pair on that line +        return false +      end +    end), +  } + +  if package.loaded["cmp"] then +    require("nvim-autopairs.completion.cmp").setup { +      map_cr = false, +      map_complete = lvim.builtin.autopairs.map_complete, +      map_char = lvim.builtin.autopairs.map_char, +    } +    -- we map CR explicitly in cmp.lua but we still need to setup the autopairs CR keymap +    vim.api.nvim_set_keymap("i", "<CR>", "v:lua.MPairs.autopairs_cr()", { expr = true, noremap = true }) +  end + +  require("nvim-treesitter.configs").setup { autopairs = { enable = true } } + +  local ts_conds = require "nvim-autopairs.ts-conds" + +  -- TODO: can these rules be safely added from "config.lua" ? +  -- press % => %% is only inside comment or string +  autopairs.add_rules { +    Rule("%", "%", "lua"):with_pair(ts_conds.is_ts_node { "string", "comment" }), +    Rule("$", "$", "lua"):with_pair(ts_conds.is_not_ts_node { "function" }), +  } + +  if lvim.builtin.autopairs.on_config_done then +    lvim.builtin.autopairs.on_config_done(autopairs) +  end +end + +return M diff --git a/lua/lvim/core/bufferline.lua b/lua/lvim/core/bufferline.lua new file mode 100644 index 00000000..ae6542d1 --- /dev/null +++ b/lua/lvim/core/bufferline.lua @@ -0,0 +1,25 @@ +local M = {} + +M.config = function() +  lvim.builtin.bufferline = { +    active = true, +    on_config_done = nil, +    keymap = { +      normal_mode = { +        ["<S-l>"] = ":BufferNext<CR>", +        ["<S-h>"] = ":BufferPrevious<CR>", +      }, +    }, +  } +end + +M.setup = function() +  local keymap = require "lvim.keymappings" +  keymap.append_to_defaults(lvim.builtin.bufferline.keymap) + +  if lvim.builtin.bufferline.on_config_done then +    lvim.builtin.bufferline.on_config_done() +  end +end + +return M diff --git a/lua/lvim/core/builtins/init.lua b/lua/lvim/core/builtins/init.lua new file mode 100644 index 00000000..8f83072e --- /dev/null +++ b/lua/lvim/core/builtins/init.lua @@ -0,0 +1,28 @@ +local M = {} + +local builtins = { +  "lvim.keymappings", +  "lvim.core.which-key", +  "lvim.core.gitsigns", +  "lvim.core.cmp", +  "lvim.core.dashboard", +  "lvim.core.dap", +  "lvim.core.terminal", +  "lvim.core.telescope", +  "lvim.core.treesitter", +  "lvim.core.nvimtree", +  "lvim.core.project", +  "lvim.core.bufferline", +  "lvim.core.autopairs", +  "lvim.core.comment", +  "lvim.core.lualine", +} + +function M.config(config) +  for _, builtin_path in ipairs(builtins) do +    local builtin = require(builtin_path) +    builtin.config(config) +  end +end + +return M diff --git a/lua/lvim/core/cmp.lua b/lua/lvim/core/cmp.lua new file mode 100644 index 00000000..ad06a360 --- /dev/null +++ b/lua/lvim/core/cmp.lua @@ -0,0 +1,265 @@ +local M = {} + +local check_backspace = function() +  local col = vim.fn.col "." - 1 +  return col == 0 or vim.fn.getline("."):sub(col, col):match "%s" +end + +local function T(str) +  return vim.api.nvim_replace_termcodes(str, true, true, true) +end + +local is_emmet_active = function() +  local clients = vim.lsp.buf_get_clients() + +  for _, client in pairs(clients) do +    if client.name == "emmet_ls" then +      return true +    end +  end +  return false +end + +M.config = function() +  local status_cmp_ok, cmp = pcall(require, "cmp") +  if not status_cmp_ok then +    return +  end +  local status_luasnip_ok, luasnip = pcall(require, "luasnip") +  if not status_luasnip_ok then +    return +  end +  local win_get_cursor = vim.api.nvim_win_get_cursor +  local get_current_buf = vim.api.nvim_get_current_buf + +  local function inside_snippet() +    -- for outdated versions of luasnip +    if not luasnip.session.current_nodes then +      return false +    end + +    local node = luasnip.session.current_nodes[get_current_buf()] +    if not node then +      return false +    end + +    local snip_begin_pos, snip_end_pos = node.parent.snippet.mark:pos_begin_end() +    local pos = win_get_cursor(0) +    pos[1] = pos[1] - 1 -- LuaSnip is 0-based not 1-based like nvim for rows +    return pos[1] >= snip_begin_pos[1] and pos[1] <= snip_end_pos[1] +  end + +  ---sets the current buffer's luasnip to the one nearest the cursor +  ---@return boolean true if a node is found, false otherwise +  local function seek_luasnip_cursor_node() +    -- for outdated versions of luasnip +    if not luasnip.session.current_nodes then +      return false +    end + +    local pos = win_get_cursor(0) +    pos[1] = pos[1] - 1 +    local node = luasnip.session.current_nodes[get_current_buf()] +    if not node then +      return false +    end + +    local snippet = node.parent.snippet +    local exit_node = snippet.insert_nodes[0] + +    -- exit early if we're past the exit node +    if exit_node then +      local exit_pos_end = exit_node.mark:pos_end() +      if (pos[1] > exit_pos_end[1]) or (pos[1] == exit_pos_end[1] and pos[2] > exit_pos_end[2]) then +        snippet:remove_from_jumplist() +        luasnip.session.current_nodes[get_current_buf()] = nil + +        return false +      end +    end + +    node = snippet.inner_first:jump_into(1, true) +    while node ~= nil and node.next ~= nil and node ~= snippet do +      local n_next = node.next +      local next_pos = n_next and n_next.mark:pos_begin() +      local candidate = n_next ~= snippet and next_pos and (pos[1] < next_pos[1]) +        or (pos[1] == next_pos[1] and pos[2] < next_pos[2]) + +      -- Past unmarked exit node, exit early +      if n_next == nil or n_next == snippet.next then +        snippet:remove_from_jumplist() +        luasnip.session.current_nodes[get_current_buf()] = nil + +        return false +      end + +      if candidate then +        luasnip.session.current_nodes[get_current_buf()] = node +        return true +      end + +      local ok +      ok, node = pcall(node.jump_from, node, 1, true) -- no_move until last stop +      if not ok then +        snippet:remove_from_jumplist() +        luasnip.session.current_nodes[get_current_buf()] = nil + +        return false +      end +    end + +    -- No candidate, but have an exit node +    if exit_node then +      -- to jump to the exit node, seek to snippet +      luasnip.session.current_nodes[get_current_buf()] = snippet +      return true +    end + +    -- No exit node, exit from snippet +    snippet:remove_from_jumplist() +    luasnip.session.current_nodes[get_current_buf()] = nil +    return false +  end + +  lvim.builtin.cmp = { +    confirm_opts = { +      behavior = cmp.ConfirmBehavior.Replace, +      select = false, +    }, +    experimental = { +      ghost_text = true, +      native_menu = false, +    }, +    formatting = { +      kind_icons = { +        Class = " ", +        Color = " ", +        Constant = "ﲀ ", +        Constructor = " ", +        Enum = "練", +        EnumMember = " ", +        Event = " ", +        Field = " ", +        File = "", +        Folder = " ", +        Function = " ", +        Interface = "ﰮ ", +        Keyword = " ", +        Method = " ", +        Module = " ", +        Operator = "", +        Property = " ", +        Reference = " ", +        Snippet = " ", +        Struct = " ", +        Text = " ", +        TypeParameter = " ", +        Unit = "塞", +        Value = " ", +        Variable = " ", +      }, +      source_names = { +        nvim_lsp = "(LSP)", +        emoji = "(Emoji)", +        path = "(Path)", +        calc = "(Calc)", +        cmp_tabnine = "(Tabnine)", +        vsnip = "(Snippet)", +        luasnip = "(Snippet)", +        buffer = "(Buffer)", +      }, +      duplicates = { +        buffer = 1, +        path = 1, +        nvim_lsp = 0, +        luasnip = 1, +      }, +      duplicates_default = 0, +      format = function(entry, vim_item) +        vim_item.kind = lvim.builtin.cmp.formatting.kind_icons[vim_item.kind] +        vim_item.menu = lvim.builtin.cmp.formatting.source_names[entry.source.name] +        vim_item.dup = lvim.builtin.cmp.formatting.duplicates[entry.source.name] +          or lvim.builtin.cmp.formatting.duplicates_default +        return vim_item +      end, +    }, +    snippet = { +      expand = function(args) +        require("luasnip").lsp_expand(args.body) +      end, +    }, +    documentation = { +      border = { "╭", "─", "╮", "│", "╯", "─", "╰", "│" }, +    }, +    sources = { +      { name = "nvim_lsp" }, +      { name = "path" }, +      { name = "luasnip" }, +      { name = "cmp_tabnine" }, +      { name = "nvim_lua" }, +      { name = "buffer" }, +      { name = "calc" }, +      { name = "emoji" }, +      { name = "treesitter" }, +      { name = "crates" }, +    }, +    mapping = { +      ["<C-d>"] = cmp.mapping.scroll_docs(-4), +      ["<C-f>"] = cmp.mapping.scroll_docs(4), +      -- TODO: potentially fix emmet nonsense +      ["<Tab>"] = cmp.mapping(function() +        if cmp.visible() then +          cmp.select_next_item() +        elseif luasnip.expandable() then +          luasnip.expand() +        elseif inside_snippet() and seek_luasnip_cursor_node() and luasnip.jumpable() then +          luasnip.jump(1) +        elseif check_backspace() then +          vim.fn.feedkeys(T "<Tab>", "n") +        elseif is_emmet_active() then +          return vim.fn["cmp#complete"]() +        else +          vim.fn.feedkeys(T "<Tab>", "n") +        end +      end, { +        "i", +        "s", +      }), +      ["<S-Tab>"] = cmp.mapping(function(fallback) +        if cmp.visible() then +          cmp.select_prev_item() +        elseif inside_snippet() and luasnip.jumpable(-1) then +          luasnip.jump(-1) +        else +          fallback() +        end +      end, { +        "i", +        "s", +      }), + +      ["<C-Space>"] = cmp.mapping.complete(), +      ["<C-e>"] = cmp.mapping.close(), +      ["<CR>"] = cmp.mapping(function(fallback) +        if cmp.visible() and cmp.confirm(lvim.builtin.cmp.confirm_opts) then +          return +        end + +        if inside_snippet() and seek_luasnip_cursor_node() and luasnip.jumpable() then +          if not luasnip.jump(1) then +            fallback() +          end +        else +          fallback() +        end +      end), +    }, +  } +end + +M.setup = function() +  require("luasnip/loaders/from_vscode").lazy_load() +  require("cmp").setup(lvim.builtin.cmp) +end + +return M diff --git a/lua/lvim/core/commands.lua b/lua/lvim/core/commands.lua new file mode 100644 index 00000000..b750f12b --- /dev/null +++ b/lua/lvim/core/commands.lua @@ -0,0 +1,25 @@ +local M = {} + +M.defaults = { +  [[ +  function! QuickFixToggle() +    if empty(filter(getwininfo(), 'v:val.quickfix')) +      copen +    else +      cclose +    endif +  endfunction +  ]], +  -- :LvimInfo +  [[ command! LvimInfo lua require('lvim.core.info').toggle_popup(vim.bo.filetype) ]], +  [[ command! LvimCacheReset lua require('lvim.utils.hooks').reset_cache() ]], +  [[ command! LvimUpdate lua require('lvim.bootstrap').update() ]], +} + +M.load = function(commands) +  for _, command in ipairs(commands) do +    vim.cmd(command) +  end +end + +return M diff --git a/lua/lvim/core/comment.lua b/lua/lvim/core/comment.lua new file mode 100644 index 00000000..b98410ab --- /dev/null +++ b/lua/lvim/core/comment.lua @@ -0,0 +1,31 @@ +local M = {} + +function M.config() +  lvim.builtin.comment = { +    active = true, +    on_config_done = nil, +    -- Linters prefer comment and line to have a space in between markers +    marker_padding = true, +    -- should comment out empty or whitespace only lines +    comment_empty = false, +    -- Should key mappings be created +    create_mappings = true, +    -- Normal mode mapping left hand side +    line_mapping = "gcc", +    -- Visual/Operator mapping left hand side +    operator_mapping = "gc", +    -- Hook function to call before commenting takes place +    hook = nil, +  } +end + +function M.setup() +  local nvim_comment = require "nvim_comment" + +  nvim_comment.setup(lvim.builtin.comment) +  if lvim.builtin.comment.on_config_done then +    lvim.builtin.comment.on_config_done(nvim_comment) +  end +end + +return M diff --git a/lua/lvim/core/dap.lua b/lua/lvim/core/dap.lua new file mode 100644 index 00000000..d9b59641 --- /dev/null +++ b/lua/lvim/core/dap.lua @@ -0,0 +1,76 @@ +local M = {} + +M.config = function() +  lvim.builtin.dap = { +    active = false, +    on_config_done = nil, +    breakpoint = { +      text = "", +      texthl = "LspDiagnosticsSignError", +      linehl = "", +      numhl = "", +    }, +    breakpoint_rejected = { +      text = "", +      texthl = "LspDiagnosticsSignHint", +      linehl = "", +      numhl = "", +    }, +    stopped = { +      text = "", +      texthl = "LspDiagnosticsSignInformation", +      linehl = "DiagnosticUnderlineInfo", +      numhl = "LspDiagnosticsSignInformation", +    }, +  } +end + +M.setup = function() +  local dap = require "dap" + +  vim.fn.sign_define("DapBreakpoint", lvim.builtin.dap.breakpoint) +  vim.fn.sign_define("DapBreakpointRejected", lvim.builtin.dap.breakpoint_rejected) +  vim.fn.sign_define("DapStopped", lvim.builtin.dap.stopped) + +  dap.defaults.fallback.terminal_win_cmd = "50vsplit new" + +  lvim.builtin.which_key.mappings["d"] = { +    name = "Debug", +    t = { "<cmd>lua require'dap'.toggle_breakpoint()<cr>", "Toggle Breakpoint" }, +    b = { "<cmd>lua require'dap'.step_back()<cr>", "Step Back" }, +    c = { "<cmd>lua require'dap'.continue()<cr>", "Continue" }, +    C = { "<cmd>lua require'dap'.run_to_cursor()<cr>", "Run To Cursor" }, +    d = { "<cmd>lua require'dap'.disconnect()<cr>", "Disconnect" }, +    g = { "<cmd>lua require'dap'.session()<cr>", "Get Session" }, +    i = { "<cmd>lua require'dap'.step_into()<cr>", "Step Into" }, +    o = { "<cmd>lua require'dap'.step_over()<cr>", "Step Over" }, +    u = { "<cmd>lua require'dap'.step_out()<cr>", "Step Out" }, +    p = { "<cmd>lua require'dap'.pause.toggle()<cr>", "Pause" }, +    r = { "<cmd>lua require'dap'.repl.toggle()<cr>", "Toggle Repl" }, +    s = { "<cmd>lua require'dap'.continue()<cr>", "Start" }, +    q = { "<cmd>lua require'dap'.close()<cr>", "Quit" }, +  } + +  if lvim.builtin.dap.on_config_done then +    lvim.builtin.dap.on_config_done(dap) +  end +end + +-- TODO put this up there ^^^ call in ftplugin + +-- M.dap = function() +--   if lvim.plugin.dap.active then +--     local dap_install = require "dap-install" +--     dap_install.config("python_dbg", {}) +--   end +-- end +-- +-- M.dap = function() +--   -- gem install readapt ruby-debug-ide +--   if lvim.plugin.dap.active then +--     local dap_install = require "dap-install" +--     dap_install.config("ruby_vsc_dbg", {}) +--   end +-- end + +return M diff --git a/lua/lvim/core/dashboard.lua b/lua/lvim/core/dashboard.lua new file mode 100644 index 00000000..38d9d226 --- /dev/null +++ b/lua/lvim/core/dashboard.lua @@ -0,0 +1,112 @@ +local M = {} +local utils = require "lvim.utils" + +M.config = function(config) +  lvim.builtin.dashboard = { +    active = false, +    on_config_done = nil, +    search_handler = "telescope", +    disable_at_vim_enter = 0, +    session_directory = utils.join_paths(get_cache_dir(), "sessions"), +    custom_header = { +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣀⣀⣀⣀⣀⣀⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣤⣶⣾⠿⠿⠟⠛⠛⠛⠛⠿⠿⣿⣷⣤⣄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "  ⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣾⡿⠋⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠿⣷⣤⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣤⡿⠛⠉⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠛⢿⣦⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⠒⠂⠉⠉⠉⠉⢩⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠹⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠸⡀⠀⠀⠀⠀⠀⢰⣿⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⣿⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠑⠠⡀⠀⠀⢀⣾⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠘⣿⡆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠢⢀⣸⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢹⣇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡧⢄⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠈⠁⠒⠤⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣿⣀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀⠀⠉⠢⠤⠀⢀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡟⠈⠑⠄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠑⠒⠤⣀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⡇⠀⠀⢀⣣⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⣿⡟⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠸⣷⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠀⠀⠒⠢⠤⠄⣀⣀⠀⠀⠀⢠⣿⡟⠀⠀⠀⣺⣿⡿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀", +      "⠀⣿⠇⠀⠀⠀⠀⠀⣤⡄⠀⠀⢠⣤⡄⠀⢨⣭⣠⣤⣤⣤⡀⠀⠀⢀⣤⣤⣤⣤⡄⠀⠀⠀⣤⣄⣤⣤⣤⠀⠀⣿⣯⠉⠉⣿⡟⠀⠈⢩⣭⣤⣤⠀⠀⠀⠀⣠⣤⣤⣤⣄⣤⣤", +      "⢠⣿⠀⠀⠀⠀⠀⠀⣿⠃⠀⠀⣸⣿⠁⠀⣿⣿⠉⠀⠈⣿⡇⠀⠀⠛⠋⠀⠀⢹⣿⠀⠀⠀⣿⠏⠀⠸⠿⠃⠀⣿⣿⠀⣰⡟⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⠀⣿⡟⢸⣿⡇⢀⣿", +      "⣸⡇⠀⠀⠀⠀⠀⢸⣿⠀⠀⠀⣿⡟⠀⢠⣿⡇⠀⠀⢰⣿⡇⠀⣰⣾⠟⠛⠛⣻⡇⠀⠀⢸⡿⠀⠀⠀⠀⠀⠀⢻⣿⢰⣿⠀⠀⠀⠀⠀⠀⣾⡇⠀⠀⠀⢸⣿⠇⢸⣿⠀⢸⡏", +      "⣿⣧⣤⣤⣤⡄⠀⠘⣿⣤⣤⡤⣿⠇⠀⢸⣿⠁⠀⠀⣼⣿⠀⠀⢿⣿⣤⣤⠔⣿⠃⠀⠀⣾⡇⠀⠀⠀⠀⠀⠀⢸⣿⣿⠋⠀⠀⠀⢠⣤⣤⣿⣥⣤⡄⠀⣼⣿⠀⣸⡏⠀⣿⠃", +      "⠉⠉⠉⠉⠉⠁⠀⠀⠈⠉⠉⠀⠉⠀⠀⠈⠉⠀⠀⠀⠉⠉⠀⠀⠀⠉⠉⠁⠈⠉⠀⠀⠀⠉⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠀⠀⠀⠀⠈⠉⠉⠉⠉⠉⠁⠀⠉⠁⠀⠉⠁⠀⠉⠀", +    }, + +    custom_section = { +      a = { +        description = { "  Find File          " }, +        command = "Telescope find_files", +      }, +      b = { +        description = { "  Recent Projects    " }, +        command = "Telescope projects", +      }, +      c = { +        description = { "  Recently Used Files" }, +        command = "Telescope oldfiles", +      }, +      d = { +        description = { "  Find Word          " }, +        command = "Telescope live_grep", +      }, +      e = { +        description = { "  Configuration      " }, +        command = ":e " .. config.user_config_file, +      }, +    }, + +    footer = { "lunarvim.org" }, +  } +end + +M.setup = function() +  vim.g.dashboard_disable_at_vimenter = lvim.builtin.dashboard.disable_at_vim_enter + +  vim.g.dashboard_custom_header = lvim.builtin.dashboard.custom_header + +  vim.g.dashboard_default_executive = lvim.builtin.dashboard.search_handler + +  vim.g.dashboard_custom_section = lvim.builtin.dashboard.custom_section + +  lvim.builtin.which_key.mappings[";"] = { "<cmd>Dashboard<CR>", "Dashboard" } + +  vim.g.dashboard_session_directory = lvim.builtin.dashboard.session_directory + +  local lvim_site = "lunarvim.org" +  local lvim_version = get_version "short" +  local num_plugins_loaded = #vim.fn.globpath(get_runtime_dir() .. "/site/pack/packer/start", "*", 0, 1) + +  local footer = { +    "LunarVim loaded " .. num_plugins_loaded .. " plugins ", +    "", +    lvim_site, +  } + +  if lvim_version then +    table.insert(footer, 2, "") +    table.insert(footer, 3, "v" .. lvim_version) +  end + +  local text = require "lvim.interface.text" +  vim.g.dashboard_custom_footer = text.align_center({ width = 0 }, footer, 0.49) -- Use 0.49 as  counts for 2 characters + +  require("lvim.core.autocmds").define_augroups { +    _dashboard = { +      -- seems to be nobuflisted that makes my stuff disappear will do more testing +      { +        "FileType", +        "dashboard", +        "setlocal nocursorline noswapfile synmaxcol& signcolumn=no norelativenumber nocursorcolumn nospell  nolist  nonumber bufhidden=wipe colorcolumn= foldcolumn=0 matchpairs= ", +      }, +      { +        "FileType", +        "dashboard", +        "set showtabline=0 | autocmd BufLeave <buffer> set showtabline=" .. vim.opt.showtabline._value, +      }, +      { "FileType", "dashboard", "nnoremap <silent> <buffer> q :q<CR>" }, +    }, +  } + +  if lvim.builtin.dashboard.on_config_done then +    lvim.builtin.dashboard.on_config_done() +  end +end + +return M diff --git a/lua/lvim/core/gitsigns.lua b/lua/lvim/core/gitsigns.lua new file mode 100644 index 00000000..cc6387dc --- /dev/null +++ b/lua/lvim/core/gitsigns.lua @@ -0,0 +1,64 @@ +local M = {} + +M.config = function() +  lvim.builtin.gitsigns = { +    active = true, +    on_config_done = nil, +    opts = { +      signs = { +        add = { +          hl = "GitSignsAdd", +          text = "▎", +          numhl = "GitSignsAddNr", +          linehl = "GitSignsAddLn", +        }, +        change = { +          hl = "GitSignsChange", +          text = "▎", +          numhl = "GitSignsChangeNr", +          linehl = "GitSignsChangeLn", +        }, +        delete = { +          hl = "GitSignsDelete", +          text = "契", +          numhl = "GitSignsDeleteNr", +          linehl = "GitSignsDeleteLn", +        }, +        topdelete = { +          hl = "GitSignsDelete", +          text = "契", +          numhl = "GitSignsDeleteNr", +          linehl = "GitSignsDeleteLn", +        }, +        changedelete = { +          hl = "GitSignsChange", +          text = "▎", +          numhl = "GitSignsChangeNr", +          linehl = "GitSignsChangeLn", +        }, +      }, +      numhl = false, +      linehl = false, +      keymaps = { +        -- Default keymap options +        noremap = true, +        buffer = true, +      }, +      watch_gitdir = { interval = 1000 }, +      sign_priority = 6, +      update_debounce = 200, +      status_formatter = nil, -- Use default +    }, +  } +end + +M.setup = function() +  local gitsigns = require "gitsigns" + +  gitsigns.setup(lvim.builtin.gitsigns.opts) +  if lvim.builtin.gitsigns.on_config_done then +    lvim.builtin.gitsigns.on_config_done(gitsigns) +  end +end + +return M diff --git a/lua/lvim/core/info.lua b/lua/lvim/core/info.lua new file mode 100644 index 00000000..7fdb665b --- /dev/null +++ b/lua/lvim/core/info.lua @@ -0,0 +1,174 @@ +local M = { +  banner = { +    "", +    [[    __                          _    ___         ]], +    [[   / /   __  ______  ____ _____| |  / (_)___ ___ ]], +    [[  / /   / / / / __ \/ __ `/ ___/ | / / / __ `__ \]], +    [[ / /___/ /_/ / / / / /_/ / /   | |/ / / / / / / /]], +    [[/_____/\__,_/_/ /_/\__,_/_/    |___/_/_/ /_/ /_/ ]], +  }, +} + +local fmt = string.format +local text = require "lvim.interface.text" +local lsp_utils = require "lvim.lsp.utils" +local user_config_file = require("lvim.config"):get_user_config_path() + +local function str_list(list) +  return fmt("[ %s ]", table.concat(list, ", ")) +end + +local function get_formatter_suggestion_msg(ft) +  local null_formatters = require "lvim.lsp.null-ls.formatters" +  local supported_formatters = null_formatters.list_available(ft) +  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_file), +      "", +      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 "lvim.lsp.null-ls.linters" +  local supported_linters = null_linters.list_available(ft) +  local section = { +    " HINT ", +    "", +    fmt("* List of supported linters: %s", str_list(supported_linters)), +  } + +  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_file), +      "", +      fmt("  lvim.lang.%s.linters = { { exe = '%s' } }", ft, table.concat(supported_linters, "│")), +    }) +  end + +  return section +end + +local function tbl_set_highlight(terms, highlight_group) +  for _, v in pairs(terms) do +    vim.cmd('let m=matchadd("' .. highlight_group .. '", "' .. v .. "[ ,│']\")") +  end +end + +local function make_client_info(client) +  local client_enabled_caps = lsp_utils.get_client_capabilities(client.id) +  local name = client.name +  local id = client.id +  local document_formatting = client.resolved_capabilities.document_formatting +  local client_info = { +    fmt("* Name:                 %s", name), +    fmt("* Id:                   %s", tostring(id)), +    fmt("* Supports formatting:  %s", tostring(document_formatting)), +  } +  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_right(enabled_caps, caps_text_len) +    enabled_caps[1] = fmt("%s%s", caps_text, enabled_caps[1]:sub(caps_text_len + 1)) +    vim.list_extend(client_info, enabled_caps) +  end + +  return client_info +end + +function M.toggle_popup(ft) +  local clients = lsp_utils.get_active_clients_by_ft(ft) +  local client_names = {} + +  local header = { +    fmt("Detected filetype:      %s", ft), +    fmt("Treesitter active:      %s", tostring(next(vim.treesitter.highlighter.active) ~= nil)), +  } + +  local lsp_info = { +    "Language Server Protocol (LSP) info", +    fmt "* Associated server(s):", +  } + +  for _, client in pairs(clients) do +    vim.list_extend(lsp_info, make_client_info(client)) +    table.insert(client_names, client.name) +  end + +  local null_formatters = require "lvim.lsp.null-ls.formatters" +  local null_linters = require "lvim.lsp.null-ls.linters" +  local registered_formatters = null_formatters.list_supported_names(ft) +  local registered_linters = null_linters.list_supported_names(ft) +  local registered_providers = {} +  vim.list_extend(registered_providers, registered_formatters) +  vim.list_extend(registered_providers, registered_linters) +  local registered_count = vim.tbl_count(registered_providers) +  local null_ls_info = { +    "Formatters and linters", +    fmt( +      "* Configured providers: %s%s", +      table.concat(registered_providers, "  , "), +      registered_count > 0 and "  " or "" +    ), +  } + +  local content_provider = function(popup) +    local content = {} + +    for _, section in ipairs { +      M.banner, +      { "" }, +      { "" }, +      header, +      { "" }, +      lsp_info, +      { "" }, +      null_ls_info, +      { "" }, +      { "" }, +      get_formatter_suggestion_msg(ft), +      { "" }, +      { "" }, +      get_linter_suggestion_msg(ft), +    } do +      vim.list_extend(content, section) +    end + +    return text.align_left(popup, content, 0.5) +  end + +  local function set_syntax_hl() +    vim.cmd [[highlight LvimInfoIdentifier gui=bold]] +    vim.cmd [[highlight link LvimInfoHeader Type]] +    vim.cmd [[let m=matchadd("LvimInfoHeader", "Language Server Protocol (LSP) info")]] +    vim.cmd [[let m=matchadd("LvimInfoHeader", "Formatters and linters")]] +    vim.cmd('let m=matchadd("LvimInfoIdentifier", " ' .. ft .. '$")') +    vim.cmd 'let m=matchadd("string", "true")' +    vim.cmd 'let m=matchadd("error", "false")' +    tbl_set_highlight(registered_providers, "LvimInfoIdentifier") +    -- tbl_set_highlight(require("lvim.lsp.null-ls.formatters").list_available(ft), "LvimInfoIdentifier") +    -- tbl_set_highlight(require("lvim.lsp.null-ls.linters").list_available(ft), "LvimInfoIdentifier") +  end + +  local Popup = require("lvim.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 diff --git a/lua/lvim/core/log.lua b/lua/lvim/core/log.lua new file mode 100644 index 00000000..fca1fcb4 --- /dev/null +++ b/lua/lvim/core/log.lua @@ -0,0 +1,60 @@ +local Log = {} + +--- Adds a log entry using Plenary.log +---@param msg any +---@param level string [same as vim.log.log_levels] +function Log:add_entry(msg, level) +  assert(type(level) == "string") +  if self.__handle then +    -- plenary uses lower-case log levels +    self.__handle[level:lower()](msg) +    return +  end +  local status_ok, plenary = pcall(require, "plenary") +  if status_ok then +    local default_opts = { plugin = "lunarvim", level = lvim.log.level } +    local handle = plenary.log.new(default_opts) +    handle[level:lower()](msg) +    self.__handle = handle +  end +  -- don't do anything if plenary is not available +end + +---Retrieves the path of the logfile +---@return string path of the logfile +function Log:get_path() +  return string.format("%s/%s.log", vim.fn.stdpath "cache", "lunarvim") +end + +---Add a log entry at TRACE level +---@param msg any +function Log:trace(msg) +  self:add_entry(msg, "TRACE") +end + +---Add a log entry at DEBUG level +---@param msg any +function Log:debug(msg) +  self:add_entry(msg, "DEBUG") +end + +---Add a log entry at INFO level +---@param msg any +function Log:info(msg) +  self:add_entry(msg, "INFO") +end + +---Add a log entry at WARN level +---@param msg any +function Log:warn(msg) +  self:add_entry(msg, "WARN") +end + +---Add a log entry at ERROR level +---@param msg any +function Log:error(msg) +  self:add_entry(msg, "ERROR") +end + +setmetatable({}, Log) +return Log diff --git a/lua/lvim/core/lualine/colors.lua b/lua/lvim/core/lualine/colors.lua new file mode 100644 index 00000000..4984cd1f --- /dev/null +++ b/lua/lvim/core/lualine/colors.lua @@ -0,0 +1,16 @@ +local colors = { +  bg = "#202328", +  fg = "#bbc2cf", +  yellow = "#ECBE7B", +  cyan = "#008080", +  darkblue = "#081633", +  green = "#98be65", +  orange = "#FF8800", +  violet = "#a9a1e1", +  magenta = "#c678dd", +  purple = "#c678dd", +  blue = "#51afef", +  red = "#ec5f67", +} + +return colors diff --git a/lua/lvim/core/lualine/components.lua b/lua/lvim/core/lualine/components.lua new file mode 100644 index 00000000..5c0fb84b --- /dev/null +++ b/lua/lvim/core/lualine/components.lua @@ -0,0 +1,154 @@ +local conditions = require "lvim.core.lualine.conditions" +local colors = require "lvim.core.lualine.colors" + +local function diff_source() +  local gitsigns = vim.b.gitsigns_status_dict +  if gitsigns then +    return { +      added = gitsigns.added, +      modified = gitsigns.changed, +      removed = gitsigns.removed, +    } +  end +end + +return { +  mode = { +    function() +      return " " +    end, +    padding = { left = 0, right = 0 }, +    color = {}, +    cond = nil, +  }, +  branch = { +    "b:gitsigns_head", +    icon = " ", +    color = { gui = "bold" }, +    cond = conditions.hide_in_width, +  }, +  filename = { +    "filename", +    color = {}, +    cond = nil, +  }, +  diff = { +    "diff", +    source = diff_source, +    symbols = { added = "  ", modified = "柳", removed = " " }, +    diff_color = { +      added = { fg = colors.green }, +      modified = { fg = colors.yellow }, +      removed = { fg = colors.red }, +    }, +    color = {}, +    cond = nil, +  }, +  python_env = { +    function() +      local utils = require "lvim.core.lualine.utils" +      if vim.bo.filetype == "python" then +        local venv = os.getenv "CONDA_DEFAULT_ENV" +        if venv then +          return string.format("  (%s)", utils.env_cleanup(venv)) +        end +        venv = os.getenv "VIRTUAL_ENV" +        if venv then +          return string.format("  (%s)", utils.env_cleanup(venv)) +        end +        return "" +      end +      return "" +    end, +    color = { fg = colors.green }, +    cond = conditions.hide_in_width, +  }, +  diagnostics = { +    "diagnostics", +    sources = { "nvim_lsp" }, +    symbols = { error = " ", warn = " ", info = " ", hint = " " }, +    color = {}, +    cond = conditions.hide_in_width, +  }, +  treesitter = { +    function() +      local b = vim.api.nvim_get_current_buf() +      if next(vim.treesitter.highlighter.active[b]) then +        return "  " +      end +      return "" +    end, +    color = { fg = colors.green }, +    cond = conditions.hide_in_width, +  }, +  lsp = { +    function(msg) +      msg = msg or "LS Inactive" +      local buf_clients = vim.lsp.buf_get_clients() +      if next(buf_clients) == nil then +        -- TODO: clean up this if statement +        if type(msg) == "boolean" or #msg == 0 then +          return "LS Inactive" +        end +        return msg +      end +      local buf_ft = vim.bo.filetype +      local buf_client_names = {} + +      -- add client +      for _, client in pairs(buf_clients) do +        if client.name ~= "null-ls" then +          table.insert(buf_client_names, client.name) +        end +      end + +      -- add formatter +      local formatters = require "lvim.lsp.null-ls.formatters" +      local supported_formatters = formatters.list_supported_names(buf_ft) +      vim.list_extend(buf_client_names, supported_formatters) + +      -- add linter +      local linters = require "lvim.lsp.null-ls.linters" +      local supported_linters = linters.list_supported_names(buf_ft) +      vim.list_extend(buf_client_names, supported_linters) + +      return table.concat(buf_client_names, ", ") +    end, +    icon = " ", +    color = { gui = "bold" }, +    cond = conditions.hide_in_width, +  }, +  location = { "location", cond = conditions.hide_in_width, color = {} }, +  progress = { "progress", cond = conditions.hide_in_width, color = {} }, +  spaces = { +    function() +      local label = "Spaces: " +      if not vim.api.nvim_buf_get_option(0, "expandtab") then +        label = "Tab size: " +      end +      return label .. vim.api.nvim_buf_get_option(0, "shiftwidth") .. " " +    end, +    cond = conditions.hide_in_width, +    color = {}, +  }, +  encoding = { +    "o:encoding", +    fmt = string.upper, +    color = {}, +    cond = conditions.hide_in_width, +  }, +  filetype = { "filetype", cond = conditions.hide_in_width, color = {} }, +  scrollbar = { +    function() +      local current_line = vim.fn.line "." +      local total_lines = vim.fn.line "$" +      local chars = { "__", "▁▁", "▂▂", "▃▃", "▄▄", "▅▅", "▆▆", "▇▇", "██" } +      local line_ratio = current_line / total_lines +      local index = math.ceil(line_ratio * #chars) +      return chars[index] +    end, +    padding = { left = 0, right = 0 }, +    color = { fg = colors.yellow, bg = colors.bg }, +    cond = nil, +  }, +} diff --git a/lua/lvim/core/lualine/conditions.lua b/lua/lvim/core/lualine/conditions.lua new file mode 100644 index 00000000..3ee4fbb8 --- /dev/null +++ b/lua/lvim/core/lualine/conditions.lua @@ -0,0 +1,17 @@ +local window_width_limit = 80 + +local conditions = { +  buffer_not_empty = function() +    return vim.fn.empty(vim.fn.expand "%:t") ~= 1 +  end, +  hide_in_width = function() +    return vim.fn.winwidth(0) > window_width_limit +  end, +  -- check_git_workspace = function() +  --   local filepath = vim.fn.expand "%:p:h" +  --   local gitdir = vim.fn.finddir(".git", filepath .. ";") +  --   return gitdir and #gitdir > 0 and #gitdir < #filepath +  -- end, +} + +return conditions diff --git a/lua/lvim/core/lualine/init.lua b/lua/lvim/core/lualine/init.lua new file mode 100644 index 00000000..c5d024c2 --- /dev/null +++ b/lua/lvim/core/lualine/init.lua @@ -0,0 +1,47 @@ +local M = {} +M.config = function() +  lvim.builtin.lualine = { +    active = true, +    style = "lvim", +    options = { +      icons_enabled = nil, +      component_separators = nil, +      section_separators = nil, +      theme = nil, +      disabled_filetypes = nil, +    }, +    sections = { +      lualine_a = nil, +      lualine_b = nil, +      lualine_c = nil, +      lualine_x = nil, +      lualine_y = nil, +      lualine_z = nil, +    }, +    inactive_sections = { +      lualine_a = nil, +      lualine_b = nil, +      lualine_c = nil, +      lualine_x = nil, +      lualine_y = nil, +      lualine_z = nil, +    }, +    tabline = nil, +    extensions = nil, +    on_config_done = nil, +  } +end + +M.setup = function() +  require("lvim.core.lualine.styles").update() +  require("lvim.core.lualine.utils").validate_theme() + +  local lualine = require "lualine" +  lualine.setup(lvim.builtin.lualine) + +  if lvim.builtin.lualine.on_config_done then +    lvim.builtin.lualine.on_config_done(lualine) +  end +end + +return M diff --git a/lua/lvim/core/lualine/styles.lua b/lua/lvim/core/lualine/styles.lua new file mode 100644 index 00000000..0843aead --- /dev/null +++ b/lua/lvim/core/lualine/styles.lua @@ -0,0 +1,137 @@ +local M = {} +local components = require "lvim.core.lualine.components" + +local styles = { +  lvim = nil, +  default = nil, +  none = nil, +} + +styles.none = { +  style = "none", +  options = { +    icons_enabled = true, +    component_separators = { left = "", right = "" }, +    section_separators = { left = "", right = "" }, +    disabled_filetypes = {}, +  }, +  sections = { +    lualine_a = {}, +    lualine_b = {}, +    lualine_c = {}, +    lualine_x = {}, +    lualine_y = {}, +    lualine_z = {}, +  }, +  inactive_sections = { +    lualine_a = {}, +    lualine_b = {}, +    lualine_c = {}, +    lualine_x = {}, +    lualine_y = {}, +    lualine_z = {}, +  }, +  tabline = {}, +  extensions = {}, +} + +styles.default = { +  style = "default", +  options = { +    icons_enabled = true, +    component_separators = { left = "", right = "" }, +    section_separators = { left = "", right = "" }, +    disabled_filetypes = {}, +  }, +  sections = { +    lualine_a = { "mode" }, +    lualine_b = { "branch" }, +    lualine_c = { "filename" }, +    lualine_x = { "encoding", "fileformat", "filetype" }, +    lualine_y = { "progress" }, +    lualine_z = { "location" }, +  }, +  inactive_sections = { +    lualine_a = {}, +    lualine_b = {}, +    lualine_c = { "filename" }, +    lualine_x = { "location" }, +    lualine_y = {}, +    lualine_z = {}, +  }, +  tabline = {}, +  extensions = {}, +} + +styles.lvim = { +  style = "lvim", +  options = { +    icons_enabled = true, +    component_separators = { left = "", right = "" }, +    section_separators = { left = "", right = "" }, +    disabled_filetypes = { "dashboard", "NvimTree", "Outline" }, +  }, +  sections = { +    lualine_a = { +      components.mode, +    }, +    lualine_b = { +      components.branch, +      components.filename, +    }, +    lualine_c = { +      components.diff, +      components.python_env, +    }, +    lualine_x = { +      components.diagnostics, +      components.treesitter, +      components.lsp, +      components.filetype, +    }, +    lualine_y = {}, +    lualine_z = { +      components.scrollbar, +    }, +  }, +  inactive_sections = { +    lualine_a = { +      "filename", +    }, +    lualine_b = {}, +    lualine_c = {}, +    lualine_x = {}, +    lualine_y = {}, +    lualine_z = {}, +  }, +  tabline = {}, +  extensions = { "nvim-tree" }, +} + +function M.get_style(style) +  local style_keys = vim.tbl_keys(styles) +  if not vim.tbl_contains(style_keys, style) then +    local Log = require "lvim.core.log" +    Log:error( +      "Invalid lualine style", +      string.format('"%s"', style), +      "options are: ", +      string.format('"%s"', table.concat(style_keys, '", "')) +    ) +    Log:debug '"lvim" style is applied.' +    style = "lvim" +  end + +  return vim.deepcopy(styles[style]) +end + +function M.update() +  local style = M.get_style(lvim.builtin.lualine.style) +  if lvim.builtin.lualine.options.theme == nil then +    lvim.builtin.lualine.options.theme = lvim.colorscheme +  end + +  lvim.builtin.lualine = vim.tbl_deep_extend("keep", lvim.builtin.lualine, style) +end + +return M diff --git a/lua/lvim/core/lualine/utils.lua b/lua/lvim/core/lualine/utils.lua new file mode 100644 index 00000000..cf80a99e --- /dev/null +++ b/lua/lvim/core/lualine/utils.lua @@ -0,0 +1,27 @@ +local M = {} + +function M.validate_theme() +  local theme = lvim.builtin.lualine.options.theme +  if type(theme) == "table" then +    return +  end + +  local lualine_loader = require "lualine.utils.loader" +  local ok = pcall(lualine_loader.load_theme, theme) +  if not ok then +    lvim.builtin.lualine.options.theme = "auto" +  end +end + +function M.env_cleanup(venv) +  if string.find(venv, "/") then +    local final_venv = venv +    for w in venv:gmatch "([^/]+)" do +      final_venv = w +    end +    venv = final_venv +  end +  return venv +end + +return M diff --git a/lua/lvim/core/nvimtree.lua b/lua/lvim/core/nvimtree.lua new file mode 100644 index 00000000..d9e6fb5d --- /dev/null +++ b/lua/lvim/core/nvimtree.lua @@ -0,0 +1,141 @@ +local M = {} +local Log = require "lvim.core.log" + +function M.config() +  lvim.builtin.nvimtree = { +    active = true, +    on_config_done = nil, +    setup = { +      open_on_setup = false, +      auto_close = true, +      open_on_tab = false, +      update_focused_file = { +        enable = true, +      }, +      diagnostics = { +        enable = true, +        icons = { +          hint = "", +          info = "", +          warning = "", +          error = "", +        }, +      }, +      view = { +        width = 30, +        side = "left", +        auto_resize = false, +        mappings = { +          custom_only = false, +        }, +      }, +    }, +    show_icons = { +      git = 1, +      folders = 1, +      files = 1, +      folder_arrows = 1, +      tree_width = 30, +    }, +    ignore = { ".git", "node_modules", ".cache" }, +    quit_on_open = 0, +    hide_dotfiles = 1, +    git_hl = 1, +    root_folder_modifier = ":t", +    allow_resize = 1, +    auto_ignore_ft = { "startify", "dashboard" }, +    icons = { +      default = "", +      symlink = "", +      git = { +        unstaged = "", +        staged = "S", +        unmerged = "", +        renamed = "➜", +        deleted = "", +        untracked = "U", +        ignored = "◌", +      }, +      folder = { +        default = "", +        open = "", +        empty = "", +        empty_open = "", +        symlink = "", +      }, +    }, +  } +end + +function M.setup() +  local status_ok, nvim_tree_config = pcall(require, "nvim-tree.config") +  if not status_ok then +    Log:error "Failed to load nvim-tree.config" +    return +  end +  local g = vim.g + +  for opt, val in pairs(lvim.builtin.nvimtree) do +    g["nvim_tree_" .. opt] = val +  end + +  -- Implicitly update nvim-tree when project module is active +  if lvim.builtin.project.active then +    lvim.builtin.nvimtree.respect_buf_cwd = 1 +    lvim.builtin.nvimtree.setup.update_cwd = true +    lvim.builtin.nvimtree.setup.disable_netrw = false +    lvim.builtin.nvimtree.setup.hijack_netrw = false +    vim.g.netrw_banner = false +  end + +  local tree_cb = nvim_tree_config.nvim_tree_callback + +  if not lvim.builtin.nvimtree.setup.view.mappings.list then +    lvim.builtin.nvimtree.setup.view.mappings.list = { +      { key = { "l", "<CR>", "o" }, cb = tree_cb "edit" }, +      { key = "h", cb = tree_cb "close_node" }, +      { key = "v", cb = tree_cb "vsplit" }, +    } +  end + +  lvim.builtin.which_key.mappings["e"] = { "<cmd>NvimTreeToggle<CR>", "Explorer" } + +  local tree_view = require "nvim-tree.view" + +  -- Add nvim_tree open callback +  local open = tree_view.open +  tree_view.open = function() +    M.on_open() +    open() +  end + +  vim.cmd "au WinClosed * lua require('lvim.core.nvimtree').on_close()" + +  if lvim.builtin.nvimtree.on_config_done then +    lvim.builtin.nvimtree.on_config_done(nvim_tree_config) +  end +  require("nvim-tree").setup(lvim.builtin.nvimtree.setup) +end + +function M.on_open() +  if package.loaded["bufferline.state"] and lvim.builtin.nvimtree.setup.view.side == "left" then +    require("bufferline.state").set_offset(lvim.builtin.nvimtree.setup.view.width + 1, "") +  end +end + +function M.on_close() +  local buf = tonumber(vim.fn.expand "<abuf>") +  local ft = vim.api.nvim_buf_get_option(buf, "filetype") +  if ft == "NvimTree" and package.loaded["bufferline.state"] then +    require("bufferline.state").set_offset(0) +  end +end + +function M.change_tree_dir(dir) +  local lib_status_ok, lib = pcall(require, "nvim-tree.lib") +  if lib_status_ok then +    lib.change_dir(dir) +  end +end + +return M diff --git a/lua/lvim/core/project.lua b/lua/lvim/core/project.lua new file mode 100644 index 00000000..e7527440 --- /dev/null +++ b/lua/lvim/core/project.lua @@ -0,0 +1,51 @@ +local M = {} + +function M.config() +  lvim.builtin.project = { +    ---@usage set to false to disable project.nvim. +    --- This is on by default since it's currently the expected behavior. +    active = true, + +    on_config_done = nil, + +    ---@usage set to true to disable setting the current-woriking directory +    --- Manual mode doesn't automatically change your root directory, so you have +    --- the option to manually do so using `:ProjectRoot` command. +    manual_mode = false, + +    ---@usage Methods of detecting the root directory +    --- Allowed values: **"lsp"** uses the native neovim lsp +    --- **"pattern"** uses vim-rooter like glob pattern matching. Here +    --- order matters: if one is not detected, the other is used as fallback. You +    --- can also delete or rearangne the detection methods. +    detection_methods = { "lsp", "pattern" }, + +    ---@usage patterns used to detect root dir, when **"pattern"** is in detection_methods +    patterns = { ".git", "_darcs", ".hg", ".bzr", ".svn", "Makefile", "package.json" }, + +    ---@ Show hidden files in telescope when searching for files in a project +    show_hidden = false, + +    ---@usage When set to false, you will get a message when project.nvim changes your directory. +    -- When set to false, you will get a message when project.nvim changes your directory. +    silent_chdir = true, + +    ---@usage list of lsp client names to ignore when using **lsp** detection. eg: { "efm", ... } +    ignore_lsp = {}, + +    ---@type string +    ---@usage path to store the project history for use in telescope +    datapath = get_cache_dir(), +  } +end + +function M.setup() +  local project = require "project_nvim" + +  project.setup(lvim.builtin.project) +  if lvim.builtin.project.on_config_done then +    lvim.builtin.project.on_config_done(project) +  end +end + +return M diff --git a/lua/lvim/core/telescope.lua b/lua/lvim/core/telescope.lua new file mode 100644 index 00000000..35f6b4a2 --- /dev/null +++ b/lua/lvim/core/telescope.lua @@ -0,0 +1,193 @@ +local M = {} + +function M.config() +  -- Define this minimal config so that it's available if telescope is not yet available. +  lvim.builtin.telescope = { +    ---@usage disable telescope completely [not recommeded] +    active = true, +    on_config_done = nil, +  } + +  local status_ok, actions = pcall(require, "telescope.actions") +  if not status_ok then +    return +  end + +  lvim.builtin.telescope = vim.tbl_extend("force", lvim.builtin.telescope, { +    defaults = { +      prompt_prefix = " ", +      selection_caret = " ", +      entry_prefix = "  ", +      initial_mode = "insert", +      selection_strategy = "reset", +      sorting_strategy = "descending", +      layout_strategy = "horizontal", +      layout_config = { +        width = 0.75, +        preview_cutoff = 120, +        horizontal = { mirror = false }, +        vertical = { mirror = false }, +      }, +      file_sorter = require("telescope.sorters").get_fzy_sorter, +      file_ignore_patterns = {}, +      generic_sorter = require("telescope.sorters").get_generic_fuzzy_sorter, +      path_display = { shorten = 5 }, +      winblend = 0, +      border = {}, +      borderchars = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, +      color_devicons = true, +      use_less = true, +      set_env = { ["COLORTERM"] = "truecolor" }, -- default = nil, +      file_previewer = require("telescope.previewers").vim_buffer_cat.new, +      grep_previewer = require("telescope.previewers").vim_buffer_vimgrep.new, +      qflist_previewer = require("telescope.previewers").vim_buffer_qflist.new, + +      -- Developer configurations: Not meant for general override +      -- buffer_previewer_maker = require("telescope.previewers").buffer_previewer_maker, +      mappings = { +        i = { +          ["<C-n>"] = actions.move_selection_next, +          ["<C-p>"] = actions.move_selection_previous, +          ["<C-c>"] = actions.close, +          ["<C-j>"] = actions.cycle_history_next, +          ["<C-k>"] = actions.cycle_history_prev, +          ["<C-q>"] = actions.smart_send_to_qflist + actions.open_qflist, +          ["<CR>"] = actions.select_default + actions.center, +          -- To disable a keymap, put [map] = false +          -- So, to not map "<C-n>", just put +          -- ["<c-t>"] = trouble.open_with_trouble, +          -- ["<c-x>"] = false, +          -- ["<esc>"] = actions.close, +          -- Otherwise, just set the mapping to the function that you want it to be. +          -- ["<C-i>"] = actions.select_horizontal, +          -- Add up multiple actions +          -- You can perform as many actions in a row as you like +          -- ["<CR>"] = actions.select_default + actions.center + my_cool_custom_action, +        }, +        n = { +          ["<C-n>"] = actions.move_selection_next, +          ["<C-p>"] = actions.move_selection_previous, +          ["<C-q>"] = actions.smart_send_to_qflist + actions.open_qflist, +          -- ["<c-t>"] = trouble.open_with_trouble, +          -- ["<C-i>"] = my_cool_custom_action, +        }, +      }, +    }, +    extensions = { +      fzy_native = { +        override_generic_sorter = false, +        override_file_sorter = true, +      }, +    }, +  }) +end + +function M.find_lunarvim_files(opts) +  opts = opts or {} +  local themes = require "telescope.themes" +  local theme_opts = themes.get_ivy { +    sorting_strategy = "ascending", +    layout_strategy = "bottom_pane", +    prompt_prefix = ">> ", +    prompt_title = "~ LunarVim files ~", +    cwd = get_runtime_dir(), +    search_dirs = { get_runtime_dir() .. "/lvim", lvim.lsp.templates_dir }, +  } +  opts = vim.tbl_deep_extend("force", theme_opts, opts) +  require("telescope.builtin").find_files(opts) +end + +function M.grep_lunarvim_files(opts) +  opts = opts or {} +  local themes = require "telescope.themes" +  local theme_opts = themes.get_ivy { +    sorting_strategy = "ascending", +    layout_strategy = "bottom_pane", +    prompt_prefix = ">> ", +    prompt_title = "~ search LunarVim ~", +    cwd = get_runtime_dir(), +    search_dirs = { get_runtime_dir() .. "/lvim", lvim.lsp.templates_dir }, +  } +  opts = vim.tbl_deep_extend("force", theme_opts, opts) +  require("telescope.builtin").live_grep(opts) +end + +function M.view_lunarvim_changelog() +  local finders = require "telescope.finders" +  local make_entry = require "telescope.make_entry" +  local pickers = require "telescope.pickers" +  local previewers = require "telescope.previewers" +  local actions = require "telescope.actions" +  local opts = {} + +  local conf = require("telescope.config").values +  opts.entry_maker = make_entry.gen_from_git_commits(opts) + +  pickers.new(opts, { +    prompt_title = "LunarVim changelog", + +    finder = finders.new_oneshot_job( +      vim.tbl_flatten { +        "git", +        "log", +        "--pretty=oneline", +        "--abbrev-commit", +        "--", +        ".", +      }, +      opts +    ), +    previewer = { +      previewers.git_commit_diff_to_parent.new(opts), +      previewers.git_commit_diff_to_head.new(opts), +      previewers.git_commit_diff_as_was.new(opts), +      previewers.git_commit_message.new(opts), +    }, + +    --TODO: consider opening a diff view when pressing enter +    attach_mappings = function(_, map) +      map("i", "<enter>", actions._close) +      map("n", "<enter>", actions._close) +      map("i", "<esc>", actions._close) +      map("n", "<esc>", actions._close) +      map("n", "q", actions._close) +      return true +    end, +    sorter = conf.file_sorter(opts), +  }):find() +end + +function M.code_actions() +  local opts = { +    winblend = 15, +    layout_config = { +      prompt_position = "top", +      width = 80, +      height = 12, +    }, +    borderchars = { +      prompt = { "─", "│", " ", "│", "╭", "╮", "│", "│" }, +      results = { "─", "│", "─", "│", "├", "┤", "╯", "╰" }, +      preview = { "─", "│", "─", "│", "╭", "╮", "╯", "╰" }, +    }, +    border = {}, +    previewer = false, +    shorten_path = false, +  } +  require("telescope.builtin").lsp_code_actions(require("telescope.themes").get_dropdown(opts)) +end + +function M.setup() +  local telescope = require "telescope" + +  telescope.setup(lvim.builtin.telescope) +  if lvim.builtin.project.active then +    telescope.load_extension "projects" +  end + +  if lvim.builtin.telescope.on_config_done then +    lvim.builtin.telescope.on_config_done(telescope) +  end +end + +return M diff --git a/lua/lvim/core/terminal.lua b/lua/lvim/core/terminal.lua new file mode 100644 index 00000000..aa6989ec --- /dev/null +++ b/lua/lvim/core/terminal.lua @@ -0,0 +1,114 @@ +local M = {} +local Log = require "lvim.core.log" + +M.config = function() +  lvim.builtin["terminal"] = { +    on_config_done = nil, +    -- size can be a number or function which is passed the current terminal +    size = 20, +    -- open_mapping = [[<c-\>]], +    open_mapping = [[<c-t>]], +    hide_numbers = true, -- hide the number column in toggleterm buffers +    shade_filetypes = {}, +    shade_terminals = true, +    shading_factor = 2, -- the degree by which to darken to terminal colour, default: 1 for dark backgrounds, 3 for light +    start_in_insert = true, +    insert_mappings = true, -- whether or not the open mapping applies in insert mode +    persist_size = false, +    -- direction = 'vertical' | 'horizontal' | 'window' | 'float', +    direction = "float", +    close_on_exit = true, -- close the terminal window when the process exits +    shell = vim.o.shell, -- change the default shell +    -- This field is only relevant if direction is set to 'float' +    float_opts = { +      -- The border key is *almost* the same as 'nvim_win_open' +      -- see :h nvim_win_open for details on borders however +      -- the 'curved' border is a custom border type +      -- not natively supported but implemented in this plugin. +      -- border = 'single' | 'double' | 'shadow' | 'curved' | ... other options supported by win open +      border = "curved", +      -- width = <value>, +      -- height = <value>, +      winblend = 0, +      highlights = { +        border = "Normal", +        background = "Normal", +      }, +    }, +    -- Add executables on the config.lua +    -- { exec, keymap, name} +    -- lvim.builtin.terminal.execs = {{}} to overwrite +    -- lvim.builtin.terminal.execs[#lvim.builtin.terminal.execs+1] = {"gdb", "tg", "GNU Debugger"} +    execs = { +      { "lazygit", "gg", "LazyGit" }, +    }, +  } +end + +M.setup = function() +  local terminal = require "toggleterm" +  for _, exec in pairs(lvim.builtin.terminal.execs) do +    require("lvim.core.terminal").add_exec(exec[1], exec[2], exec[3]) +  end +  terminal.setup(lvim.builtin.terminal) + +  if lvim.builtin.terminal.on_config_done then +    lvim.builtin.terminal.on_config_done(terminal) +  end +end + +M.add_exec = function(exec, keymap, name) +  vim.api.nvim_set_keymap( +    "n", +    "<leader>" .. keymap, +    "<cmd>lua require('lvim.core.terminal')._exec_toggle('" .. exec .. "')<CR>", +    { noremap = true, silent = true } +  ) +  lvim.builtin.which_key.mappings[keymap] = name +end + +M._split = function(inputstr, sep) +  if sep == nil then +    sep = "%s" +  end +  local t = {} +  for str in string.gmatch(inputstr, "([^" .. sep .. "]+)") do +    table.insert(t, str) +  end +  return t +end + +M._exec_toggle = function(exec) +  local binary = M._split(exec)[1] +  if vim.fn.executable(binary) ~= 1 then +    Log:error("Unable to run executable " .. binary .. ". Please make sure it is installed properly.") +    return +  end +  local Terminal = require("toggleterm.terminal").Terminal +  local exec_term = Terminal:new { cmd = exec, hidden = true } +  exec_term:toggle() +end + +---Toggles a log viewer according to log.viewer.layout_config +---@param logfile string the fullpath to the logfile +M.toggle_log_view = function(logfile) +  local log_viewer = lvim.log.viewer.cmd +  if vim.fn.executable(log_viewer) ~= 1 then +    log_viewer = "less +F" +  end +  log_viewer = log_viewer .. " " .. logfile +  local term_opts = vim.tbl_deep_extend("force", lvim.builtin.terminal, { +    cmd = log_viewer, +    open_mapping = lvim.log.viewer.layout_config.open_mapping, +    direction = lvim.log.viewer.layout_config.direction, +    -- TODO: this might not be working as expected +    size = lvim.log.viewer.layout_config.size, +    float_opts = lvim.log.viewer.layout_config.float_opts, +  }) + +  local Terminal = require("toggleterm.terminal").Terminal +  local log_view = Terminal:new(term_opts) +  log_view:toggle() +end + +return M diff --git a/lua/lvim/core/treesitter.lua b/lua/lvim/core/treesitter.lua new file mode 100644 index 00000000..ce99deba --- /dev/null +++ b/lua/lvim/core/treesitter.lua @@ -0,0 +1,81 @@ +local M = {} +local Log = require "lvim.core.log" + +M.config = function() +  lvim.builtin.treesitter = { +    on_config_done = nil, +    ensure_installed = {}, -- one of "all", "maintained" (parsers with maintainers), or a list of languages +    ignore_install = {}, +    matchup = { +      enable = false, -- mandatory, false will disable the whole extension +      -- disable = { "c", "ruby" },  -- optional, list of language that will be disabled +    }, +    highlight = { +      enable = true, -- false will disable the whole extension +      additional_vim_regex_highlighting = true, +      disable = { "latex" }, +    }, +    context_commentstring = { +      enable = false, +      config = { css = "// %s" }, +    }, +    -- indent = {enable = true, disable = {"python", "html", "javascript"}}, +    -- TODO seems to be broken +    indent = { enable = true, disable = { "yaml" } }, +    autotag = { enable = false }, +    textobjects = { +      swap = { +        enable = false, +        -- swap_next = textobj_swap_keymaps, +      }, +      -- move = textobj_move_keymaps, +      select = { +        enable = false, +        -- keymaps = textobj_sel_keymaps, +      }, +    }, +    textsubjects = { +      enable = false, +      keymaps = { ["."] = "textsubjects-smart", [";"] = "textsubjects-big" }, +    }, +    playground = { +      enable = false, +      disable = {}, +      updatetime = 25, -- Debounced time for highlighting nodes in the playground from source code +      persist_queries = false, -- Whether the query persists across vim sessions +      keybindings = { +        toggle_query_editor = "o", +        toggle_hl_groups = "i", +        toggle_injected_languages = "t", +        toggle_anonymous_nodes = "a", +        toggle_language_display = "I", +        focus_language = "f", +        unfocus_language = "F", +        update = "R", +        goto_node = "<cr>", +        show_help = "?", +      }, +    }, +    rainbow = { +      enable = false, +      extended_mode = true, -- Highlight also non-parentheses delimiters, boolean or table: lang -> boolean +      max_file_lines = 1000, -- Do not enable for files with more than 1000 lines, int +    }, +  } +end + +M.setup = function() +  local status_ok, treesitter_configs = pcall(require, "nvim-treesitter.configs") +  if not status_ok then +    Log:get_default().error "Failed to load nvim-treesitter.configs" +    return +  end + +  treesitter_configs.setup(lvim.builtin.treesitter) + +  if lvim.builtin.treesitter.on_config_done then +    lvim.builtin.treesitter.on_config_done(treesitter_configs) +  end +end + +return M diff --git a/lua/lvim/core/which-key.lua b/lua/lvim/core/which-key.lua new file mode 100644 index 00000000..15f63273 --- /dev/null +++ b/lua/lvim/core/which-key.lua @@ -0,0 +1,270 @@ +local M = {} + +M.config = function() +  lvim.builtin.which_key = { +    ---@usage disable which-key completely [not recommeded] +    active = true, +    on_config_done = nil, +    setup = { +      plugins = { +        marks = true, -- shows a list of your marks on ' and ` +        registers = true, -- shows your registers on " in NORMAL or <C-r> in INSERT mode +        -- the presets plugin, adds help for a bunch of default keybindings in Neovim +        -- No actual key bindings are created +        presets = { +          operators = false, -- adds help for operators like d, y, ... +          motions = false, -- adds help for motions +          text_objects = false, -- help for text objects triggered after entering an operator +          windows = true, -- default bindings on <c-w> +          nav = true, -- misc bindings to work with windows +          z = true, -- bindings for folds, spelling and others prefixed with z +          g = true, -- bindings for prefixed with g +        }, +        spelling = { enabled = true, suggestions = 20 }, -- use which-key for spelling hints +      }, +      icons = { +        breadcrumb = "»", -- symbol used in the command line area that shows your active key combo +        separator = "➜", -- symbol used between a key and it's label +        group = "+", -- symbol prepended to a group +      }, +      window = { +        border = "single", -- none, single, double, shadow +        position = "bottom", -- bottom, top +        margin = { 1, 0, 1, 0 }, -- extra window margin [top, right, bottom, left] +        padding = { 2, 2, 2, 2 }, -- extra window padding [top, right, bottom, left] +      }, +      layout = { +        height = { min = 4, max = 25 }, -- min and max height of the columns +        width = { min = 20, max = 50 }, -- min and max width of the columns +        spacing = 3, -- spacing between columns +      }, +      hidden = { "<silent>", "<cmd>", "<Cmd>", "<CR>", "call", "lua", "^:", "^ " }, -- hide mapping boilerplate +      show_help = true, -- show help message on the command line when the popup is visible +    }, + +    opts = { +      mode = "n", -- NORMAL mode +      prefix = "<leader>", +      buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings +      silent = true, -- use `silent` when creating keymaps +      noremap = true, -- use `noremap` when creating keymaps +      nowait = true, -- use `nowait` when creating keymaps +    }, +    vopts = { +      mode = "v", -- VISUAL mode +      prefix = "<leader>", +      buffer = nil, -- Global mappings. Specify a buffer number for buffer local mappings +      silent = true, -- use `silent` when creating keymaps +      noremap = true, -- use `noremap` when creating keymaps +      nowait = true, -- use `nowait` when creating keymaps +    }, +    -- NOTE: Prefer using : over <cmd> as the latter avoids going back in normal-mode. +    -- see https://neovim.io/doc/user/map.html#:map-cmd +    vmappings = { +      ["/"] = { ":CommentToggle<CR>", "Comment" }, +    }, +    mappings = { +      ["w"] = { "<cmd>w!<CR>", "Save" }, +      ["q"] = { "<cmd>q!<CR>", "Quit" }, +      ["/"] = { "<cmd>CommentToggle<CR>", "Comment" }, +      ["c"] = { "<cmd>BufferClose!<CR>", "Close Buffer" }, +      ["f"] = { "<cmd>Telescope find_files<CR>", "Find File" }, +      ["h"] = { "<cmd>nohlsearch<CR>", "No Highlight" }, +      b = { +        name = "Buffers", +        j = { "<cmd>BufferPick<cr>", "Jump" }, +        f = { "<cmd>Telescope buffers<cr>", "Find" }, +        b = { "<cmd>b#<cr>", "Previous" }, +        w = { "<cmd>BufferWipeout<cr>", "Wipeout" }, +        e = { +          "<cmd>BufferCloseAllButCurrent<cr>", +          "Close all but current", +        }, +        h = { "<cmd>BufferCloseBuffersLeft<cr>", "Close all to the left" }, +        l = { +          "<cmd>BufferCloseBuffersRight<cr>", +          "Close all to the right", +        }, +        D = { +          "<cmd>BufferOrderByDirectory<cr>", +          "Sort by directory", +        }, +        L = { +          "<cmd>BufferOrderByLanguage<cr>", +          "Sort by language", +        }, +      }, +      p = { +        name = "Packer", +        c = { "<cmd>PackerCompile<cr>", "Compile" }, +        i = { "<cmd>PackerInstall<cr>", "Install" }, +        r = { "<cmd>lua require('lvim.utils').reload_lv_config()<cr>", "Reload" }, +        s = { "<cmd>PackerSync<cr>", "Sync" }, +        S = { "<cmd>PackerStatus<cr>", "Status" }, +        u = { "<cmd>PackerUpdate<cr>", "Update" }, +      }, + +      -- " Available Debug Adapters: +      -- "   https://microsoft.github.io/debug-adapter-protocol/implementors/adapters/ +      -- " Adapter configuration and installation instructions: +      -- "   https://github.com/mfussenegger/nvim-dap/wiki/Debug-Adapter-installation +      -- " Debug Adapter protocol: +      -- "   https://microsoft.github.io/debug-adapter-protocol/ +      -- " Debugging +      g = { +        name = "Git", +        j = { "<cmd>lua require 'gitsigns'.next_hunk()<cr>", "Next Hunk" }, +        k = { "<cmd>lua require 'gitsigns'.prev_hunk()<cr>", "Prev Hunk" }, +        l = { "<cmd>lua require 'gitsigns'.blame_line()<cr>", "Blame" }, +        p = { "<cmd>lua require 'gitsigns'.preview_hunk()<cr>", "Preview Hunk" }, +        r = { "<cmd>lua require 'gitsigns'.reset_hunk()<cr>", "Reset Hunk" }, +        R = { "<cmd>lua require 'gitsigns'.reset_buffer()<cr>", "Reset Buffer" }, +        s = { "<cmd>lua require 'gitsigns'.stage_hunk()<cr>", "Stage Hunk" }, +        u = { +          "<cmd>lua require 'gitsigns'.undo_stage_hunk()<cr>", +          "Undo Stage Hunk", +        }, +        o = { "<cmd>Telescope git_status<cr>", "Open changed file" }, +        b = { "<cmd>Telescope git_branches<cr>", "Checkout branch" }, +        c = { "<cmd>Telescope git_commits<cr>", "Checkout commit" }, +        C = { +          "<cmd>Telescope git_bcommits<cr>", +          "Checkout commit(for current file)", +        }, +        d = { +          "<cmd>Gitsigns diffthis HEAD<cr>", +          "Git Diff", +        }, +      }, + +      l = { +        name = "LSP", +        a = { "<cmd>lua require('core.telescope').code_actions()<cr>", "Code Action" }, +        d = { +          "<cmd>Telescope lsp_document_diagnostics<cr>", +          "Document Diagnostics", +        }, +        w = { +          "<cmd>Telescope lsp_workspace_diagnostics<cr>", +          "Workspace Diagnostics", +        }, +        f = { "<cmd>lua vim.lsp.buf.formatting()<cr>", "Format" }, +        i = { "<cmd>LspInfo<cr>", "Info" }, +        I = { "<cmd>LspInstallInfo<cr>", "Installer Info" }, +        j = { +          "<cmd>lua vim.lsp.diagnostic.goto_next({popup_opts = {border = lvim.lsp.popup_border}})<cr>", +          "Next Diagnostic", +        }, +        k = { +          "<cmd>lua vim.lsp.diagnostic.goto_prev({popup_opts = {border = lvim.lsp.popup_border}})<cr>", +          "Prev Diagnostic", +        }, +        l = { "<cmd>lua vim.lsp.codelens.run()<cr>", "CodeLens Action" }, +        p = { +          name = "Peek", +          d = { "<cmd>lua require('lvim.lsp.peek').Peek('definition')<cr>", "Definition" }, +          t = { "<cmd>lua require('lvim.lsp.peek').Peek('typeDefinition')<cr>", "Type Definition" }, +          i = { "<cmd>lua require('lvim.lsp.peek').Peek('implementation')<cr>", "Implementation" }, +        }, +        q = { "<cmd>lua vim.lsp.diagnostic.set_loclist()<cr>", "Quickfix" }, +        r = { "<cmd>lua vim.lsp.buf.rename()<cr>", "Rename" }, +        s = { "<cmd>Telescope lsp_document_symbols<cr>", "Document Symbols" }, +        S = { +          "<cmd>Telescope lsp_dynamic_workspace_symbols<cr>", +          "Workspace Symbols", +        }, +      }, +      L = { +        name = "+LunarVim", +        c = { +          "<cmd>edit" .. get_config_dir() .. "/config.lua<cr>", +          "Edit config.lua", +        }, +        f = { +          "<cmd>lua require('lvim.core.telescope').find_lunarvim_files()<cr>", +          "Find LunarVim files", +        }, +        g = { +          "<cmd>lua require('lvim.core.telescope').grep_lunarvim_files()<cr>", +          "Grep LunarVim files", +        }, +        k = { "<cmd>lua require('lvim.keymappings').print()<cr>", "View LunarVim's default keymappings" }, +        i = { +          "<cmd>lua require('lvim.core.info').toggle_popup(vim.bo.filetype)<cr>", +          "Toggle LunarVim Info", +        }, +        I = { +          "<cmd>lua require('lvim.core.telescope').view_lunarvim_changelog()<cr>", +          "View LunarVim's changelog", +        }, +        l = { +          name = "+logs", +          d = { +            "<cmd>lua require('lvim.core.terminal').toggle_log_view(require('lvim.core.log').get_path())<cr>", +            "view default log", +          }, +          D = { +            "<cmd>lua vim.fn.execute('edit ' .. require('lvim.core.log').get_path())<cr>", +            "Open the default logfile", +          }, +          l = { "<cmd>lua require('lvim.core.terminal').toggle_log_view(vim.lsp.get_log_path())<cr>", "view lsp log" }, +          L = { "<cmd>lua vim.fn.execute('edit ' .. vim.lsp.get_log_path())<cr>", "Open the LSP logfile" }, +          n = { +            "<cmd>lua require('lvim.core.terminal').toggle_log_view(os.getenv('NVIM_LOG_FILE'))<cr>", +            "view neovim log", +          }, +          N = { "<cmd>edit $NVIM_LOG_FILE<cr>", "Open the Neovim logfile" }, +          p = { +            "<cmd>lua require('lvim.core.terminal').toggle_log_view('packer.nvim')<cr>", +            "view packer log", +          }, +          P = { "<cmd>exe 'edit '.stdpath('cache').'/packer.nvim.log'<cr>", "Open the Packer logfile" }, +        }, +        r = { "<cmd>lua require('lvim.utils').reload_lv_config()<cr>", "Reload configurations" }, +        u = { "<cmd>LvimUpdate<cr>", "Update LunarVim" }, +      }, +      s = { +        name = "Search", +        b = { "<cmd>Telescope git_branches<cr>", "Checkout branch" }, +        c = { "<cmd>Telescope colorscheme<cr>", "Colorscheme" }, +        f = { "<cmd>Telescope find_files<cr>", "Find File" }, +        h = { "<cmd>Telescope help_tags<cr>", "Find Help" }, +        M = { "<cmd>Telescope man_pages<cr>", "Man Pages" }, +        r = { "<cmd>Telescope oldfiles<cr>", "Open Recent File" }, +        R = { "<cmd>Telescope registers<cr>", "Registers" }, +        t = { "<cmd>Telescope live_grep<cr>", "Text" }, +        k = { "<cmd>Telescope keymaps<cr>", "Keymaps" }, +        C = { "<cmd>Telescope commands<cr>", "Commands" }, +        p = { +          "<cmd>lua require('telescope.builtin.internal').colorscheme({enable_preview = true})<cr>", +          "Colorscheme with Preview", +        }, +      }, +      T = { +        name = "Treesitter", +        i = { ":TSConfigInfo<cr>", "Info" }, +      }, +    }, +  } +end + +M.setup = function() +  local which_key = require "which-key" + +  which_key.setup(lvim.builtin.which_key.setup) + +  local opts = lvim.builtin.which_key.opts +  local vopts = lvim.builtin.which_key.vopts + +  local mappings = lvim.builtin.which_key.mappings +  local vmappings = lvim.builtin.which_key.vmappings + +  which_key.register(mappings, opts) +  which_key.register(vmappings, vopts) + +  if lvim.builtin.which_key.on_config_done then +    lvim.builtin.which_key.on_config_done(which_key) +  end +end + +return M diff --git a/lua/lvim/impatient.lua b/lua/lvim/impatient.lua new file mode 100644 index 00000000..4fdc0026 --- /dev/null +++ b/lua/lvim/impatient.lua @@ -0,0 +1,360 @@ +-- modified version from https://github.com/lewis6991/impatient.nvim + +local vim = vim +local uv = vim.loop +local impatient_load_start = uv.hrtime() +local api = vim.api +local ffi = require "ffi" + +local get_option, set_option = api.nvim_get_option, api.nvim_set_option +local get_runtime_file = api.nvim_get_runtime_file + +local impatient_dur + +local M = { +  cache = {}, +  profile = nil, +  dirty = false, +  path = nil, +  log = {}, +} + +_G.__luacache = M + +--{{{ +local cachepack = {} + +-- using double for packing/unpacking numbers has no conversion overhead +-- 32-bit ARM causes a bus error when casting to double, so use int there +local number_t = jit.arch ~= "arm" and "double" or "int" +ffi.cdef("typedef " .. number_t .. " number_t;") + +local c_number_t = ffi.typeof "number_t[1]" +local c_sizeof_number_t = ffi.sizeof "number_t" + +local out_buf = {} + +function out_buf.write_number(buf, num) +  buf[#buf + 1] = ffi.string(c_number_t(num), c_sizeof_number_t) +end + +function out_buf.write_string(buf, str) +  out_buf.write_number(buf, #str) +  buf[#buf + 1] = str +end + +function out_buf.to_string(buf) +  return table.concat(buf) +end + +local in_buf = {} + +function in_buf.read_number(buf) +  if buf.size < buf.pos then +    error "buffer access violation" +  end +  local res = ffi.cast("number_t*", buf.ptr + buf.pos)[0] +  buf.pos = buf.pos + c_sizeof_number_t +  return res +end + +function in_buf.read_string(buf) +  local len = in_buf.read_number(buf) +  local res = ffi.string(buf.ptr + buf.pos, len) +  buf.pos = buf.pos + len + +  return res +end + +function cachepack.pack(cache) +  local total_keys = vim.tbl_count(cache) +  local buf = {} + +  out_buf.write_number(buf, total_keys) +  for k, v in pairs(cache) do +    out_buf.write_string(buf, k) +    out_buf.write_string(buf, v[1] or "") +    out_buf.write_number(buf, v[2] or 0) +    out_buf.write_string(buf, v[3] or "") +  end + +  return out_buf.to_string(buf) +end + +function cachepack.unpack(str, raw_buf_size) +  if raw_buf_size == 0 or str == nil or (raw_buf_size == nil and #str == 0) then +    return {} +  end + +  local buf = { +    ptr = raw_buf_size and str or ffi.new("const char[?]", #str, str), +    pos = 0, +    size = raw_buf_size or #str, +  } +  local cache = {} + +  local total_keys = in_buf.read_number(buf) +  for _ = 1, total_keys do +    local k = in_buf.read_string(buf) +    local v = { +      in_buf.read_string(buf), +      in_buf.read_number(buf), +      in_buf.read_string(buf), +    } +    cache[k] = v +  end + +  return cache +end +--}}} + +local function log(...) +  M.log[#M.log + 1] = table.concat({ string.format(...) }, " ") +end + +function M.print_log() +  for _, l in ipairs(M.log) do +    print(l) +  end +end + +function M.enable_profile() +  M.profile = {} +  M.print_profile = function() +    M.profile["lvim.impatient"] = { +      resolve = 0, +      load = impatient_dur, +      loader = "standard", +    } +    require("lvim.impatient.profile").print_profile(M.profile) +  end +  vim.cmd [[command! LuaCacheProfile lua _G.__luacache.print_profile()]] +end + +local function is_cacheable(path) +  -- Don't cache files in /tmp since they are not likely to persist. +  -- Note: Appimage versions of Neovim mount $VIMRUNTIME in /tmp in a unique +  -- directory on each launch. +  return not vim.startswith(path, "/tmp/") +end + +local function hash(modpath) +  local stat = uv.fs_stat(modpath) +  if stat then +    return stat.mtime.sec +  end +end + +local function hrtime() +  if M.profile then +    return uv.hrtime() +  end +end + +local function load_package_with_cache(name, loader) +  local resolve_start = hrtime() + +  local basename = name:gsub("%.", "/") +  local paths = { "lua/" .. basename .. ".lua", "lua/" .. basename .. "/init.lua" } + +  for _, path in ipairs(paths) do +    local modpath = get_runtime_file(path, false)[1] +    if modpath then +      local load_start = hrtime() +      local chunk, err = loadfile(modpath) + +      if M.profile then +        M.profile[name] = { +          resolve = load_start - resolve_start, +          load = hrtime() - load_start, +          loader = loader or "standard", +        } +      end + +      if chunk == nil then +        return err +      end + +      if is_cacheable(modpath) then +        log("Creating cache for module %s", name) +        M.cache[name] = { modpath, hash(modpath), string.dump(chunk) } +        M.dirty = true +      else +        log("Unable to cache module %s", name) +      end + +      return chunk +    end +  end +  return nil +end + +local reduced_rtp + +-- Speed up non-cached loads by reducing the rtp path during requires +function M.update_reduced_rtp() +  local luadirs = get_runtime_file("lua/", true) + +  for i = 1, #luadirs do +    luadirs[i] = luadirs[i]:sub(1, -6) +  end + +  reduced_rtp = table.concat(luadirs, ",") +end + +local function load_package_with_cache_reduced_rtp(name) +  local orig_rtp = get_option "runtimepath" +  local orig_ei = get_option "eventignore" + +  if not reduced_rtp then +    M.update_reduced_rtp() +  end + +  set_option("eventignore", "all") +  set_option("rtp", reduced_rtp) + +  local found = load_package_with_cache(name, "reduced") + +  set_option("rtp", orig_rtp) +  set_option("eventignore", orig_ei) + +  return found +end + +local function load_from_cache(name) +  local resolve_start = hrtime() +  if M.cache[name] == nil then +    log("No cache for module %s", name) +    return "No cache entry" +  end + +  local modpath, mhash, codes = unpack(M.cache[name]) + +  if mhash ~= hash(modpath) then +    log("Stale cache for module %s", name) +    M.cache[name] = nil +    M.dirty = true +    return "Stale cache" +  end + +  local load_start = hrtime() +  local chunk = loadstring(codes) + +  if M.profile then +    M.profile[name] = { +      resolve = load_start - resolve_start, +      load = hrtime() - load_start, +      loader = "cache", +    } +  end + +  if not chunk then +    M.cache[name] = nil +    M.dirty = true +    log("Error loading cache for module. Invalidating", name) +    return "Cache error" +  end + +  return chunk +end + +function M.save_cache() +  if M.dirty then +    log("Updating cache file: %s", M.path) +    local f = io.open(M.path, "w+b") +    f:write(cachepack.pack(M.cache)) +    f:flush() +    M.dirty = false +  end +end + +function M.clear_cache() +  M.cache = {} +  os.remove(M.path) +end + +impatient_dur = uv.hrtime() - impatient_load_start + +function M.setup(opts) +  opts = opts or {} +  M.path = opts.path or vim.fn.stdpath "cache" .. "/lvim_cache" + +  if opts.enable_profiling then +    M.enable_profile() +  end + +  local impatient_setup_start = uv.hrtime() +  local stat = uv.fs_stat(M.path) +  if stat then +    log("Loading cache file %s", M.path) +    local ok +    -- Linux/macOS lets us easily mmap the cache file for faster reads without passing to Lua +    if jit.os == "Linux" or jit.os == "OSX" then +      local size = stat.size + +      local C = ffi.C +      local O_RDONLY = 0x00 +      local PROT_READ = 0x01 +      local MAP_PRIVATE = 0x02 + +      ffi.cdef [[ +      int open(const char *pathname, int flags); +      int close(int fd); +      void *mmap(void *addr, size_t length, int prot, int flags, int fd, long int offset); +      int munmap(void *addr, size_t length); +      ]] +      local f = C.open(M.path, O_RDONLY) + +      local addr = C.mmap(nil, size, PROT_READ, MAP_PRIVATE, f, 0) +      ok = ffi.cast("intptr_t", addr) ~= -1 + +      if ok then +        M.cache = cachepack.unpack(ffi.cast("char *", addr), size) +        C.munmap(addr, size) +      end + +      C.close(f) +    else +      local f = io.open(M.path, "rb") +      ok, M.cache = pcall(function() +        return cachepack.unpack(f:read "*a") +      end) +    end + +    if not ok then +      log("Corrupted cache file, %s. Invalidating...", M.path) +      os.remove(M.path) +      M.cache = {} +    end +    M.dirty = not ok +  end + +  local insert = table.insert +  local package = package + +  -- Fix the position of the preloader. This also makes loading modules like 'ffi' +  -- and 'bit' quicker +  if package.loaders[1] == vim._load_package then +    -- Move vim._load_package to the second position +    local vim_load = table.remove(package.loaders, 1) +    insert(package.loaders, 2, vim_load) +  end + +  insert(package.loaders, 2, load_from_cache) +  insert(package.loaders, 3, load_package_with_cache_reduced_rtp) +  insert(package.loaders, 4, load_package_with_cache) + +  vim.cmd [[ +    augroup impatient +      autocmd VimEnter,VimLeave * lua _G.__luacache.save_cache() +      autocmd OptionSet runtimepath lua _G.__luacache.update_reduced_rtp(true) +    augroup END + +    command! LuaCacheClear lua _G.__luacache.clear_cache() +    command! LuaCacheLog   lua _G.__luacache.print_log() +  ]] + +  impatient_dur = impatient_dur + (uv.hrtime() - impatient_setup_start) +end + +return M diff --git a/lua/lvim/impatient/profile.lua b/lua/lvim/impatient/profile.lua new file mode 100644 index 00000000..0f4f8236 --- /dev/null +++ b/lua/lvim/impatient/profile.lua @@ -0,0 +1,145 @@ +local M = {} + +local api = vim.api + +function M.print_profile(profile) +  if not profile then +    print "Error: profiling was not enabled" +    return +  end + +  local total_resolve = 0 +  local total_load = 0 +  local name_pad = 0 +  local modules = {} +  local plugins = {} + +  for module, p in pairs(profile) do +    p.resolve = p.resolve / 1000000 +    p.load = p.load / 1000000 +    p.total = p.resolve + p.load +    p.module = module:gsub("/", ".") + +    local plugin = p.module:match "([^.]+)" +    if plugin then +      if not plugins[plugin] then +        plugins[plugin] = { +          module = plugin, +          resolve = 0, +          load = 0, +          total = 0, +        } +      end +      local r = plugins[plugin] + +      r.resolve = r.resolve + p.resolve +      r.load = r.load + p.load +      r.total = r.total + p.total + +      if not r.loader then +        r.loader = p.loader +      elseif r.loader ~= p.loader then +        r.loader = "mixed" +      end +    end + +    total_resolve = total_resolve + p.resolve +    total_load = total_load + p.load + +    if #module > name_pad then +      name_pad = #module +    end + +    modules[#modules + 1] = p +  end + +  table.sort(modules, function(a, b) +    return a.module > b.module +  end) + +  do +    local plugins_a = {} +    for _, v in pairs(plugins) do +      plugins_a[#plugins_a + 1] = v +    end +    plugins = plugins_a +  end + +  table.sort(plugins, function(a, b) +    return a.total > b.total +  end) + +  local lines = {} +  local function add(...) +    lines[#lines + 1] = string.format(...) +  end + +  local l = string.rep("─", name_pad + 1) + +  add( +    "%s┬───────────┬────────────┬────────────┬────────────┐", +    l +  ) +  add("%-" .. name_pad .. "s │ Loader    │ Resolve    │ Load       │ Total      │", "") +  add( +    "%s┼───────────┼────────────┼────────────┼────────────┤", +    l +  ) +  add( +    "%-" .. name_pad .. "s │           │ %8.4fms │ %8.4fms │ %8.4fms │", +    "Total", +    total_resolve, +    total_load, +    total_resolve + total_load +  ) +  add( +    "%s┴───────────┴────────────┴────────────┴────────────┤", +    l +  ) +  add("%-" .. name_pad .. "s                                                    │", "By Plugin") +  add( +    "%s┬───────────┬────────────┬────────────┬────────────┤", +    l +  ) +  for _, p in ipairs(plugins) do +    add( +      "%-" .. name_pad .. "s │ %9s │ %8.4fms │ %8.4fms │ %8.4fms │", +      p.module, +      p.loader, +      p.resolve, +      p.load, +      p.total +    ) +  end +  add( +    "%s┴───────────┴────────────┴────────────┴────────────┤", +    l +  ) +  add("%-" .. name_pad .. "s                                                    │", "By Module") +  add( +    "%s┬───────────┬────────────┬────────────┬────────────┤", +    l +  ) +  for _, p in pairs(modules) do +    add( +      "%-" .. name_pad .. "s │ %9s │ %8.4fms │ %8.4fms │ %8.4fms │", +      p.module, +      p.loader, +      p.resolve, +      p.load, +      p.total +    ) +  end +  add( +    "%s┴───────────┴────────────┴────────────┴────────────┘", +    l +  ) + +  local bufnr = api.nvim_create_buf(false, false) +  api.nvim_buf_set_lines(bufnr, 0, 0, false, lines) +  api.nvim_buf_set_option(bufnr, "buftype", "nofile") +  api.nvim_buf_set_name(bufnr, "Impatient Profile Report") +  api.nvim_set_current_buf(bufnr) +end + +return M diff --git a/lua/lvim/interface/popup.lua b/lua/lvim/interface/popup.lua new file mode 100644 index 00000000..b628125c --- /dev/null +++ b/lua/lvim/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 diff --git a/lua/lvim/interface/text.lua b/lua/lvim/interface/text.lua new file mode 100644 index 00000000..6bf280e8 --- /dev/null +++ b/lua/lvim/interface/text.lua @@ -0,0 +1,95 @@ +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 + +--- Left 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_left(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_right(lines, indent_amount) +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_center(container, lines, alignment) +  local output = {} +  local max_len = max_len_line(lines) + +  for _, line in ipairs(lines) do +    local padding = string.rep(" ", (math.max(container.width, max_len) - line:len()) * alignment) +    table.insert(output, padding .. line) +  end + +  return output +end + +--- Shift lines by a given amount +-- @params lines The lines the shift +-- @param amount The amount of spaces to add +function M.shift_right(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 diff --git a/lua/lvim/keymappings.lua b/lua/lvim/keymappings.lua new file mode 100644 index 00000000..68a49393 --- /dev/null +++ b/lua/lvim/keymappings.lua @@ -0,0 +1,180 @@ +local M = {} +local Log = require "lvim.core.log" + +local generic_opts_any = { noremap = true, silent = true } + +local generic_opts = { +  insert_mode = generic_opts_any, +  normal_mode = generic_opts_any, +  visual_mode = generic_opts_any, +  visual_block_mode = generic_opts_any, +  command_mode = generic_opts_any, +  term_mode = { silent = true }, +} + +local mode_adapters = { +  insert_mode = "i", +  normal_mode = "n", +  term_mode = "t", +  visual_mode = "v", +  visual_block_mode = "x", +  command_mode = "c", +} + +-- Append key mappings to lunarvim's defaults for a given mode +-- @param keymaps The table of key mappings containing a list per mode (normal_mode, insert_mode, ..) +function M.append_to_defaults(keymaps) +  for mode, mappings in pairs(keymaps) do +    for k, v in ipairs(mappings) do +      lvim.keys[mode][k] = v +    end +  end +end + +-- Set key mappings individually +-- @param mode The keymap mode, can be one of the keys of mode_adapters +-- @param key The key of keymap +-- @param val Can be form as a mapping or tuple of mapping and user defined opt +function M.set_keymaps(mode, key, val) +  local opt = generic_opts[mode] and generic_opts[mode] or generic_opts_any +  if type(val) == "table" then +    opt = val[2] +    val = val[1] +  end +  vim.api.nvim_set_keymap(mode, key, val, opt) +end + +-- Load key mappings for a given mode +-- @param mode The keymap mode, can be one of the keys of mode_adapters +-- @param keymaps The list of key mappings +function M.load_mode(mode, keymaps) +  mode = mode_adapters[mode] and mode_adapters[mode] or mode +  for k, v in pairs(keymaps) do +    M.set_keymaps(mode, k, v) +  end +end + +-- Load key mappings for all provided modes +-- @param keymaps A list of key mappings for each mode +function M.load(keymaps) +  for mode, mapping in pairs(keymaps) do +    M.load_mode(mode, mapping) +  end +end + +function M.config() +  lvim.keys = { +    ---@usage change or add keymappings for insert mode +    insert_mode = { +      -- 'jk' for quitting insert mode +      ["jk"] = "<ESC>", +      -- 'kj' for quitting insert mode +      ["kj"] = "<ESC>", +      -- 'jj' for quitting insert mode +      ["jj"] = "<ESC>", +      -- Move current line / block with Alt-j/k ala vscode. +      ["<A-j>"] = "<Esc>:m .+1<CR>==gi", +      -- Move current line / block with Alt-j/k ala vscode. +      ["<A-k>"] = "<Esc>:m .-2<CR>==gi", +      -- navigation +      ["<A-Up>"] = "<C-\\><C-N><C-w>k", +      ["<A-Down>"] = "<C-\\><C-N><C-w>j", +      ["<A-Left>"] = "<C-\\><C-N><C-w>h", +      ["<A-Right>"] = "<C-\\><C-N><C-w>l", +      -- navigate tab completion with <c-j> and <c-k> +      -- runs conditionally +      ["<C-j>"] = { 'pumvisible() ? "\\<down>" : "\\<C-j>"', { expr = true, noremap = true } }, +      ["<C-k>"] = { 'pumvisible() ? "\\<up>" : "\\<C-k>"', { expr = true, noremap = true } }, +    }, + +    ---@usage change or add keymappings for normal mode +    normal_mode = { +      -- Better window movement +      ["<C-h>"] = "<C-w>h", +      ["<C-j>"] = "<C-w>j", +      ["<C-k>"] = "<C-w>k", +      ["<C-l>"] = "<C-w>l", + +      -- Resize with arrows +      ["<C-Up>"] = ":resize -2<CR>", +      ["<C-Down>"] = ":resize +2<CR>", +      ["<C-Left>"] = ":vertical resize -2<CR>", +      ["<C-Right>"] = ":vertical resize +2<CR>", + +      -- Tab switch buffer +      ["<S-l>"] = ":BufferNext<CR>", +      ["<S-h>"] = ":BufferPrevious<CR>", + +      -- Move current line / block with Alt-j/k a la vscode. +      ["<A-j>"] = ":m .+1<CR>==", +      ["<A-k>"] = ":m .-2<CR>==", + +      -- QuickFix +      ["]q"] = ":cnext<CR>", +      ["[q"] = ":cprev<CR>", +      ["<C-q>"] = ":call QuickFixToggle()<CR>", +    }, + +    ---@usage change or add keymappings for terminal mode +    term_mode = { +      -- Terminal window navigation +      ["<C-h>"] = "<C-\\><C-N><C-w>h", +      ["<C-j>"] = "<C-\\><C-N><C-w>j", +      ["<C-k>"] = "<C-\\><C-N><C-w>k", +      ["<C-l>"] = "<C-\\><C-N><C-w>l", +    }, + +    ---@usage change or add keymappings for visual mode +    visual_mode = { +      -- Better indenting +      ["<"] = "<gv", +      [">"] = ">gv", + +      -- ["p"] = '"0p', +      -- ["P"] = '"0P', +    }, + +    ---@usage change or add keymappings for visual block mode +    visual_block_mode = { +      -- Move selected line / block of text in visual mode +      ["K"] = ":move '<-2<CR>gv-gv", +      ["J"] = ":move '>+1<CR>gv-gv", + +      -- Move current line / block with Alt-j/k ala vscode. +      ["<A-j>"] = ":m '>+1<CR>gv-gv", +      ["<A-k>"] = ":m '<-2<CR>gv-gv", +    }, + +    ---@usage change or add keymappings for command mode +    command_mode = { +      -- navigate tab completion with <c-j> and <c-k> +      -- runs conditionally +      ["<C-j>"] = { 'pumvisible() ? "\\<C-n>" : "\\<C-j>"', { expr = true, noremap = true } }, +      ["<C-k>"] = { 'pumvisible() ? "\\<C-p>" : "\\<C-k>"', { expr = true, noremap = true } }, +    }, +  } + +  if vim.fn.has "mac" == 1 then +    lvim.keys.normal_mode["<A-Up>"] = lvim.keys.normal_mode["<C-Up>"] +    lvim.keys.normal_mode["<A-Down>"] = lvim.keys.normal_mode["<C-Down>"] +    lvim.keys.normal_mode["<A-Left>"] = lvim.keys.normal_mode["<C-Left>"] +    lvim.keys.normal_mode["<A-Right>"] = lvim.keys.normal_mode["<C-Right>"] +    Log:debug "Activated mac keymappings" +  end +end + +function M.print(mode) +  print "List of LunarVim's default keymappings (not including which-key)" +  if mode then +    print(vim.inspect(lvim.keys[mode])) +  else +    print(vim.inspect(lvim.keys)) +  end +end + +function M.setup() +  vim.g.mapleader = (lvim.leader == "space" and " ") or lvim.leader +  M.load(lvim.keys) +end + +return M diff --git a/lua/lvim/lsp/config.lua b/lua/lvim/lsp/config.lua new file mode 100644 index 00000000..30336cc2 --- /dev/null +++ b/lua/lvim/lsp/config.lua @@ -0,0 +1,45 @@ +return { +  templates_dir = join_paths(get_runtime_dir(), "site", "after", "ftplugin"), +  diagnostics = { +    signs = { +      active = true, +      values = { +        { name = "LspDiagnosticsSignError", text = "" }, +        { name = "LspDiagnosticsSignWarning", text = "" }, +        { name = "LspDiagnosticsSignHint", text = "" }, +        { name = "LspDiagnosticsSignInformation", text = "" }, +      }, +    }, +    virtual_text = true, +    update_in_insert = false, +    underline = true, +    severity_sort = true, +  }, +  override = {}, +  document_highlight = true, +  code_lens_refresh = true, +  popup_border = "single", +  on_attach_callback = nil, +  on_init_callback = nil, +  automatic_servers_installation = true, +  buffer_mappings = { +    normal_mode = { +      ["K"] = { "<cmd>lua vim.lsp.buf.hover()<CR>", "Show hover" }, +      ["gd"] = { "<cmd>lua vim.lsp.buf.definition()<CR>", "Goto Definition" }, +      ["gD"] = { "<cmd>lua vim.lsp.buf.declaration()<CR>", "Goto declaration" }, +      ["gr"] = { "<cmd>lua vim.lsp.buf.references()<CR>", "Goto references" }, +      ["gI"] = { "<cmd>lua vim.lsp.buf.implementation()<CR>", "Goto Implementation" }, +      ["gs"] = { "<cmd>lua vim.lsp.buf.signature_help()<CR>", "show signature help" }, +      ["gp"] = { "<cmd>lua require'lvim.lsp.peek'.Peek('definition')<CR>", "Peek definition" }, +      ["gl"] = { +        "<cmd>lua require'lvim.lsp.handlers'.show_line_diagnostics()<CR>", +        "Show line diagnostics", +      }, +    }, +    insert_mode = {}, +    visual_mode = {}, +  }, +  null_ls = { +    setup = {}, +  }, +} diff --git a/lua/lvim/lsp/handlers.lua b/lua/lvim/lsp/handlers.lua new file mode 100644 index 00000000..ffb7564a --- /dev/null +++ b/lua/lvim/lsp/handlers.lua @@ -0,0 +1,169 @@ +-- Set Default Prefix. +-- Note: You can set a prefix per lsp server in the lv-globals.lua file +local M = {} + +function M.setup() +  local config = { -- your config +    virtual_text = lvim.lsp.diagnostics.virtual_text, +    signs = lvim.lsp.diagnostics.signs, +    underline = lvim.lsp.diagnostics.underline, +    update_in_insert = lvim.lsp.diagnostics.update_in_insert, +    severity_sort = lvim.lsp.diagnostics.severity_sort, +  } +  if vim.fn.has "nvim-0.5.1" > 0 then +    vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, result, ctx, _) +      local uri = result.uri +      local bufnr = vim.uri_to_bufnr(uri) +      if not bufnr then +        return +      end + +      local diagnostics = result.diagnostics +      local ok, vim_diag = pcall(require, "vim.diagnostic") +      if ok then +        -- FIX: why can't we just use vim.diagnostic.get(buf_id)? +        config.signs = true +        for i, diagnostic in ipairs(diagnostics) do +          local rng = diagnostic.range +          diagnostics[i].lnum = rng["start"].line +          diagnostics[i].end_lnum = rng["end"].line +          diagnostics[i].col = rng["start"].character +          diagnostics[i].end_col = rng["end"].character +        end +        local namespace = vim.lsp.diagnostic.get_namespace(ctx.client_id) + +        vim_diag.set(namespace, bufnr, diagnostics, config) +        if not vim.api.nvim_buf_is_loaded(bufnr) then +          return +        end + +        local sign_names = { +          "DiagnosticSignError", +          "DiagnosticSignWarn", +          "DiagnosticSignInfo", +          "DiagnosticSignHint", +        } +        for i, sign in ipairs(lvim.lsp.diagnostics.signs.values) do +          vim.fn.sign_define(sign_names[i], { texthl = sign_names[i], text = sign.text, numhl = "" }) +        end +        vim_diag.show(namespace, bufnr, diagnostics, config) +      else +        vim.lsp.diagnostic.save(diagnostics, bufnr, ctx.client_id) +        if not vim.api.nvim_buf_is_loaded(bufnr) then +          return +        end +        vim.lsp.diagnostic.display(diagnostics, bufnr, ctx.client_id, config) +      end +    end +  else +    vim.lsp.handlers["textDocument/publishDiagnostics"] = function(_, _, params, client_id, _) +      local uri = params.uri +      local bufnr = vim.uri_to_bufnr(uri) +      if not bufnr then +        return +      end + +      local diagnostics = params.diagnostics +      vim.lsp.diagnostic.save(diagnostics, bufnr, client_id) +      if not vim.api.nvim_buf_is_loaded(bufnr) then +        return +      end +      vim.lsp.diagnostic.display(diagnostics, bufnr, client_id, config) +    end +  end + +  vim.lsp.handlers["textDocument/hover"] = vim.lsp.with(vim.lsp.handlers.hover, { +    border = lvim.lsp.popup_border, +  }) + +  vim.lsp.handlers["textDocument/signatureHelp"] = vim.lsp.with(vim.lsp.handlers.signature_help, { +    border = lvim.lsp.popup_border, +  }) +end + +local function split_by_chunk(text, chunkSize) +  local s = {} +  for i = 1, #text, chunkSize do +    s[#s + 1] = text:sub(i, i + chunkSize - 1) +  end +  return s +end + +function M.show_line_diagnostics() +  -- TODO: replace all this with vim.diagnostic.show_position_diagnostics() +  local diagnostics = vim.lsp.diagnostic.get_line_diagnostics() +  local severity_highlight = { +    "LspDiagnosticsFloatingError", +    "LspDiagnosticsFloatingWarning", +    "LspDiagnosticsFloatingInformation", +    "LspDiagnosticsFloatingHint", +  } +  local ok, vim_diag = pcall(require, "vim.diagnostic") +  if ok then +    local buf_id = vim.api.nvim_win_get_buf(0) +    local win_id = vim.api.nvim_get_current_win() +    local cursor_position = vim.api.nvim_win_get_cursor(win_id) +    severity_highlight = { +      "DiagnosticFloatingError", +      "DiagnosticFloatingWarn", +      "DiagnosticFloatingInfo", +      "DiagnosticFloatingHint", +    } +    diagnostics = vim_diag.get(buf_id, { lnum = cursor_position[1] - 1 }) +  end +  local lines = {} +  local max_width = vim.fn.winwidth(0) - 5 +  local height = #diagnostics +  local width = 0 +  local opts = {} +  local close_events = { "CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre" } +  if height == 0 then +    return +  end +  local bufnr = vim.api.nvim_create_buf(false, true) +  local diag_message +  table.sort(diagnostics, function(a, b) +    return a.severity < b.severity +  end) +  for i, diagnostic in ipairs(diagnostics) do +    local source = diagnostic.source +    diag_message = diagnostic.message:gsub("[\n\r]", " ") +    if source then +      if string.find(source, "/") then +        source = string.sub(diagnostic.source, string.find(diagnostic.source, "([%w-_]+)$")) +      end +      diag_message = string.format("%d. %s: %s", i, source, diag_message) +    else +      diag_message = string.format("%d. %s", i, diag_message) +    end +    if diagnostic.code then +      diag_message = string.format("%s [%s]", diag_message, diagnostic.code) +    end +    local msgs = split_by_chunk(diag_message, max_width) +    for _, diag in ipairs(msgs) do +      table.insert(lines, { message = diag, severity = diagnostic.severity }) +      width = math.max(diag:len(), width) +    end +  end +  height = #lines +  opts = vim.lsp.util.make_floating_popup_options(width, height, opts) +  opts["style"] = "minimal" +  opts["border"] = "rounded" +  opts["focusable"] = true + +  vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") +  local winnr = vim.api.nvim_open_win(bufnr, false, opts) +  vim.api.nvim_win_set_option(winnr, "winblend", 0) +  vim.api.nvim_buf_set_var(bufnr, "lsp_floating_window", winnr) +  for i, diag in ipairs(lines) do +    vim.api.nvim_buf_set_lines(bufnr, i - 1, i - 1, 0, { diag.message }) +    vim.api.nvim_buf_add_highlight(bufnr, -1, severity_highlight[diag.severity], i - 1, 0, diag.message:len()) +  end + +  vim.api.nvim_command( +    "autocmd QuitPre <buffer> ++nested ++once lua pcall(vim.api.nvim_win_close, " .. winnr .. ", true)" +  ) +  vim.lsp.util.close_preview_autocmd(close_events, winnr) +end + +return M diff --git a/lua/lvim/lsp/init.lua b/lua/lvim/lsp/init.lua new file mode 100644 index 00000000..c8d583a9 --- /dev/null +++ b/lua/lvim/lsp/init.lua @@ -0,0 +1,165 @@ +local M = {} +local Log = require "lvim.core.log" +local utils = require "lvim.utils" + +local function lsp_highlight_document(client) +  if lvim.lsp.document_highlight == false then +    return -- we don't need further +  end +  -- Set autocommands conditional on server_capabilities +  if client.resolved_capabilities.document_highlight then +    vim.api.nvim_exec( +      [[ +      augroup lsp_document_highlight +        autocmd! * <buffer> +        autocmd CursorHold <buffer> lua vim.lsp.buf.document_highlight() +        autocmd CursorMoved <buffer> lua vim.lsp.buf.clear_references() +      augroup END +    ]], +      false +    ) +  end +end + +local function lsp_code_lens_refresh(client) +  if lvim.lsp.code_lens_refresh == false then +    return +  end + +  if client.resolved_capabilities.code_lens then +    vim.api.nvim_exec( +      [[ +      augroup lsp_code_lens_refresh +        autocmd! * <buffer> +        autocmd InsertLeave <buffer> lua vim.lsp.codelens.refresh() +        autocmd InsertLeave <buffer> lua vim.lsp.codelens.display() +      augroup END +    ]], +      false +    ) +  end +end + +local function add_lsp_buffer_keybindings(bufnr) +  local mappings = { +    normal_mode = "n", +    insert_mode = "i", +    visual_mode = "v", +  } + +  if lvim.builtin.which_key.active then +    -- Remap using which_key +    local status_ok, wk = pcall(require, "which-key") +    if not status_ok then +      return +    end +    for mode_name, mode_char in pairs(mappings) do +      wk.register(lvim.lsp.buffer_mappings[mode_name], { mode = mode_char, buffer = bufnr }) +    end +  else +    -- Remap using nvim api +    for mode_name, mode_char in pairs(mappings) do +      for key, remap in pairs(lvim.lsp.buffer_mappings[mode_name]) do +        vim.api.nvim_buf_set_keymap(bufnr, mode_char, key, remap[1], { noremap = true, silent = true }) +      end +    end +  end +end + +function M.common_capabilities() +  local capabilities = vim.lsp.protocol.make_client_capabilities() +  capabilities.textDocument.completion.completionItem.snippetSupport = true +  capabilities.textDocument.completion.completionItem.resolveSupport = { +    properties = { +      "documentation", +      "detail", +      "additionalTextEdits", +    }, +  } + +  local status_ok, cmp_nvim_lsp = pcall(require, "cmp_nvim_lsp") +  if status_ok then +    capabilities = cmp_nvim_lsp.update_capabilities(capabilities) +  end + +  return capabilities +end + +local function select_default_formater(client) +  local client_formatting = client.resolved_capabilities.document_formatting +    or client.resolved_capabilities.document_range_formatting +  if client.name == "null-ls" or not client_formatting then +    return +  end +  Log:debug("Checking for formatter overriding for " .. client.name) +  local client_filetypes = client.config.filetypes or {} +  for _, filetype in ipairs(client_filetypes) do +    if lvim.lang[filetype] and #vim.tbl_keys(lvim.lang[filetype].formatters) > 0 then +      Log:debug("Formatter overriding detected. Disabling formatting capabilities for " .. client.name) +      client.resolved_capabilities.document_formatting = false +      client.resolved_capabilities.document_range_formatting = false +    end +  end +end + +function M.common_on_init(client, bufnr) +  if lvim.lsp.on_init_callback then +    lvim.lsp.on_init_callback(client, bufnr) +    Log:debug "Called lsp.on_init_callback" +    return +  end +  select_default_formater(client) +end + +function M.common_on_attach(client, bufnr) +  if lvim.lsp.on_attach_callback then +    lvim.lsp.on_attach_callback(client, bufnr) +    Log:debug "Called lsp.on_attach_callback" +  end +  lsp_highlight_document(client) +  lsp_code_lens_refresh(client) +  add_lsp_buffer_keybindings(bufnr) +end + +local function bootstrap_nlsp(opts) +  opts = opts or {} +  local lsp_settings_status_ok, lsp_settings = pcall(require, "nlspsettings") +  if lsp_settings_status_ok then +    lsp_settings.setup(opts) +  end +end + +function M.get_common_opts() +  return { +    on_attach = M.common_on_attach, +    on_init = M.common_on_init, +    capabilities = M.common_capabilities(), +  } +end + +function M.setup() +  Log:debug "Setting up LSP support" + +  local lsp_status_ok, _ = pcall(require, "lspconfig") +  if not lsp_status_ok then +    return +  end + +  for _, sign in ipairs(lvim.lsp.diagnostics.signs.values) do +    vim.fn.sign_define(sign.name, { texthl = sign.name, text = sign.text, numhl = sign.name }) +  end + +  require("lvim.lsp.handlers").setup() + +  if not utils.is_directory(lvim.lsp.templates_dir) then +    require("lvim.lsp.templates").generate_templates() +  end + +  bootstrap_nlsp { config_home = utils.join_paths(get_config_dir(), "lsp-settings") } + +  require("lvim.lsp.null-ls").setup() + +  require("lvim.utils").toggle_autoformat() +end + +return M diff --git a/lua/lvim/lsp/manager.lua b/lua/lvim/lsp/manager.lua new file mode 100644 index 00000000..678a08af --- /dev/null +++ b/lua/lvim/lsp/manager.lua @@ -0,0 +1,86 @@ +local M = {} + +local Log = require "lvim.core.log" +local lsp_utils = require "lvim.lsp.utils" + +function M.init_defaults(languages) +  for _, entry in ipairs(languages) do +    if not lvim.lang[entry] then +      lvim.lang[entry] = { +        formatters = {}, +        linters = {}, +        lsp = {}, +      } +    end +  end +end + +local function is_overridden(server) +  local overrides = lvim.lsp.override +  if type(overrides) == "table" then +    if vim.tbl_contains(overrides, server) then +      return true +    end +  end +end + +---Resolve the configuration for a server based on both common and user configuration +---@param name string +---@param user_config table [optional] +---@return table +local function resolve_config(name, user_config) +  local config = { +    on_attach = require("lvim.lsp").common_on_attach, +    on_init = require("lvim.lsp").common_on_init, +    capabilities = require("lvim.lsp").common_capabilities(), +  } + +  local status_ok, custom_config = pcall(require, "lvim.lsp/providers/" .. name) +  if status_ok then +    Log:debug("Using custom configuration for requested server: " .. name) +    config = vim.tbl_deep_extend("force", config, custom_config) +  end + +  if user_config then +    config = vim.tbl_deep_extend("force", config, user_config) +  end + +  return config +end + +---Setup a language server by providing a name +---@param server_name string name of the language server +---@param user_config table [optional] when available it will take predence over any default configurations +function M.setup(server_name, user_config) +  vim.validate { name = { server_name, "string" } } + +  if lsp_utils.is_client_active(server_name) or is_overridden(server_name) then +    return +  end + +  local config = resolve_config(server_name, user_config) +  local server_available, requested_server = require("nvim-lsp-installer.servers").get_server(server_name) + +  local function ensure_installed(server) +    if server:is_installed() then +      return true +    end +    if not lvim.lsp.automatic_servers_installation then +      Log:debug(server.name .. " is not managed by the automatic installer") +      return false +    end +    Log:debug(string.format("Installing [%s]", server.name)) +    server:install() +    vim.schedule(function() +      vim.cmd [[LspStart]] +    end) +  end + +  if server_available and ensure_installed(requested_server) then +    requested_server:setup(config) +  else +    require("lspconfig")[server_name].setup(config) +  end +end + +return M diff --git a/lua/lvim/lsp/null-ls/formatters.lua b/lua/lvim/lsp/null-ls/formatters.lua new file mode 100644 index 00000000..663e2586 --- /dev/null +++ b/lua/lvim/lsp/null-ls/formatters.lua @@ -0,0 +1,66 @@ +local M = {} + +local null_ls = require "null-ls" +local services = require "lvim.lsp.null-ls.services" +local Log = require "lvim.core.log" + +function M.list_supported_names(filetype) +  local null_ls_methods = require "null-ls.methods" +  local formatter_method = null_ls_methods.internal["FORMATTING"] +  local registered_providers = services.list_registered_providers_names(filetype) +  return registered_providers[formatter_method] or {} +end + +function M.list_available(filetype) +  local formatters = {} +  local tbl = require "lvim.utils.table" +  for _, provider in pairs(null_ls.builtins.formatting) do +    if tbl.contains(provider.filetypes or {}, function(ft) +      return ft == "*" or ft == filetype +    end) then +      table.insert(formatters, provider.name) +    end +  end + +  return formatters +end + +function M.list_configured(formatter_configs) +  local formatters, errors = {}, {} + +  for _, fmt_config in ipairs(formatter_configs) do +    local formatter_name = fmt_config.exe:gsub("-", "_") +    local formatter = null_ls.builtins.formatting[formatter_name] + +    if not formatter then +      Log:error("Not a valid formatter: " .. fmt_config.exe) +      errors[fmt_config.exe] = {} -- Add data here when necessary +    else +      local formatter_cmd = services.find_command(formatter._opts.command) +      if not formatter_cmd then +        Log:warn("Not found: " .. formatter._opts.command) +        errors[fmt_config.exe] = {} -- Add data here when necessary +      else +        Log:debug("Using formatter: " .. formatter_cmd) +        formatters[fmt_config.exe] = formatter.with { +          command = formatter_cmd, +          extra_args = fmt_config.args, +          filetypes = fmt_config.filetypes, +        } +      end +    end +  end + +  return { supported = formatters, unsupported = errors } +end + +function M.setup(formatter_configs) +  if vim.tbl_isempty(formatter_configs) then +    return +  end + +  local formatters_by_ft = M.list_configured(formatter_configs) +  null_ls.register { sources = formatters_by_ft.supported } +end + +return M diff --git a/lua/lvim/lsp/null-ls/init.lua b/lua/lvim/lsp/null-ls/init.lua new file mode 100644 index 00000000..f2d3216d --- /dev/null +++ b/lua/lvim/lsp/null-ls/init.lua @@ -0,0 +1,32 @@ +local M = {} + +local Log = require "lvim.core.log" +local formatters = require "lvim.lsp.null-ls.formatters" +local linters = require "lvim.lsp.null-ls.linters" + +function M:setup() +  local status_ok, null_ls = pcall(require, "null-ls") +  if not status_ok then +    Log:error "Missing null-ls dependency" +    return +  end + +  null_ls.config() +  require("lspconfig")["null-ls"].setup(lvim.lsp.null_ls.setup) +  for filetype, config in pairs(lvim.lang) do +    if not vim.tbl_isempty(config.formatters) then +      vim.tbl_map(function(c) +        c.filetypes = { filetype } +      end, config.formatters) +      formatters.setup(config.formatters) +    end +    if not vim.tbl_isempty(config.linters) then +      vim.tbl_map(function(c) +        c.filetypes = { filetype } +      end, config.formatters) +      linters.setup(config.linters) +    end +  end +end + +return M diff --git a/lua/lvim/lsp/null-ls/linters.lua b/lua/lvim/lsp/null-ls/linters.lua new file mode 100644 index 00000000..9ea2d55b --- /dev/null +++ b/lua/lvim/lsp/null-ls/linters.lua @@ -0,0 +1,66 @@ +local M = {} + +local null_ls = require "null-ls" +local services = require "lvim.lsp.null-ls.services" +local Log = require "lvim.core.log" + +function M.list_supported_names(filetype) +  local null_ls_methods = require "null-ls.methods" +  local linter_method = null_ls_methods.internal["DIAGNOSTICS"] +  local registered_providers = services.list_registered_providers_names(filetype) +  return registered_providers[linter_method] or {} +end + +function M.list_available(filetype) +  local linters = {} +  local tbl = require "lvim.utils.table" +  for _, provider in pairs(null_ls.builtins.diagnostics) do +    if tbl.contains(provider.filetypes or {}, function(ft) +      return ft == "*" or ft == filetype +    end) then +      table.insert(linters, provider.name) +    end +  end + +  return linters +end + +function M.list_configured(linter_configs) +  local linters, errors = {}, {} + +  for _, lnt_config in pairs(linter_configs) do +    local linter_name = lnt_config.exe:gsub("-", "_") +    local linter = null_ls.builtins.diagnostics[linter_name] + +    if not linter then +      Log:error("Not a valid linter: " .. lnt_config.exe) +      errors[lnt_config.exe] = {} -- Add data here when necessary +    else +      local linter_cmd = services.find_command(linter._opts.command) +      if not linter_cmd then +        Log:warn("Not found: " .. linter._opts.command) +        errors[lnt_config.exe] = {} -- Add data here when necessary +      else +        Log:debug("Using linter: " .. linter_cmd) +        linters[lnt_config.exe] = linter.with { +          command = linter_cmd, +          extra_args = lnt_config.args, +          filetypes = lnt_config.filetypes, +        } +      end +    end +  end + +  return { supported = linters, unsupported = errors } +end + +function M.setup(linter_configs) +  if vim.tbl_isempty(linter_configs) then +    return +  end + +  local linters = M.list_configured(linter_configs) +  null_ls.register { sources = linters.supported } +end + +return M diff --git a/lua/lvim/lsp/null-ls/services.lua b/lua/lvim/lsp/null-ls/services.lua new file mode 100644 index 00000000..9cb29f49 --- /dev/null +++ b/lua/lvim/lsp/null-ls/services.lua @@ -0,0 +1,63 @@ +local M = {} + +local function find_root_dir() +  local util = require "lspconfig/util" +  local lsp_utils = require "lvim.lsp.utils" + +  local ts_client = lsp_utils.is_client_active "typescript" +  if ts_client then +    return ts_client.config.root_dir +  end +  local dirname = vim.fn.expand "%:p:h" +  return util.root_pattern "package.json"(dirname) +end + +local function from_node_modules(command) +  local root_dir = find_root_dir() + +  if not root_dir then +    return nil +  end + +  return root_dir .. "/node_modules/.bin/" .. command +end + +local local_providers = { +  prettier = { find = from_node_modules }, +  prettierd = { find = from_node_modules }, +  prettier_d_slim = { find = from_node_modules }, +  eslint_d = { find = from_node_modules }, +  eslint = { find = from_node_modules }, +  stylelint = { find = from_node_modules }, +} + +function M.find_command(command) +  if local_providers[command] then +    local local_command = local_providers[command].find(command) +    if local_command and vim.fn.executable(local_command) == 1 then +      return local_command +    end +  end + +  if vim.fn.executable(command) == 1 then +    return command +  end +  return nil +end + +function M.list_registered_providers_names(filetype) +  local u = require "null-ls.utils" +  local c = require "null-ls.config" +  local registered = {} +  for method, source in pairs(c.get()._methods) do +    for name, filetypes in pairs(source) do +      if u.filetype_matches(filetypes, filetype) then +        registered[method] = registered[method] or {} +        table.insert(registered[method], name) +      end +    end +  end +  return registered +end + +return M diff --git a/lua/lvim/lsp/peek.lua b/lua/lvim/lsp/peek.lua new file mode 100644 index 00000000..08345aff --- /dev/null +++ b/lua/lvim/lsp/peek.lua @@ -0,0 +1,152 @@ +local M = { +  floating_buf = nil, +  floating_win = nil, +  prev_result = nil, +} + +local function create_floating_file(location, opts) +  vim.validate { +    location = { location, "t" }, +    opts = { opts, "t", true }, +  } + +  -- Set some defaults +  opts = opts or {} +  local close_events = opts.close_events or { "CursorMoved", "CursorMovedI", "BufHidden", "InsertCharPre" } + +  -- location may be LocationLink or Location +  local uri = location.targetUri or location.uri +  if uri == nil then +    return +  end +  local bufnr = vim.uri_to_bufnr(uri) +  if not vim.api.nvim_buf_is_loaded(bufnr) then +    vim.fn.bufload(bufnr) +  end + +  local range = location.targetRange or location.range + +  local contents = vim.api.nvim_buf_get_lines( +    bufnr, +    range.start.line, +    math.min(range["end"].line + 1 + (opts.context or 10), range.start.line + (opts.max_height or 15)), -- Don't let the window be more that 15 lines long(height) +    false +  ) +  local width, height = vim.lsp.util._make_floating_popup_size(contents, opts) +  opts = vim.lsp.util.make_floating_popup_options(width, height, opts) +  -- Don't make it minimal as it is meant to be fully featured +  opts["style"] = nil + +  vim.api.nvim_buf_set_option(bufnr, "bufhidden", "wipe") + +  local winnr = vim.api.nvim_open_win(bufnr, false, opts) +  vim.api.nvim_win_set_option(winnr, "winblend", 0) + +  vim.api.nvim_win_set_cursor(winnr, { range.start.line + 1, range.start.character }) +  vim.api.nvim_buf_set_var(bufnr, "lsp_floating_window", winnr) + +  -- Set some autocmds to close the window +  vim.api.nvim_command( +    "autocmd QuitPre <buffer> ++nested ++once lua pcall(vim.api.nvim_win_close, " .. winnr .. ", true)" +  ) +  vim.lsp.util.close_preview_autocmd(close_events, winnr) + +  return bufnr, winnr +end + +local function preview_location_callback(result) +  if result == nil or vim.tbl_isempty(result) then +    return nil +  end + +  local opts = { +    border = "rounded", +    context = 10, +  } + +  if vim.tbl_islist(result) then +    M.prev_result = result[1] +    M.floating_buf, M.floating_win = create_floating_file(result[1], opts) +  else +    M.prev_result = result +    M.floating_buf, M.floating_win = create_floating_file(result, opts) +  end +end + +local function preview_location_callback_old_signature(_, _, result) +  return preview_location_callback(result) +end + +local function preview_location_callback_new_signature(_, result) +  return preview_location_callback(result) +end + +function M.open_file() +  -- Get the file currently open in the floating window +  local filepath = vim.fn.expand "%:." + +  if not filepath then +    print "peek: Unable to open the file!" +    return +  end + +  -- Close the floating window +  pcall(vim.api.nvim_win_close, M.floating_win, true) + +  -- Edit the file +  vim.cmd("edit " .. filepath) + +  local winnr = vim.api.nvim_get_current_win() + +  -- Set the cursor at the right position +  M.set_cursor_to_prev_pos(winnr) +end + +function M.set_cursor_to_prev_pos(winnr) +  -- Get position of the thing to peek at +  local location = M.prev_result +  local range = location.targetRange or location.range +  local cursor_pos = { range.start.line + 1, range.start.character } + +  -- Set the winnr to the floating window if none was passed in +  winnr = winnr or M.floating_win +  -- Set the cursor at the correct position in the floating window +  vim.api.nvim_win_set_cursor(winnr, cursor_pos) +end + +function M.Peek(what) +  -- If a window already exists, focus it at the right position! +  if vim.tbl_contains(vim.api.nvim_list_wins(), M.floating_win) then +    local success_1, _ = pcall(vim.api.nvim_set_current_win, M.floating_win) +    if not success_1 then +      print "peek: You cannot edit the current file in a preview!" +      return +    end + +    -- Set the cursor at the correct position in the floating window +    M.set_cursor_to_prev_pos() + +    vim.api.nvim_buf_set_keymap( +      M.floating_buf, +      "n", +      "<CR>", +      ":lua require('lvim.lsp.peek').open_file()<CR>", +      { noremap = true, silent = true } +    ) +  else +    -- Make a new request and then create the new window in the callback +    local params = vim.lsp.util.make_position_params() +    local preview_callback = preview_location_callback_old_signature +    if vim.fn.has "nvim-0.5.1" > 0 then +      preview_callback = preview_location_callback_new_signature +    end +    local success, _ = pcall(vim.lsp.buf_request, 0, "textDocument/" .. what, params, preview_callback) +    if not success then +      print( +        'peek: Error calling LSP method "textDocument/' .. what .. '". The current language lsp might not support it.' +      ) +    end +  end +end + +return M diff --git a/lua/lvim/lsp/providers/jsonls.lua b/lua/lvim/lsp/providers/jsonls.lua new file mode 100644 index 00000000..1fffa686 --- /dev/null +++ b/lua/lvim/lsp/providers/jsonls.lua @@ -0,0 +1,197 @@ +local default_schemas = nil +local status_ok, jsonls_settings = pcall(require, "nlspsettings.jsonls") +if status_ok then +  default_schemas = jsonls_settings.get_default_schemas() +end + +local schemas = { +  { +    description = "TypeScript compiler configuration file", +    fileMatch = { +      "tsconfig.json", +      "tsconfig.*.json", +    }, +    url = "https://json.schemastore.org/tsconfig.json", +  }, +  { +    description = "Lerna config", +    fileMatch = { "lerna.json" }, +    url = "https://json.schemastore.org/lerna.json", +  }, +  { +    description = "Babel configuration", +    fileMatch = { +      ".babelrc.json", +      ".babelrc", +      "babel.config.json", +    }, +    url = "https://json.schemastore.org/babelrc.json", +  }, +  { +    description = "ESLint config", +    fileMatch = { +      ".eslintrc.json", +      ".eslintrc", +    }, +    url = "https://json.schemastore.org/eslintrc.json", +  }, +  { +    description = "Bucklescript config", +    fileMatch = { "bsconfig.json" }, +    url = "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/8.2.0/docs/docson/build-schema.json", +  }, +  { +    description = "Prettier config", +    fileMatch = { +      ".prettierrc", +      ".prettierrc.json", +      "prettier.config.json", +    }, +    url = "https://json.schemastore.org/prettierrc", +  }, +  { +    description = "Vercel Now config", +    fileMatch = { "now.json" }, +    url = "https://json.schemastore.org/now", +  }, +  { +    description = "Stylelint config", +    fileMatch = { +      ".stylelintrc", +      ".stylelintrc.json", +      "stylelint.config.json", +    }, +    url = "https://json.schemastore.org/stylelintrc", +  }, +  { +    description = "A JSON schema for the ASP.NET LaunchSettings.json files", +    fileMatch = { "launchsettings.json" }, +    url = "https://json.schemastore.org/launchsettings.json", +  }, +  { +    description = "Schema for CMake Presets", +    fileMatch = { +      "CMakePresets.json", +      "CMakeUserPresets.json", +    }, +    url = "https://raw.githubusercontent.com/Kitware/CMake/master/Help/manual/presets/schema.json", +  }, +  { +    description = "Configuration file as an alternative for configuring your repository in the settings page.", +    fileMatch = { +      ".codeclimate.json", +    }, +    url = "https://json.schemastore.org/codeclimate.json", +  }, +  { +    description = "LLVM compilation database", +    fileMatch = { +      "compile_commands.json", +    }, +    url = "https://json.schemastore.org/compile-commands.json", +  }, +  { +    description = "Config file for Command Task Runner", +    fileMatch = { +      "commands.json", +    }, +    url = "https://json.schemastore.org/commands.json", +  }, +  { +    description = "AWS CloudFormation provides a common language for you to describe and provision all the infrastructure resources in your cloud environment.", +    fileMatch = { +      "*.cf.json", +      "cloudformation.json", +    }, +    url = "https://raw.githubusercontent.com/awslabs/goformation/v5.2.9/schema/cloudformation.schema.json", +  }, +  { +    description = "The AWS Serverless Application Model (AWS SAM, previously known as Project Flourish) extends AWS CloudFormation to provide a simplified way of defining the Amazon API Gateway APIs, AWS Lambda functions, and Amazon DynamoDB tables needed by your serverless application.", +    fileMatch = { +      "serverless.template", +      "*.sam.json", +      "sam.json", +    }, +    url = "https://raw.githubusercontent.com/awslabs/goformation/v5.2.9/schema/sam.schema.json", +  }, +  { +    description = "Json schema for properties json file for a GitHub Workflow template", +    fileMatch = { +      ".github/workflow-templates/**.properties.json", +    }, +    url = "https://json.schemastore.org/github-workflow-template-properties.json", +  }, +  { +    description = "golangci-lint configuration file", +    fileMatch = { +      ".golangci.toml", +      ".golangci.json", +    }, +    url = "https://json.schemastore.org/golangci-lint.json", +  }, +  { +    description = "JSON schema for the JSON Feed format", +    fileMatch = { +      "feed.json", +    }, +    url = "https://json.schemastore.org/feed.json", +    versions = { +      ["1"] = "https://json.schemastore.org/feed-1.json", +      ["1.1"] = "https://json.schemastore.org/feed.json", +    }, +  }, +  { +    description = "Packer template JSON configuration", +    fileMatch = { +      "packer.json", +    }, +    url = "https://json.schemastore.org/packer.json", +  }, +  { +    description = "NPM configuration file", +    fileMatch = { +      "package.json", +    }, +    url = "https://json.schemastore.org/package.json", +  }, +  { +    description = "JSON schema for Visual Studio component configuration files", +    fileMatch = { +      "*.vsconfig", +    }, +    url = "https://json.schemastore.org/vsconfig.json", +  }, +  { +    description = "Resume json", +    fileMatch = { "resume.json" }, +    url = "https://raw.githubusercontent.com/jsonresume/resume-schema/v1.0.0/schema.json", +  }, +} + +local function extend(tab1, tab2) +  for _, value in ipairs(tab2) do +    table.insert(tab1, value) +  end +  return tab1 +end + +local extended_schemas = extend(schemas, default_schemas) + +local opts = { +  settings = { +    json = { +      schemas = extended_schemas, +    }, +  }, +  setup = { +    commands = { +      Format = { +        function() +          vim.lsp.buf.range_formatting({}, { 0, 0 }, { vim.fn.line "$", 0 }) +        end, +      }, +    }, +  }, +} + +return opts diff --git a/lua/lvim/lsp/providers/sumneko_lua.lua b/lua/lvim/lsp/providers/sumneko_lua.lua new file mode 100644 index 00000000..6585c8c7 --- /dev/null +++ b/lua/lvim/lsp/providers/sumneko_lua.lua @@ -0,0 +1,19 @@ +local opts = { +  settings = { +    Lua = { +      diagnostics = { +        globals = { "vim", "lvim" }, +      }, +      workspace = { +        library = { +          [require("lvim.utils").join_paths(get_runtime_dir(), "lvim", "lua")] = true, +          [vim.fn.expand "$VIMRUNTIME/lua"] = true, +          [vim.fn.expand "$VIMRUNTIME/lua/vim/lsp"] = true, +        }, +        maxPreload = 100000, +        preloadFileSize = 10000, +      }, +    }, +  }, +} +return opts diff --git a/lua/lvim/lsp/providers/vuels.lua b/lua/lvim/lsp/providers/vuels.lua new file mode 100644 index 00000000..326363fd --- /dev/null +++ b/lua/lvim/lsp/providers/vuels.lua @@ -0,0 +1,26 @@ +local opts = { +  setup = { +    root_dir = function(fname) +      local util = require "lvim.lspconfig/util" +      return util.root_pattern "package.json"(fname) or util.root_pattern "vue.config.js"(fname) or vim.fn.getcwd() +    end, +    init_options = { +      config = { +        vetur = { +          completion = { +            autoImport = true, +            tagCasing = "kebab", +            useScaffoldSnippets = true, +          }, +          useWorkspaceDependencies = true, +          validation = { +            script = true, +            style = true, +            template = true, +          }, +        }, +      }, +    }, +  }, +} +return opts diff --git a/lua/lvim/lsp/providers/yamlls.lua b/lua/lvim/lsp/providers/yamlls.lua new file mode 100644 index 00000000..156a35b0 --- /dev/null +++ b/lua/lvim/lsp/providers/yamlls.lua @@ -0,0 +1,30 @@ +local opts = { +  settings = { +    yaml = { +      hover = true, +      completion = true, +      validate = true, +      schemaStore = { +        enable = true, +        url = "https://www.schemastore.org/api/json/catalog.json", +      }, +      schemas = { +        kubernetes = { +          "daemon.{yml,yaml}", +          "manager.{yml,yaml}", +          "restapi.{yml,yaml}", +          "role.{yml,yaml}", +          "role_binding.{yml,yaml}", +          "*onfigma*.{yml,yaml}", +          "*ngres*.{yml,yaml}", +          "*ecre*.{yml,yaml}", +          "*eployment*.{yml,yaml}", +          "*ervic*.{yml,yaml}", +          "kubectl-edit*.yaml", +        }, +      }, +    }, +  }, +} + +return opts diff --git a/lua/lvim/lsp/templates.lua b/lua/lvim/lsp/templates.lua new file mode 100644 index 00000000..e0741b1b --- /dev/null +++ b/lua/lvim/lsp/templates.lua @@ -0,0 +1,98 @@ +local M = {} + +local Log = require "lvim.core.log" +local utils = require "lvim.utils" +local get_supported_filetypes = require("lvim.lsp.utils").get_supported_filetypes + +local ftplugin_dir = lvim.lsp.templates_dir + +local join_paths = _G.join_paths + +function M.remove_template_files() +  -- remove any outdated files +  for _, file in ipairs(vim.fn.glob(ftplugin_dir .. "/*.lua", 1, 1)) do +    vim.fn.delete(file) +  end +end + +---Checks if a server is ignored by default because of a conflict +---Only TSServer is enabled by default for the javascript-family +---@param server_name string +function M.is_ignored(server_name, filetypes) +  --TODO: this is easy to be made configurable once stable +  filetypes = filetypes or get_supported_filetypes(server_name) + +  if vim.tbl_contains(filetypes, "javascript") then +    if server_name == "tsserver" then +      return false +    else +      return true +    end +  end + +  local blacklist = { +    "jedi_language_server", +    "pylsp", +    "sqlls", +    "sqls", +    "angularls", +    "ansiblels", +  } +  return vim.tbl_contains(blacklist, server_name) +end + +---Generates an ftplugin file based on the server_name in the selected directory +---@param server_name string name of a valid language server, e.g. pyright, gopls, tsserver, etc. +---@param dir string the full path to the desired directory +function M.generate_ftplugin(server_name, dir) +  -- we need to go through lspconfig to get the corresponding filetypes currently +  local filetypes = get_supported_filetypes(server_name) or {} +  if not filetypes then +    return +  end + +  if M.is_ignored(server_name, filetypes) then +    return +  end + +  -- print("got associated filetypes: " .. vim.inspect(filetypes)) + +  for _, filetype in ipairs(filetypes) do +    local filename = join_paths(dir, filetype .. ".lua") +    local setup_cmd = string.format([[require("lvim.lsp.manager").setup(%q)]], server_name) +    -- print("using setup_cmd: " .. setup_cmd) +    -- overwrite the file completely +    utils.write_file(filename, setup_cmd .. "\n", "a") +  end +end + +---Generates ftplugin files based on a list of server_names +---The files are generated to a runtimepath: "$LUNARVIM_RUNTIME_DIR/site/after/ftplugin/template.lua" +---@param servers_names table list of servers to be enabled. Will add all by default +function M.generate_templates(servers_names) +  servers_names = servers_names or {} + +  Log:debug "Templates installation in progress" + +  M.remove_template_files() + +  if vim.tbl_isempty(servers_names) then +    local available_servers = require("nvim-lsp-installer.servers").get_available_servers() + +    for _, server in pairs(available_servers) do +      table.insert(servers_names, server.name) +    end +  end + +  -- create the directory if it didn't exist +  if not utils.is_directory(lvim.lsp.templates_dir) then +    vim.fn.mkdir(ftplugin_dir, "p") +  end + +  for _, server in ipairs(servers_names) do +    M.generate_ftplugin(server, ftplugin_dir) +  end +  Log:debug "Templates installation is complete" +end + +return M diff --git a/lua/lvim/lsp/utils.lua b/lua/lvim/lsp/utils.lua new file mode 100644 index 00000000..a34fbf44 --- /dev/null +++ b/lua/lvim/lsp/utils.lua @@ -0,0 +1,62 @@ +local M = {} + +local tbl = require "lvim.utils.table" + +function M.is_client_active(name) +  local clients = vim.lsp.get_active_clients() +  return tbl.find_first(clients, function(client) +    return client.name == name +  end) +end + +function M.get_active_clients_by_ft(filetype) +  local matches = {} +  local clients = vim.lsp.get_active_clients() +  for _, client in pairs(clients) do +    local supported_filetypes = client.config.filetypes or {} +    if client.name ~= "null-ls" and vim.tbl_contains(supported_filetypes, filetype) then +      table.insert(matches, client) +    end +  end +  return matches +end + +function M.get_client_capabilities(client_id) +  if not client_id then +    local buf_clients = vim.lsp.buf_get_clients() +    for _, buf_client in ipairs(buf_clients) do +      if buf_client.name ~= "null-ls" then +        client_id = buf_client.id +        break +      end +    end +  end +  if not client_id then +    error "Unable to determine client_id" +    return +  end + +  local client = vim.lsp.get_client_by_id(tonumber(client_id)) + +  local enabled_caps = {} +  for capability, status in pairs(client.resolved_capabilities) do +    if status == true then +      table.insert(enabled_caps, capability) +    end +  end + +  return enabled_caps +end + +function M.get_supported_filetypes(server_name) +  -- print("got filetypes query request for: " .. server_name) +  local configs = require "lspconfig/configs" +  pcall(require, ("lspconfig/" .. server_name)) +  for _, config in pairs(configs) do +    if config.name == server_name then +      return config.document_config.default_config.filetypes or {} +    end +  end +end + +return M diff --git a/lua/lvim/lualine/themes/onedarker.lua b/lua/lvim/lualine/themes/onedarker.lua new file mode 100644 index 00000000..396657bb --- /dev/null +++ b/lua/lvim/lualine/themes/onedarker.lua @@ -0,0 +1,35 @@ +-- Copyright (c) 2020-2021 shadmansaleh +-- MIT license, see LICENSE for more details. +-- Credit: Zoltan Dalmadi(lightline) +-- LuaFormatter off +local colors = { +  blue = "#61afef", +  green = "#98c379", +  purple = "#c678dd", +  red1 = "#e06c75", +  red2 = "#be5046", +  yellow = "#e5c07b", +  orange = "#D19A66", +  fg = "#abb2bf", +  bg = "#282c34", +  gray1 = "#5c6370", +  gray2 = "#2c323d", +  gray3 = "#3e4452", +} +-- LuaFormatter on +return { +  normal = { +    a = { fg = colors.fg, bg = colors.blue, gui = "bold" }, +    b = { fg = colors.fg, bg = colors.bg }, +    c = { fg = colors.fg, bg = colors.bg }, +  }, +  insert = { a = { fg = colors.fg, bg = colors.green, gui = "bold" } }, +  visual = { a = { fg = colors.fg, bg = colors.purple, gui = "bold" } }, +  command = { a = { fg = colors.fg, bg = colors.yellow, gui = "bold" } }, +  replace = { a = { fg = colors.fg, bg = colors.red1, gui = "bold" } }, +  inactive = { +    a = { fg = colors.gray1, bg = colors.bg, gui = "bold" }, +    b = { fg = colors.gray1, bg = colors.bg }, +    c = { fg = colors.gray1, bg = colors.bg }, +  }, +} diff --git a/lua/lvim/plugin-loader.lua b/lua/lvim/plugin-loader.lua new file mode 100644 index 00000000..feef7ea7 --- /dev/null +++ b/lua/lvim/plugin-loader.lua @@ -0,0 +1,63 @@ +local plugin_loader = {} + +local utils = require "lvim.utils" +local Log = require "lvim.core.log" +-- we need to reuse this outside of init() +local compile_path = get_config_dir() .. "/plugin/packer_compiled.lua" + +function plugin_loader:init(opts) +  opts = opts or {} + +  local install_path = opts.install_path or vim.fn.stdpath "data" .. "/site/pack/packer/start/packer.nvim" +  local package_root = opts.package_root or vim.fn.stdpath "data" .. "/site/pack" + +  if vim.fn.empty(vim.fn.glob(install_path)) > 0 then +    vim.fn.system { "git", "clone", "--depth", "1", "https://github.com/wbthomason/packer.nvim", install_path } +    vim.cmd "packadd packer.nvim" +  end + +  local packer_ok, packer = pcall(require, "packer") +  if not packer_ok then +    return +  end + +  packer.init { +    package_root = package_root, +    compile_path = compile_path, +    git = { clone_timeout = 300 }, +    display = { +      open_fn = function() +        return require("packer.util").float { border = "rounded" } +      end, +    }, +  } + +  self.packer = packer +  return self +end + +function plugin_loader:cache_clear() +  if vim.fn.delete(compile_path) == 0 then +    Log:debug "deleted packer_compiled.lua" +  end +end + +function plugin_loader:cache_reset() +  self.cache_clear() +  require("packer").compile() +  if utils.is_file(compile_path) then +    Log:debug "generated packer_compiled.lua" +  end +end + +function plugin_loader:load(configurations) +  return self.packer.startup(function(use) +    for _, plugins in ipairs(configurations) do +      for _, plugin in ipairs(plugins) do +        use(plugin) +      end +    end +  end) +end + +return plugin_loader diff --git a/lua/lvim/plugins.lua b/lua/lvim/plugins.lua new file mode 100644 index 00000000..fcb23328 --- /dev/null +++ b/lua/lvim/plugins.lua @@ -0,0 +1,180 @@ +return { +  -- Packer can manage itself as an optional plugin +  { "wbthomason/packer.nvim" }, +  { "neovim/nvim-lspconfig" }, +  { "tamago324/nlsp-settings.nvim" }, +  { "jose-elias-alvarez/null-ls.nvim" }, +  { "antoinemadec/FixCursorHold.nvim" }, -- Needed while issue https://github.com/neovim/neovim/issues/12587 is still open +  { +    "williamboman/nvim-lsp-installer", +  }, + +  { "nvim-lua/popup.nvim" }, +  { "nvim-lua/plenary.nvim" }, +  -- Telescope +  { +    "nvim-telescope/telescope.nvim", +    config = function() +      require("lvim.core.telescope").setup() +    end, +    disable = not lvim.builtin.telescope.active, +  }, +  -- Install nvim-cmp, and buffer source as a dependency +  { +    "hrsh7th/nvim-cmp", +    config = function() +      require("lvim.core.cmp").setup() +    end, +    requires = { +      "L3MON4D3/LuaSnip", +      "saadparwaiz1/cmp_luasnip", +      "hrsh7th/cmp-buffer", +      "hrsh7th/cmp-nvim-lsp", +      "hrsh7th/cmp-path", +      "hrsh7th/cmp-nvim-lua", +    }, +    run = function() +      -- cmp's config requires cmp to be installed to run the first time +      if not lvim.builtin.cmp then +        require("lvim.core.cmp").config() +      end +    end, +  }, +  { +    "rafamadriz/friendly-snippets", +    -- event = "InsertCharPre", +    -- disable = not lvim.builtin.compe.active, +  }, + +  -- Autopairs +  { +    "windwp/nvim-autopairs", +    -- event = "InsertEnter", +    config = function() +      require("lvim.core.autopairs").setup() +    end, +    disable = not lvim.builtin.autopairs.active, +  }, + +  -- Treesitter +  { +    "nvim-treesitter/nvim-treesitter", +    branch = "0.5-compat", +    -- run = ":TSUpdate", +    config = function() +      require("lvim.core.treesitter").setup() +    end, +  }, + +  -- NvimTree +  { +    "kyazdani42/nvim-tree.lua", +    -- event = "BufWinOpen", +    -- cmd = "NvimTreeToggle", +    -- commit = "fd7f60e242205ea9efc9649101c81a07d5f458bb", +    config = function() +      require("lvim.core.nvimtree").setup() +    end, +    disable = not lvim.builtin.nvimtree.active, +  }, + +  { +    "lewis6991/gitsigns.nvim", + +    config = function() +      require("lvim.core.gitsigns").setup() +    end, +    event = "BufRead", +    disable = not lvim.builtin.gitsigns.active, +  }, + +  -- Whichkey +  { +    "folke/which-key.nvim", +    config = function() +      require("lvim.core.which-key").setup() +    end, +    event = "BufWinEnter", +    disable = not lvim.builtin.which_key.active, +  }, + +  -- Comments +  { +    "terrortylor/nvim-comment", +    event = "BufRead", +    config = function() +      require("lvim.core.comment").setup() +    end, +    disable = not lvim.builtin.comment.active, +  }, + +  -- project.nvim +  { +    "ahmedkhalf/project.nvim", +    config = function() +      require("lvim.core.project").setup() +    end, +    disable = not lvim.builtin.project.active, +  }, + +  -- Icons +  { "kyazdani42/nvim-web-devicons" }, + +  -- Status Line and Bufferline +  { +    -- "hoob3rt/lualine.nvim", +    "shadmansaleh/lualine.nvim", +    -- "Lunarvim/lualine.nvim", +    config = function() +      require("lvim.core.lualine").setup() +    end, +    disable = not lvim.builtin.lualine.active, +  }, + +  { +    "romgrk/barbar.nvim", +    config = function() +      require("lvim.core.bufferline").setup() +    end, +    event = "BufWinEnter", +    disable = not lvim.builtin.bufferline.active, +  }, + +  -- Debugging +  { +    "mfussenegger/nvim-dap", +    -- event = "BufWinEnter", +    config = function() +      require("lvim.core.dap").setup() +    end, +    disable = not lvim.builtin.dap.active, +  }, + +  -- Debugger management +  { +    "Pocco81/DAPInstall.nvim", +    -- event = "BufWinEnter", +    -- event = "BufRead", +    disable = not lvim.builtin.dap.active, +  }, + +  -- Dashboard +  { +    "ChristianChiarulli/dashboard-nvim", +    event = "BufWinEnter", +    config = function() +      require("lvim.core.dashboard").setup() +    end, +    disable = not lvim.builtin.dashboard.active, +  }, + +  -- Terminal +  { +    "akinsho/toggleterm.nvim", +    event = "BufWinEnter", +    config = function() +      require("lvim.core.terminal").setup() +    end, +    disable = not lvim.builtin.terminal.active, +  }, +} diff --git a/lua/lvim/utils/ft.lua b/lua/lvim/utils/ft.lua new file mode 100644 index 00000000..e9852e6f --- /dev/null +++ b/lua/lvim/utils/ft.lua @@ -0,0 +1,47 @@ +-- Here be dragons +-- Opening files with telescope will not start LSP without this +local ft = {} + +ft.find_lua_ftplugins = function(filetype) +  local patterns = { +    string.format("ftplugin/%s.lua", filetype), + +    -- Looks like we don't need this, because the first one works +    -- string.format("after/ftplugin/%s.lua", filetype), +  } + +  local result = {} +  for _, pat in ipairs(patterns) do +    vim.list_extend(result, vim.api.nvim_get_runtime_file(pat, true)) +  end + +  return result +end + +ft.do_filetype = function(filetype) +  local ftplugins = ft.find_lua_ftplugins(filetype) + +  local f_env = setmetatable({ +    -- Override print, so the prints still go through, otherwise it's confusing for people +    print = vim.schedule_wrap(print), +  }, { +    -- Buf default back read/write to whatever is going on in the global landscape +    __index = _G, +    __newindex = _G, +  }) + +  for _, file in ipairs(ftplugins) do +    local f = loadfile(file) +    if not f then +      vim.api.nvim_err_writeln("Unable to load file: " .. file) +    else +      local ok, msg = pcall(setfenv(f, f_env)) + +      if not ok then +        vim.api.nvim_err_writeln("Error while processing file: " .. file .. "\n" .. msg) +      end +    end +  end +end + +return ft diff --git a/lua/lvim/utils/hooks.lua b/lua/lvim/utils/hooks.lua new file mode 100644 index 00000000..d536bc76 --- /dev/null +++ b/lua/lvim/utils/hooks.lua @@ -0,0 +1,35 @@ +local M = {} + +local Log = require "lvim.core.log" +local in_headless = #vim.api.nvim_list_uis() == 0 + +function M.run_pre_update() +  Log:debug "Starting pre-update hook" +  _G.__luacache.clear_cache() +end + +---Reset any startup cache files used by Packer and Impatient +---It also forces regenerating any template ftplugin files +---Tip: Useful for clearing any outdated settings +function M.reset_cache() +  _G.__luacache.clear_cache() +  require("lvim.plugin-loader"):cache_reset() +  package.loaded["lvim.lsp.templates"] = nil +  require("lvim.lsp.templates").generate_templates() +end + +function M.run_post_update() +  Log:debug "Starting post-update hook" +  M.reset_cache() + +  if not in_headless then +    vim.schedule(function() +      require("packer").install() +      -- TODO: add a changelog +      vim.notify("Update complete", vim.log.levels.INFO) +      vim.cmd "LspStart" +    end) +  end +end + +return M diff --git a/lua/lvim/utils/init.lua b/lua/lvim/utils/init.lua new file mode 100644 index 00000000..cebbe75c --- /dev/null +++ b/lua/lvim/utils/init.lua @@ -0,0 +1,205 @@ +local utils = {} +local Log = require "lvim.core.log" +local uv = vim.loop + +-- recursive Print (structure, limit, separator) +local function r_inspect_settings(structure, limit, separator) +  limit = limit or 100 -- default item limit +  separator = separator or "." -- indent string +  if limit < 1 then +    print "ERROR: Item limit reached." +    return limit - 1 +  end +  if structure == nil then +    io.write("-- O", separator:sub(2), " = nil\n") +    return limit - 1 +  end +  local ts = type(structure) + +  if ts == "table" then +    for k, v in pairs(structure) do +      -- replace non alpha keys with ["key"] +      if tostring(k):match "[^%a_]" then +        k = '["' .. tostring(k) .. '"]' +      end +      limit = r_inspect_settings(v, limit, separator .. "." .. tostring(k)) +      if limit < 0 then +        break +      end +    end +    return limit +  end + +  if ts == "string" then +    -- escape sequences +    structure = string.format("%q", structure) +  end +  separator = separator:gsub("%.%[", "%[") +  if type(structure) == "function" then +    -- don't print functions +    io.write("-- lvim", separator:sub(2), " = function ()\n") +  else +    io.write("lvim", separator:sub(2), " = ", tostring(structure), "\n") +  end +  return limit - 1 +end + +function utils.generate_settings() +  -- Opens a file in append mode +  local file = io.open("lv-settings.lua", "w") + +  -- sets the default output file as test.lua +  io.output(file) + +  -- write all `lvim` related settings to `lv-settings.lua` file +  r_inspect_settings(lvim, 10000, ".") + +  -- closes the open file +  io.close(file) +end + +-- autoformat +function utils.toggle_autoformat() +  if lvim.format_on_save then +    require("lvim.core.autocmds").define_augroups { +      autoformat = { +        { +          "BufWritePre", +          "*", +          ":silent lua vim.lsp.buf.formatting_sync()", +        }, +      }, +    } +    Log:debug "Format on save active" +  end + +  if not lvim.format_on_save then +    vim.cmd [[ +      if exists('#autoformat#BufWritePre') +        :autocmd! autoformat +      endif +    ]] +    Log:debug "Format on save off" +  end +end + +function utils.unrequire(m) +  package.loaded[m] = nil +  _G[m] = nil +end + +function utils.gsub_args(args) +  if args == nil or type(args) ~= "table" then +    return args +  end +  local buffer_filepath = vim.fn.fnameescape(vim.api.nvim_buf_get_name(0)) +  for i = 1, #args do +    args[i] = string.gsub(args[i], "${FILEPATH}", buffer_filepath) +  end +  return args +end + +--- Returns a table with the default values that are missing. +--- either paramter can be empty. +--@param config (table) table containing entries that take priority over defaults +--@param default_config (table) table contatining default values if found +function utils.apply_defaults(config, default_config) +  config = config or {} +  default_config = default_config or {} +  local new_config = vim.tbl_deep_extend("keep", vim.empty_dict(), config) +  new_config = vim.tbl_deep_extend("keep", new_config, default_config) +  return new_config +end + +--- Checks whether a given path exists and is a file. +--@param path (string) path to check +--@returns (bool) +function utils.is_file(path) +  local stat = uv.fs_stat(path) +  return stat and stat.type == "file" or false +end + +--- Checks whether a given path exists and is a directory +--@param path (string) path to check +--@returns (bool) +function utils.is_directory(path) +  local stat = uv.fs_stat(path) +  return stat and stat.type == "directory" or false +end + +utils.join_paths = _G.join_paths + +function utils.write_file(path, txt, flag) +  uv.fs_open(path, flag, 438, function(open_err, fd) +    assert(not open_err, open_err) +    uv.fs_write(fd, txt, -1, function(write_err) +      assert(not write_err, write_err) +      uv.fs_close(fd, function(close_err) +        assert(not close_err, close_err) +      end) +    end) +  end) +end + +function utils.debounce(ms, fn) +  local timer = vim.loop.new_timer() +  return function(...) +    local argv = { ... } +    timer:start(ms, 0, function() +      timer:stop() +      vim.schedule_wrap(fn)(unpack(argv)) +    end) +  end +end + +function utils.search_file(file, args) +  local Job = require "plenary.job" +  local stderr = {} +  local stdout, ret = Job +    :new({ +      command = "grep", +      args = { args, file }, +      cwd = get_cache_dir(), +      on_stderr = function(_, data) +        table.insert(stderr, data) +      end, +    }) +    :sync() +  return stdout, ret, stderr +end + +function utils.file_contains(file, query) +  local stdout, ret, stderr = utils.search_file(file, query) +  if ret == 0 then +    return true +  end +  if not vim.tbl_isempty(stderr) then +    error(vim.inspect(stderr)) +  end +  if not vim.tbl_isempty(stdout) then +    error(vim.inspect(stdout)) +  end +  return false +end + +function utils.log_contains(query) +  local logfile = require("lvim.core.log"):get_path() +  local stdout, ret, stderr = utils.search_file(logfile, query) +  if ret == 0 then +    return true +  end +  if not vim.tbl_isempty(stderr) then +    error(vim.inspect(stderr)) +  end +  if not vim.tbl_isempty(stdout) then +    error(vim.inspect(stdout)) +  end +  if not vim.tbl_isempty(stderr) then +    error(vim.inspect(stderr)) +  end +  return false +end + +return utils + +-- TODO: find a new home for these autocommands diff --git a/lua/lvim/utils/table.lua b/lua/lvim/utils/table.lua new file mode 100644 index 00000000..1ac5949e --- /dev/null +++ b/lua/lvim/utils/table.lua @@ -0,0 +1,24 @@ +local Table = {} + +--- Find the first entry for which the predicate returns true. +-- @param t The table +-- @param predicate The function called for each entry of t +-- @return The entry for which the predicate returned True or nil +function Table.find_first(t, predicate) +  for _, entry in pairs(t) do +    if predicate(entry) then +      return entry +    end +  end +  return nil +end + +--- Check if the predicate returns True for at least one entry of the table. +-- @param t The table +-- @param predicate The function called for each entry of t +-- @return True if predicate returned True at least once, false otherwise +function Table.contains(t, predicate) +  return Table.find_first(t, predicate) ~= nil +end + +return Table | 
