From 3075a218b8b6067b875d30579e856a26aeacd7e7 Mon Sep 17 00:00:00 2001 From: Tomas Mirchev Date: Sun, 26 Oct 2025 04:18:14 +0000 Subject: [PATCH] feat/pack (#1) Reviewed-on: https://gitea.tomastm.com/tomas.mirchev/nvim-config/pulls/1 Co-authored-by: Tomas Mirchev Co-committed-by: Tomas Mirchev --- init.lua | 35 +- lazy-lock.json | 11 - lsp/eslint.lua | 29 +- lsp/tailwindcss.lua | 29 +- lua/{config/autocmds.lua => core/events.lua} | 54 ++- lua/{config => core}/keymaps.lua | 4 +- lua/{config => core}/options.lua | 3 +- lua/custom/language-manager.lua | 128 ------- lua/custom/lm-cmds.lua | 98 ----- lua/{config => modules}/diagnostics.lua | 0 lua/modules/language-specs.lua | 23 ++ lua/modules/navigation.lua | 2 + lua/{config => modules}/terminal.lua | 16 +- lua/modules/theme.lua | 32 ++ lua/plugins/colorscheme.lua | 36 -- lua/plugins/filetree.lua | 160 ++++---- .../navigation.lua => plugins/finder.lua} | 0 lua/plugins/language-manager.lua | 359 ++++++++++++++++++ lua/plugins/syntax.lua | 67 ---- lua/{custom => plugins}/tabline.lua | 7 +- lua/setup/init.lua | 93 +++++ lua/setup/packages.lua | 20 + lua/utils.lua | 41 +- paq-lock.json | 1 + 24 files changed, 735 insertions(+), 513 deletions(-) delete mode 100644 lazy-lock.json rename lua/{config/autocmds.lua => core/events.lua} (58%) rename lua/{config => core}/keymaps.lua (98%) rename lua/{config => core}/options.lua (98%) delete mode 100644 lua/custom/language-manager.lua delete mode 100644 lua/custom/lm-cmds.lua rename lua/{config => modules}/diagnostics.lua (100%) create mode 100644 lua/modules/language-specs.lua create mode 100644 lua/modules/navigation.lua rename lua/{config => modules}/terminal.lua (83%) create mode 100644 lua/modules/theme.lua delete mode 100644 lua/plugins/colorscheme.lua rename lua/{custom/navigation.lua => plugins/finder.lua} (100%) create mode 100644 lua/plugins/language-manager.lua delete mode 100644 lua/plugins/syntax.lua rename lua/{custom => plugins}/tabline.lua (88%) create mode 100644 lua/setup/init.lua create mode 100644 lua/setup/packages.lua create mode 100644 paq-lock.json diff --git a/init.lua b/init.lua index 93b7bfc..6ca8ece 100644 --- a/init.lua +++ b/init.lua @@ -1,29 +1,10 @@ -local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim' -if not (vim.uv or vim.loop).fs_stat(lazypath) then - local lazyrepo = 'https://github.com/folke/lazy.nvim.git' - local out = - vim.fn.system({ 'git', 'clone', '--filter=blob:none', '--branch=stable', lazyrepo, lazypath }) - if vim.v.shell_error ~= 0 then - vim.api.nvim_echo({ - { 'Failed to clone lazy.nvim:\n', 'ErrorMsg' }, - { out, 'WarningMsg' }, - { '\nPress any key to exit...' }, - }, true, {}) - vim.fn.getchar() - os.exit(1) - end +if #vim.api.nvim_list_uis() == 0 then + require('setup') + return end -vim.opt.rtp:prepend(lazypath) -require('config.options') -require('config.keymaps') -require('config.autocmds') -require('config.terminal') -require('custom.navigation') -require('custom.tabline').setup() -require('lazy').setup({ - spec = { { import = 'plugins' } }, - install = { missing = false }, - change_detection = { notify = false }, - rocks = { enabled = false }, -}) +vim.env.PATH = vim.fn.stdpath('data') .. '/mason/bin:' .. vim.env.PATH + +require('core.options') +require('core.keymaps') +require('core.events') diff --git a/lazy-lock.json b/lazy-lock.json deleted file mode 100644 index 2f44ad4..0000000 --- a/lazy-lock.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "conform.nvim": { "branch": "master", "commit": "9fd3d5e0b689ec1bf400c53cbbec72c6fdf24081" }, - "invero.nvim": { "branch": "main", "commit": "6acdefa2f2fdf544b53b5786e748be0ae8e9fd23" }, - "lazy.nvim": { "branch": "main", "commit": "1ea3c4085785f460fb0e46d2fe1ee895f5f9e7c1" }, - "mason.nvim": { "branch": "main", "commit": "ad7146aa61dcaeb54fa900144d768f040090bff0" }, - "nvim-autopairs": { "branch": "master", "commit": "7a2c97cccd60abc559344042fefb1d5a85b3e33b" }, - "nvim-lint": { "branch": "master", "commit": "9da1fb942dd0668d5182f9c8dee801b9c190e2bb" }, - "nvim-tree.lua": { "branch": "master", "commit": "7c0f7e906ab6f11b61eec52171eaf7dc06726ef1" }, - "nvim-treesitter": { "branch": "master", "commit": "42fc28ba918343ebfd5565147a42a26580579482" }, - "nvim-ts-autotag": { "branch": "main", "commit": "c4ca798ab95b316a768d51eaaaee48f64a4a46bc" } -} diff --git a/lsp/eslint.lua b/lsp/eslint.lua index f80694c..4770e1f 100644 --- a/lsp/eslint.lua +++ b/lsp/eslint.lua @@ -1,3 +1,31 @@ +local utils = {} + +function utils.root_markers_with_field(root_files, new_names, field, fname) + local path = vim.fn.fnamemodify(fname, ':h') + local found = vim.fs.find(new_names, { path = path, upward = true }) + + for _, f in ipairs(found or {}) do + -- Match the given `field`. + for line in io.lines(f) do + if line:find(field) then + root_files[#root_files + 1] = vim.fs.basename(f) + break + end + end + end + + return root_files +end + +function utils.insert_package_json(root_files, field, fname) + return utils.root_markers_with_field( + root_files, + { 'package.json', 'package.json5' }, + field, + fname + ) +end + --- @brief --- --- https://github.com/hrsh7th/vscode-langservers-extracted @@ -39,7 +67,6 @@ --- --- /!\ When using flat config files, you need to use them across all your packages in your monorepo, as it's a global setting for the server. -local utils = require('utils') local lsp = vim.lsp local eslint_config_files = { diff --git a/lsp/tailwindcss.lua b/lsp/tailwindcss.lua index 3c432cd..1d523b6 100644 --- a/lsp/tailwindcss.lua +++ b/lsp/tailwindcss.lua @@ -1,10 +1,37 @@ +local utils = {} + +function utils.root_markers_with_field(root_files, new_names, field, fname) + local path = vim.fn.fnamemodify(fname, ':h') + local found = vim.fs.find(new_names, { path = path, upward = true }) + + for _, f in ipairs(found or {}) do + -- Match the given `field`. + for line in io.lines(f) do + if line:find(field) then + root_files[#root_files + 1] = vim.fs.basename(f) + break + end + end + end + + return root_files +end + +function utils.insert_package_json(root_files, field, fname) + return utils.root_markers_with_field( + root_files, + { 'package.json', 'package.json5' }, + field, + fname + ) +end + ---@brief --- https://github.com/tailwindlabs/tailwindcss-intellisense --- --- Tailwind CSS Language Server can be installed via npm: --- --- npm install -g @tailwindcss/language-server -local utils = require('utils') ---@type vim.lsp.Config return { diff --git a/lua/config/autocmds.lua b/lua/core/events.lua similarity index 58% rename from lua/config/autocmds.lua rename to lua/core/events.lua index 53255d5..6ce7b18 100644 --- a/lua/config/autocmds.lua +++ b/lua/core/events.lua @@ -1,5 +1,10 @@ +local api = vim.api +local au = api.nvim_create_autocmd +local group = api.nvim_create_augroup('core.events', { clear = true }) + -- Automatically create a scratch buffer if Neovim starts with no files -vim.api.nvim_create_autocmd('VimEnter', { +au('VimEnter', { + group = group, callback = function() -- Only trigger if no file arguments are passed if vim.fn.argc() == 0 then @@ -12,14 +17,16 @@ vim.api.nvim_create_autocmd('VimEnter', { }) -- Highlight when yanking (copying) text -vim.api.nvim_create_autocmd('TextYankPost', { +au('TextYankPost', { + group = group, callback = function() - vim.highlight.on_yank() + vim.highlight.on_yank({ timeout = 200 }) end, }) -- Disable comment continuation only when using 'o'/'O', but keep it for -vim.api.nvim_create_autocmd('FileType', { +au('FileType', { + group = group, pattern = '*', callback = function() vim.opt_local.formatoptions:remove('o') @@ -27,7 +34,8 @@ vim.api.nvim_create_autocmd('FileType', { }) -- Show cursor line only in active window -vim.api.nvim_create_autocmd({ 'WinEnter', 'InsertLeave' }, { +au({ 'WinEnter', 'InsertLeave' }, { + group = group, callback = function() if vim.w.auto_cursorline then vim.wo.cursorline = true @@ -36,8 +44,10 @@ vim.api.nvim_create_autocmd({ 'WinEnter', 'InsertLeave' }, { end, }) -vim.api.nvim_create_autocmd({ 'WinLeave', 'InsertEnter' }, { +au({ 'WinLeave', 'InsertEnter' }, { + group = group, callback = function() + -- Keep it on NvimTree to show current file if vim.bo.filetype == 'NvimTree' then return end @@ -49,17 +59,17 @@ vim.api.nvim_create_autocmd({ 'WinLeave', 'InsertEnter' }, { }) -- Autocompletion -vim.api.nvim_create_autocmd('LspAttach', { - group = vim.api.nvim_create_augroup('minimal_lsp', { clear = true }), - callback = function(ev) - local client = vim.lsp.get_client_by_id(ev.data.client_id) +au('LspAttach', { + group = group, + callback = function(event) + local client = vim.lsp.get_client_by_id(event.data.client_id) if not client then return end -- Enable native completion if client:supports_method('textDocument/completion') then - vim.lsp.completion.enable(true, client.id, ev.buf, { autotrigger = true }) + vim.lsp.completion.enable(true, client.id, event.buf, { autotrigger = true }) end end, }) @@ -75,3 +85,25 @@ vim.lsp.handlers['textDocument/completion'] = function(err, result, ctx, config) end return vim.lsp.completion._on_completion_result(err, result, ctx, config) end + +au('InsertEnter', { + group = group, + once = true, + callback = function() + require('nvim-ts-autotag').setup() + require('nvim-autopairs').setup() + end, +}) + +require('modules.theme') +au('UIEnter', { + group = group, + once = true, + callback = function() + vim.schedule(function() + require('modules.navigation') + require('plugins.language-manager').setup() + require('modules.diagnostics') + end) + end, +}) diff --git a/lua/config/keymaps.lua b/lua/core/keymaps.lua similarity index 98% rename from lua/config/keymaps.lua rename to lua/core/keymaps.lua index 837f9a4..906891a 100644 --- a/lua/config/keymaps.lua +++ b/lua/core/keymaps.lua @@ -9,6 +9,7 @@ end -- QOL map('i', 'jk', '') +map('i', '', '') map('n', '', cmd('nohlsearch')) map('n', 'q:', '') @@ -80,9 +81,8 @@ map('n', '[d', function() end) map('n', 'q', vim.diagnostic.setloclist) map('n', 'd', vim.diagnostic.open_float) -map('n', 's', vim.lsp.buf.signature_help) +map('n', 'K', vim.lsp.buf.hover) map('n', 'gd', vim.lsp.buf.definition) map('n', 'gr', vim.lsp.buf.references) -map('n', 'K', vim.lsp.buf.hover) map('n', '', vim.lsp.buf.signature_help) diff --git a/lua/config/options.lua b/lua/core/options.lua similarity index 98% rename from lua/config/options.lua rename to lua/core/options.lua index 4c3d5ce..cac91c8 100644 --- a/lua/config/options.lua +++ b/lua/core/options.lua @@ -16,7 +16,6 @@ vim.g.loaded_vimballPlugin = 1 vim.g.loaded_matchit = 1 vim.g.loaded_2html_plugin = 1 vim.g.loaded_rrhelper = 1 -vim.g.loaded_matchparen = 1 vim.g.loaded_tutor_mode_plugin = 1 vim.g.loaded_spellfile_plugin = 1 vim.g.loaded_logipat = 1 @@ -87,7 +86,7 @@ vim.opt.undofile = true vim.opt.swapfile = false -- Tweaks -vim.opt.updatetime = 1000 +vim.opt.updatetime = 50 vim.opt.timeout = true vim.opt.ttimeout = true vim.opt.timeoutlen = 500 diff --git a/lua/custom/language-manager.lua b/lua/custom/language-manager.lua deleted file mode 100644 index 48d86f0..0000000 --- a/lua/custom/language-manager.lua +++ /dev/null @@ -1,128 +0,0 @@ ---[[ -local lm = require('language_manager') -lm.install() or .install({'ts_parsers','language_servers','linters','formatters'}) - - treesitter.install specs.ts_parsers - - mason.install mason-registry.get_package(name):install() -lm.generate_specs() or {use_cache=true, only_installed=true} -lm.delete_cache() -lm.enable() ---]] - -local M = {} - -local function wrap(item) - if type(item) == 'string' then - return { item } - elseif type(item) == 'table' then - return item - else - return {} - end -end - -local function create_set() - local seen = {} - return function(ref, item) -- unique() - if not seen[ref] then - seen[ref] = {} - end - if item and not seen[ref][item] then - seen[ref][item] = true - return true - end - return false - end -end - -local function create_spec() - local M = {} - local unique = create_set() - local specs = {} - - function M.set(item, group) - specs[group] = item - end - - function M.get() - return specs - end - - function M.add(list, ...) - local groups = { ... } - local ref = table.concat(groups, '.') - local pointer = specs - local last_i = #groups - - for i = 1, last_i do - local group = groups[i] - - if i == last_i then - for _, item in ipairs(wrap(list)) do - if unique(ref, item) then - if not pointer[group] then - pointer[group] = {} - end - table.insert(pointer[group], item) - end - end - else - if not pointer[group] then - pointer[group] = {} - end - pointer = pointer[group] - end - end - end - - return M -end - -function M.generate_specs(specs_raw) - local specs = create_spec() - - for _, spec in ipairs(specs_raw) do - -- -> { ft = } - if type(spec) == 'string' then - spec = { ft = spec } - end - - -- Filetype = TS Parser - if not spec.ts then - spec.ts = spec.ft - end - - specs.add(spec.ts, 'ts_parsers') - specs.add(spec.lsp, 'language_servers') - - for _, filetype in ipairs(wrap(spec.ft)) do - specs.add(spec.lint, 'linters_by_ft', filetype) - specs.add(spec.format, 'formatters_by_ft', filetype) - end - end - - M.general = specs.get() - return M.general -end - -M.lsp = {} -function M.lsp.enable() - local registry = require('mason-registry') - local lsp_configs = {} - - for _, lsp in ipairs(M.general.language_servers) do - if registry.has_package(lsp) then - local pkg = registry.get_package(lsp) - local spec = pkg.spec and pkg.spec.neovim - local lsp_name = (spec and spec.lspconfig) or lsp - table.insert(lsp_configs, lsp_name) - - vim.lsp.enable(lsp_name) - else - vim.notify('Unknown LSP: ' .. lsp, vim.log.levels.WARN) - end - end - - vim.notify('Enabled LSPs: ' .. table.concat(lsp_configs, ', '), vim.log.levels.INFO) -end - -return M diff --git a/lua/custom/lm-cmds.lua b/lua/custom/lm-cmds.lua deleted file mode 100644 index 868cc2f..0000000 --- a/lua/custom/lm-cmds.lua +++ /dev/null @@ -1,98 +0,0 @@ -local general = {} ---------------------------------------------------------------------- --- Mason Install Command ---------------------------------------------------------------------- -vim.api.nvim_create_user_command('MasonInstallAll', function() - local registry = require('mason-registry') - local list = vim.list_extend(vim.list_extend({}, general.lsps), {}) - - for _, ftmap in pairs({ general.linters_by_ft, general.formatters_by_ft }) do - for _, tools in pairs(ftmap) do - vim.list_extend(list, tools) - end - end - - list = uniq(list) - local installed = {} - for _, pkg in ipairs(registry.get_installed_packages()) do - installed[pkg.name] = true - end - - for _, name in ipairs(list) do - if registry.has_package(name) then - if not installed[name] then - vim.notify('Installing ' .. name, vim.log.levels.INFO) - registry.get_package(name):install() - else - vim.notify('Already installed ' .. name, vim.log.levels.INFO) - end - else - vim.notify('Package not found in registry: ' .. name, vim.log.levels.WARN) - end - end -end, { desc = 'Install all Mason LSPs, linters, and formatters' }) - ---------------------------------------------------------------------- --- Fetch LSP default configs from nvim-lspconfig ---------------------------------------------------------------------- -vim.api.nvim_create_user_command('FetchLspConfigs', function() - local registry = require('mason-registry') - local lspconfig_names = {} - - for _, lsp in ipairs(general.lsps) do - if registry.has_package(lsp) then - local pkg = registry.get_package(lsp) - local spec = pkg.spec and pkg.spec.neovim - if spec and spec.lspconfig then - table.insert(lspconfig_names, spec.lspconfig) - else - table.insert(lspconfig_names, lsp) - end - else - table.insert(lspconfig_names, lsp) - end - end - - lspconfig_names = uniq(lspconfig_names) - - -- base URL same as your original - local base_url = 'https://raw.githubusercontent.com/neovim/nvim-lspconfig/master/lsp/' - -- write to current working directory - local lsp_dir = vim.fs.joinpath(vim.fn.getcwd(), 'lsp') - vim.fn.mkdir(lsp_dir, 'p') - - for _, name in ipairs(lspconfig_names) do - local file = vim.fs.joinpath(lsp_dir, name .. '.lua') - if vim.fn.filereadable(file) == 0 then - local url = base_url .. name .. '.lua' - local cmd = string.format('curl -fsSL -o %q %q', file, url) - vim.fn.system(cmd) - if vim.v.shell_error ~= 0 then - vim.notify('Failed to fetch ' .. name .. '.lua', vim.log.levels.ERROR) - vim.fn.delete(file) - else - vim.notify('Fetched ' .. name .. '.lua', vim.log.levels.INFO) - end - else - vim.notify('Skipped existing ' .. name .. '.lua', vim.log.levels.INFO) - end - end -end, { desc = 'Fetch default LSP configs into ./lsp in cwd' }) - -vim.api.nvim_create_user_command('TreesitterInstallAll', function() - local parsers = require('nvim-treesitter.parsers') - local configs = require('nvim-treesitter.configs') - local langs = configs.get_module('ensure_installed') or {} - if type(langs) == 'string' and langs == 'all' then - vim.notify('Treesitter ensure_installed = "all" not supported here', vim.log.levels.WARN) - return - end - - for _, lang in ipairs(langs) do - if not parsers.has_parser(lang) then - vim.cmd('TSInstall ' .. lang) - else - vim.notify('Parser already installed: ' .. lang, vim.log.levels.INFO) - end - end -end, { desc = 'Install all Treesitter parsers defined in ensure_installed' }) diff --git a/lua/config/diagnostics.lua b/lua/modules/diagnostics.lua similarity index 100% rename from lua/config/diagnostics.lua rename to lua/modules/diagnostics.lua diff --git a/lua/modules/language-specs.lua b/lua/modules/language-specs.lua new file mode 100644 index 0000000..5aa31b5 --- /dev/null +++ b/lua/modules/language-specs.lua @@ -0,0 +1,23 @@ +local M = {} + +function M.get() + return { + { ts = { 'yaml', 'toml', 'sql', 'diff', 'dockerfile', 'gitcommit', 'gitignore' } }, + { ts = { 'c', 'cpp', 'go', 'rust', 'python' } }, + + { ft = 'markdown', ts = { 'markdown', 'markdown_inline' }, format = 'prettier' }, + { ft = 'bash', lsp = 'bash-language-server', lint = 'shellcheck', format = 'shfmt' }, + { ft = 'lua', lsp = 'lua-language-server', lint = 'luacheck', format = 'stylua' }, + { ft = { 'json', 'jsonc' }, lsp = 'json-lsp' }, + { ft = 'html', lsp = 'html-lsp' }, + { ft = 'css', lsp = { 'css-lsp', 'tailwindcss-language-server' } }, + { + ft = { 'javascript', 'typescript', 'javascriptreact', 'typescriptreact' }, + ts = { 'javascript', 'typescript', 'tsx' }, + lsp = { 'vtsls', 'eslint-lsp' }, + format = { 'prettierd', 'prettier' }, + }, +} +end + +return M diff --git a/lua/modules/navigation.lua b/lua/modules/navigation.lua new file mode 100644 index 0000000..e291a6f --- /dev/null +++ b/lua/modules/navigation.lua @@ -0,0 +1,2 @@ +require('plugins.finder') +require('plugins.filetree') diff --git a/lua/config/terminal.lua b/lua/modules/terminal.lua similarity index 83% rename from lua/config/terminal.lua rename to lua/modules/terminal.lua index 675262e..c44f88e 100644 --- a/lua/config/terminal.lua +++ b/lua/modules/terminal.lua @@ -1,8 +1,10 @@ -local term_group = vim.api.nvim_create_augroup('custom-term', { clear = true }) +local api = vim.api +local au = api.nvim_create_autocmd +local group = api.nvim_create_augroup('triimd.term', { clear = true }) -- Custom terminal -vim.api.nvim_create_autocmd('TermOpen', { - group = term_group, +au('TermOpen', { + group = group, callback = function() vim.opt_local.number = false vim.opt_local.relativenumber = false @@ -13,8 +15,8 @@ vim.api.nvim_create_autocmd('TermOpen', { }) -- Insert when re-entering a terminal window (after switching back) -vim.api.nvim_create_autocmd('BufEnter', { - group = term_group, +au('BufEnter', { + group = group, pattern = 'term://*', callback = function() if vim.bo.buftype == 'terminal' and vim.fn.mode() ~= 'i' then @@ -24,8 +26,8 @@ vim.api.nvim_create_autocmd('BufEnter', { }) -- Close all terminal buffers before quitting -vim.api.nvim_create_autocmd('QuitPre', { - group = term_group, +au('QuitPre', { + group = group, callback = function() for _, buf in ipairs(vim.api.nvim_list_bufs()) do if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].buftype == 'terminal' then diff --git a/lua/modules/theme.lua b/lua/modules/theme.lua new file mode 100644 index 0000000..9ee7cfc --- /dev/null +++ b/lua/modules/theme.lua @@ -0,0 +1,32 @@ + +local function load_theme() + require('invero').setup({ + highlights = function(c, tool) + c.bg_float = tool(152) + return { + ModeMsg = { fg = c.yellow, bg = c.none, bold = true }, + WinSeparator = { fg = c.outline, bg = c.base }, + StatusLine = { fg = c.outline, bg = c.base }, + StatusLineNC = { fg = c.text, bg = c.base, bold = true }, + TabLine = { fg = c.muted, bg = c.none }, + TabLineSel = { fg = c.text, bg = c.none, bold = true }, + TabLineFill = { fg = c.outline_light, bg = c.none }, + Pmenu = { fg = c.text, bg = c.surface }, + PmenuSel = { fg = c.text, bg = c.accent_light }, + QuickFixLine = { fg = c.accent, bg = c.none, bold = true }, + } + end, + }) + + vim.o.background = 'light' + vim.cmd.colorscheme('invero') +end + +vim.api.nvim_create_user_command('ReloadInvero', function() + require('invero').invalidate_cache() + load_theme() +end, {}) + +load_theme() + +require('plugins.tabline').setup() diff --git a/lua/plugins/colorscheme.lua b/lua/plugins/colorscheme.lua deleted file mode 100644 index 65c3568..0000000 --- a/lua/plugins/colorscheme.lua +++ /dev/null @@ -1,36 +0,0 @@ -return { - 'triimdev/invero.nvim', - lazy = false, - priority = 1000, - config = function() - vim.api.nvim_create_user_command('ReloadInvero', function() - require('invero').invalidate_cache() - vim.cmd('Lazy reload invero.nvim') - end, {}) - - require('invero').setup({ - highlights = function(c, tool) - c.bg_float = tool(152) - return { - ModeMsg = { fg = c.yellow, bg = c.none, bold = true }, - WinSeparator = { fg = c.outline, bg = c.base }, - StatusLine = { fg = c.outline, bg = c.base }, - StatusLineNC = { fg = c.text, bg = c.base, bold = true }, - TabLine = { fg = c.muted, bg = c.none }, - TabLineSel = { fg = c.text, bg = c.none, bold = true }, - TabLineFill = { fg = c.outline_light, bg = c.none }, - - Pmenu = { fg = c.text, bg = c.surface }, - PmenuSel = { fg = c.text, bg = c.accent_light }, - QuickFixLine = { fg = c.accent, bg = c.none, bold = true }, - -- PmenuSbar = { bg = c.surface }, - -- PmenuThumb = { bg = c.outline }, - -- PmenuBorder = { fg = c.outline }, - } - end, - }) - - vim.o.background = 'light' - vim.cmd.colorscheme('invero') - end, -} diff --git a/lua/plugins/filetree.lua b/lua/plugins/filetree.lua index 01acb31..bc1ff69 100644 --- a/lua/plugins/filetree.lua +++ b/lua/plugins/filetree.lua @@ -46,102 +46,78 @@ local function my_on_attach(bufnr) vim.keymap.set('n', 'U', api.tree.reload, opts) end -return { - 'https://gitea.tomastm.com/tomas.mirchev/nvim-tree.lua', - branch = 'master', - opts = { - on_attach = my_on_attach, - view = { signcolumn = 'no' }, - actions = { file_popup = { open_win_config = { border = 'rounded' } } }, - renderer = { - root_folder_label = false, - -- root_folder_label = function(path) - -- return '-- ' .. vim.fn.fnamemodify(path, ':t') .. ' --' - -- end, - special_files = {}, +require('nvim-tree').setup({ + on_attach = my_on_attach, + view = { signcolumn = 'no' }, + actions = { file_popup = { open_win_config = { border = 'rounded' } } }, + renderer = { + root_folder_label = false, + special_files = {}, - highlight_hidden = 'all', - highlight_clipboard = 'all', + highlight_hidden = 'all', + highlight_clipboard = 'all', - indent_markers = { - enable = true, - inline_arrows = false, - icons = { corner = '│', none = '│', bottom = ' ' }, + indent_markers = { + enable = true, + inline_arrows = false, + icons = { corner = '│', none = '│', bottom = ' ' }, + }, + icons = { + bookmarks_placement = 'after', + git_placement = 'after', + show = { + file = false, + folder = false, + folder_arrow = false, + git = true, + modified = false, + hidden = false, + diagnostics = false, + bookmarks = true, }, - icons = { - bookmarks_placement = 'after', - git_placement = 'after', - show = { - file = false, - folder = false, - folder_arrow = false, -- KEEP FALSE - git = true, - modified = false, - hidden = false, - diagnostics = false, - bookmarks = true, + glyphs = { + git = { + unstaged = '◇', + staged = '', + unmerged = '', + renamed = '', + untracked = '', + deleted = '', + ignored = '', }, - glyphs = { - -- default = '•', - default = ' ', - symlink = '', - bookmark = '󰆤', - modified = '●', - hidden = '󰜌', - folder = { - arrow_closed = '', - arrow_open = '', - default = '▸', - open = '▾', - empty = '', - empty_open = '', - symlink = '', - symlink_open = '', - }, - - git = { - unstaged = '◇', -- '✗', - staged = '', - unmerged = '', - renamed = '', - untracked = '', - deleted = '', -- '󰧧', - ignored = '', - }, - }, - }, - }, - - hijack_cursor = true, - prefer_startup_root = true, - update_focused_file = { - enable = true, - update_root = { enable = true, ignore_list = {} }, - exclude = false, - }, - modified = { enable = true, show_on_dirs = true, show_on_open_dirs = true }, - filters = { - enable = true, - git_ignored = true, - dotfiles = false, - git_clean = false, - no_buffer = false, - no_bookmark = false, - custom = {}, - exclude = {}, - }, - filesystem_watchers = { - enable = true, - debounce_delay = 50, - ignore_dirs = { - '/.git', - '/.DS_Store', - '/build', - '/dist', - '/public', - '/node_modules', - '/target', }, }, }, -} + + hijack_cursor = true, + prefer_startup_root = true, + update_focused_file = { + enable = true, + update_root = { enable = true, ignore_list = {} }, + exclude = false, + }, + modified = { enable = true, show_on_dirs = true, show_on_open_dirs = true }, + filters = { + enable = true, + git_ignored = true, + dotfiles = false, + git_clean = false, + no_buffer = false, + no_bookmark = false, + custom = {}, + exclude = {}, + }, + filesystem_watchers = { + enable = true, + debounce_delay = 50, + ignore_dirs = { + '/.git', + '/.DS_Store', + '/build', + '/dist', + '/public', + '/node_modules', + '/target', + }, + }, +}) diff --git a/lua/custom/navigation.lua b/lua/plugins/finder.lua similarity index 100% rename from lua/custom/navigation.lua rename to lua/plugins/finder.lua diff --git a/lua/plugins/language-manager.lua b/lua/plugins/language-manager.lua new file mode 100644 index 0000000..c9ab9d9 --- /dev/null +++ b/lua/plugins/language-manager.lua @@ -0,0 +1,359 @@ +local utils = require('utils') +local M = { + ts = {}, + lsp = {}, + lint = {}, + format = {}, +} + +M.group = vim.api.nvim_create_augroup('language-manager', { clear = true }) + +local cache_path = vim.fn.stdpath('cache') .. '/language-manager.json' + +local function wrap(item) + if type(item) == 'string' then + return { item } + elseif type(item) == 'table' then + return item + else + return {} + end +end + +local function create_set() + local seen = {} + return function(ref, item) + if not seen[ref] then + seen[ref] = {} + end + if item and not seen[ref][item] then + seen[ref][item] = true + return true + end + return false + end +end + +local function read_file(path) + local f = io.open(path, 'r') + if not f then + return nil + end + local content = f:read('*a') + f:close() + return content +end + +local function write_file(path, content) + local f = io.open(path, 'w') + if not f then + return false + end + f:write(content) + f:close() + return true +end + +local function to_json(tbl) + local ok, result = pcall(vim.json.encode, tbl) + return ok and result or nil +end + +local function from_json(str) + local ok, result = pcall(vim.json.decode, str) + return ok and result or nil +end + +local function hash_spec(tbl) + local encoded = to_json(tbl) or '' + if vim.fn.exists('*sha256') == 1 then + return vim.fn.sha256(encoded) + else + local tmp = vim.fn.tempname() + write_file(tmp, encoded) + local handle = io.popen('openssl dgst -sha256 ' .. tmp) + local result = handle and handle:read('*a') or '' + if handle then + handle:close() + end + return result:match('([a-f0-9]+)') or '' + end +end + +local function save_cache(data) + local encoded = to_json(data) + if encoded then + write_file(cache_path, encoded) + end +end + +local function load_cache() + local raw = read_file(cache_path) + if not raw then + return nil + end + return from_json(raw) +end + +-- ======== Spec Builder ======== + +local function create_spec() + local S = {} + local unique = create_set() + local specs = {} + + function S.set(item, group) + specs[group] = item + end + + function S.get() + return specs + end + + function S.add(list, ...) + local groups = { ... } + local ref = table.concat(groups, '.') + local pointer = specs + local last_i = #groups + + for i = 1, last_i do + local group = groups[i] + if i == last_i then + for _, item in ipairs(wrap(list)) do + if unique(ref, item) then + pointer[group] = pointer[group] or {} + table.insert(pointer[group], item) + end + end + else + pointer[group] = pointer[group] or {} + pointer = pointer[group] + end + end + end + + return S +end + +-- ======== Spec Generation ======== + +local function get_mason_registry() + vim.cmd.packadd('mason.nvim') + require('mason').setup() + + local registry = require('mason-registry') + registry.refresh() + + return registry +end + +function M.generate_specs(specs_raw) + local install_spec = create_spec() + local specs = create_spec() + local registry = get_mason_registry() + + local lsp_map = {} + + for _, spec in ipairs(specs_raw) do + if type(spec) == 'string' then + spec = { ft = spec } + end + + spec.ts = spec.ts or spec.ft + specs.add(spec.ts, 'ts_parsers') + + install_spec.add(spec.lsp, 'code_tools') + local resolved_lsps = {} + for _, language_server in ipairs(wrap(spec.lsp)) do + if registry.has_package(language_server) then + local pkg = registry.get_package(language_server) + if pkg.spec and pkg.spec.neovim and pkg.spec.neovim.lspconfig then + local lspconfig_name = pkg.spec and pkg.spec.neovim and pkg.spec.neovim.lspconfig + lsp_map[lspconfig_name] = language_server + table.insert(resolved_lsps, lspconfig_name) + else + print('Package found but not lspconfig name: ' .. language_server) + end + else + print('Package not found: ' .. language_server) + end + end + specs.add(resolved_lsps, 'language_servers') + + install_spec.add(spec.lint, 'code_tools') + install_spec.add(spec.format, 'code_tools') + for _, ft in ipairs(wrap(spec.ft)) do + specs.add(spec.lint, 'linters_by_ft', ft) + specs.add(spec.format, 'formatters_by_ft', ft) + end + end + + local result = specs.get() + result.lsp_map = lsp_map + return result, install_spec.get() +end + +-- ======== Cache ======== + +function M.load_specs() + local cache = load_cache() + if cache then + M.general = cache.spec + else + local specs_raw = require('modules.language-specs').get() + M.general, M.mason = M.generate_specs(specs_raw) + save_cache({ hash = hash_spec(specs_raw), spec = M.general }) + end + return M.general, M.mason +end + +function M.invalidate_cache() + local ok = os.remove(cache_path) + if ok then + vim.notify('Language manager cache invalidated', vim.log.levels.INFO) + else + vim.notify('No cache to invalidate or failed to delete', vim.log.levels.WARN) + end + M.general = nil + M.mason = nil +end + +function M.ts.install() + vim.cmd.packadd('nvim-treesitter') + local ts_install = require('nvim-treesitter.install').ensure_installed_sync + ts_install(M.general.ts_parsers) +end + +function M.mason_install() + local packages = M.mason.code_tools + local registry = get_mason_registry() + + local pending = #packages + + local result = utils.await(function(resolve) + for _, name in ipairs(packages) do + local pkg = registry.get_package(name) + if pkg:is_installed() then + print('Mason package already installed: ' .. name) + pending = pending - 1 + if pending == 0 then + resolve(true) + end + else + print('Mason package installing: ' .. name) + pkg:install({}, function(success, error) + if success then + print('Mason package installed: ' .. name) + else + print('Mason package failed: ' .. name) + print(' > Error: ' .. vim.inspect(error)) + end + + pending = pending - 1 + if pending == 0 then + resolve(true) + end + end) + end + end + end, 5 * 60 * 1000, 200) + + if not result.ok or pending ~= 0 then + print('\n!! >> Exited timeout, possible clean up needed!') + print(' > status: ' .. result.ok) + print(' > pending: ' .. pending) + end +end + +-- ======== Public API ======== + +function M.install() + print('\n> Starting ts parsers install') + M.ts.install() + + print('\n> Starting mason install: lsp, lint, format') + M.mason_install() +end + +function M.ts.setup() + require('nvim-treesitter.configs').setup({ + highlight = { enable = true }, + incremental_selection = { enable = true }, + }) +end + +function M.lsp.setup() + for _, lsp_name in ipairs((M.general and M.general.language_servers) or {}) do + vim.lsp.enable(lsp_name) + end + + vim.api.nvim_buf_create_user_command(0, 'LspInfo', function() + vim.cmd('checkhealth vim.lsp') + end, {}) +end + +function M.lint.setup() + vim.api.nvim_create_autocmd({ 'BufReadPre', 'BufNewFile' }, { + group = M.group, + once = true, + callback = function() + local lint = require('lint') + lint.linters_by_ft = M.general.linters_by_ft + + function M.debounce(ms, fn) + local timer = vim.uv.new_timer() + return function(...) + local argv = { ... } + timer:start(ms, 0, function() + timer:stop() + vim.schedule_wrap(fn)(unpack(argv)) + end) + end + end + + function M.lint() + if vim.bo.modifiable then + lint.try_lint() + end + end + + vim.api.nvim_create_autocmd({ 'BufReadPost', 'InsertLeave', 'TextChanged' }, { + group = vim.api.nvim_create_augroup('language-manager.lint', { clear = true }), + callback = M.debounce(100, M.lint), + }) + + vim.api.nvim_create_autocmd('BufEnter', { + group = M.group, + callback = function(args) + local bufnr = args.buf + local ft = vim.bo[bufnr].filetype + local linters = lint.linters_by_ft[ft] + + if linters then + vim.api.nvim_buf_create_user_command(bufnr, 'LintInfo', function() + print('Linters for ' .. ft .. ': ' .. table.concat(linters, ', ')) + end, {}) + end + end, + }) + end, + }) +end + +function M.format.setup() + require('conform').setup({ + formatters_by_ft = (M.general and M.general.formatters_by_ft) or {}, + default_format_opts = { stop_after_first = true, lsp_format = 'fallback' }, + format_on_save = { timeout_ms = 500 }, + }) +end + +function M.setup() + M.load_specs() + M.ts.setup() + M.lsp.setup() + M.lint.setup() + M.format.setup() +end + +return M diff --git a/lua/plugins/syntax.lua b/lua/plugins/syntax.lua deleted file mode 100644 index 3ca2f6f..0000000 --- a/lua/plugins/syntax.lua +++ /dev/null @@ -1,67 +0,0 @@ -local lm = require('custom.language-manager') -local language_specs = { - { ts = { 'yaml', 'toml', 'sql', 'diff', 'dockerfile', 'gitcommit', 'gitignore' } }, - { ts = { 'c', 'cpp', 'go', 'rust', 'python' } }, - - { ft = 'markdown', ts = { 'markdown', 'markdown_inline' }, format = 'prettier' }, - { ft = 'bash', lsp = 'bash-language-server', lint = 'shellcheck', format = 'shfmt' }, - { ft = 'lua', lsp = 'lua-language-server', lint = 'luacheck', format = 'stylua' }, - { ft = { 'json', 'jsonc' }, lsp = 'json-lsp' }, - { ft = 'html', lsp = 'html-lsp' }, - { ft = 'css', lsp = { 'css-lsp', 'tailwindcss-language-server' } }, - { - ft = { 'javascript', 'typescript', 'javascriptreact', 'typescriptreact' }, - ts = { 'javascript', 'typescript', 'tsx' }, - lsp = { 'vtsls', 'eslint-lsp' }, - format = { 'prettierd', 'prettier' }, - }, -} - -local general = lm.generate_specs(language_specs) - -return { - { 'windwp/nvim-ts-autotag', config = true }, - { 'windwp/nvim-autopairs', event = 'InsertEnter', config = true }, - { - 'mason-org/mason.nvim', - config = function() - require('mason').setup() - lm.lsp.enable() - end, - }, - { - 'nvim-treesitter/nvim-treesitter', - build = ':TSUpdate', - main = 'nvim-treesitter.configs', - opts = { - highlight = { enable = true }, - incremental_selection = { enable = true }, - ensure_installed = general.ts_parsers, - }, - }, - { - 'mfussenegger/nvim-lint', - event = { 'BufReadPre', 'BufNewFile' }, - opts = { linters_by_ft = general.linters_by_ft }, - config = function(_, opts) - local lint = require('lint') - lint.linters_by_ft = opts.linters_by_ft - - vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost', 'InsertLeave' }, { - group = vim.api.nvim_create_augroup('lint_autocmd', { clear = true }), - callback = function() - lint.try_lint() - end, - }) - end, - }, - { - 'stevearc/conform.nvim', - event = { 'BufWritePre' }, - opts = { - format_on_save = { timeout_ms = 500, lsp_format = 'fallback' }, - default_format_opts = { stop_after_first = true }, - formatters_by_ft = general.formatters_by_ft, - }, - }, -} diff --git a/lua/custom/tabline.lua b/lua/plugins/tabline.lua similarity index 88% rename from lua/custom/tabline.lua rename to lua/plugins/tabline.lua index 411024a..edefc5f 100644 --- a/lua/custom/tabline.lua +++ b/lua/plugins/tabline.lua @@ -1,6 +1,3 @@ --- ~/.config/nvim/lua/custom/tabline.lua --- Custom Lua tabline with proper modified/unsaved and window count handling - local M = {} -- Helper to get label for each tab page @@ -79,8 +76,8 @@ function M.tabline() end function M.setup() - vim.o.showtabline = 1 - vim.o.tabline = "%!v:lua.require'custom.tabline'.tabline()" + vim.opt.showtabline = 1 + vim.opt.tabline = "%!v:lua.require'plugins.tabline'.tabline()" end return M diff --git a/lua/setup/init.lua b/lua/setup/init.lua new file mode 100644 index 0000000..f990335 --- /dev/null +++ b/lua/setup/init.lua @@ -0,0 +1,93 @@ +local function clone_package_manager() + local path = vim.fn.stdpath('data') .. '/site/pack/paqs/opt/paq-nvim' + if not vim.uv.fs_stat(path) then + local repo = 'https://github.com/savq/paq-nvim.git' + local cmd = { 'git', 'clone', '--depth=1', repo, path } + local result = vim.system(cmd):wait() + if result.code == 0 then + print('Package manager installed correctly') + end + end +end + +local function install_packages() + local done = false + vim.api.nvim_create_autocmd('User', { + pattern = 'PaqDoneInstall', + once = true, + callback = function() + done = true + end, + }) + + vim.cmd.packadd('paq-nvim') + local paq = require('paq') + paq:setup({ lock = vim.fn.stdpath("config") .. "/paq-lock.json" }) + local packages = require('setup.packages').get() + + paq(packages) + paq.install() + + local to_install = paq.query('to_install') + if #to_install == 0 then + return + end + + vim.wait(60000, function() + return done + end, 200) + + if not done then + print('Paq installation timeout or failed') + else + print('Paq installation completed') + end +end + +local function install_languages() + vim.cmd.packadd('mason.nvim') + require('mason').setup() + + local lm = require('plugins.language-manager') + lm.invalidate_cache() + lm.load_specs() + lm.install() +end + +vim.api.nvim_create_user_command('InstallAll', function() + print('> Starting clone package manager...') + clone_package_manager() + print('\n> Starting installing packages...') + install_packages() + print('\n> Starting installing languages: ts parsers, language servers, linters, formatters...') + install_languages() + print('\n=== Install Finished ===\n\n') +end, {}) + +vim.api.nvim_create_user_command('FetchLspConfigs', function() + local base_url = 'https://raw.githubusercontent.com/neovim/nvim-lspconfig/master/lsp/' + + local lm = require('plugins.language-manager') + lm.invalidate_cache() + local general = lm.load_specs() + + local lsp_dir = vim.fs.joinpath(vim.fn.getcwd(), 'lsp') + vim.fn.mkdir(lsp_dir, 'p') + + for _, name in ipairs(general.language_servers or {}) do + local file = vim.fs.joinpath(lsp_dir, name .. '.lua') + if vim.fn.filereadable(file) == 0 then + local url = base_url .. name .. '.lua' + local cmd = string.format('curl -fsSL -o %q %q', file, url) + vim.fn.system(cmd) + if vim.v.shell_error ~= 0 then + vim.notify('Failed to fetch ' .. name .. '.lua', vim.log.levels.ERROR) + vim.fn.delete(file) + else + vim.notify('Fetched ' .. name .. '.lua', vim.log.levels.INFO) + end + else + vim.notify('Skipped existing ' .. name .. '.lua', vim.log.levels.INFO) + end + end +end, { desc = 'Fetch default LSP configs into ./lsp in cwd' }) diff --git a/lua/setup/packages.lua b/lua/setup/packages.lua new file mode 100644 index 0000000..ec36031 --- /dev/null +++ b/lua/setup/packages.lua @@ -0,0 +1,20 @@ +local M = {} + +function M.get() + return { + { 'savq/paq-nvim', opt = true }, + { 'https://github.com/mason-org/mason.nvim', opt = true }, + + { 'https://github.com/triimd/invero.nvim' }, + { 'https://gitea.tomastm.com/tomas.mirchev/nvim-tree.lua', version = 'master' }, + + { 'https://github.com/windwp/nvim-ts-autotag' }, + { 'https://github.com/windwp/nvim-autopairs' }, + + { 'https://github.com/nvim-treesitter/nvim-treesitter', version = 'master' }, + { 'https://github.com/mfussenegger/nvim-lint' }, + { 'https://github.com/stevearc/conform.nvim' }, + } +end + +return M diff --git a/lua/utils.lua b/lua/utils.lua index 6105bde..3799130 100644 --- a/lua/utils.lua +++ b/lua/utils.lua @@ -1,33 +1,24 @@ +-- utils.lua local M = {} ---- Appends `new_names` to `root_files` if `field` is found in any such file in any ancestor of `fname`. ---- ---- NOTE: this does a "breadth-first" search, so is broken for multi-project workspaces: ---- https://github.com/neovim/nvim-lspconfig/issues/3818#issuecomment-2848836794 ---- ---- @param root_files string[] List of root-marker files to append to. ---- @param new_names string[] Potential root-marker filenames (e.g. `{ 'package.json', 'package.json5' }`) to inspect for the given `field`. ---- @param field string Field to search for in the given `new_names` files. ---- @param fname string Full path of the current buffer name to start searching upwards from. -function M.root_markers_with_field(root_files, new_names, field, fname) - local path = vim.fn.fnamemodify(fname, ':h') - local found = vim.fs.find(new_names, { path = path, upward = true }) +function M.await(fn, timeout, interval) + local done = false + local ok, data - for _, f in ipairs(found or {}) do - -- Match the given `field`. - for line in io.lines(f) do - if line:find(field) then - root_files[#root_files + 1] = vim.fs.basename(f) - break - end - end - end + -- Wrap resolve in vim.schedule so it runs on main loop + fn(function(success, result) + vim.schedule(function() + done = true + ok = success + data = result + end) + end) - return root_files -end + vim.wait(timeout, function() + return done + end, interval) -function M.insert_package_json(root_files, field, fname) - return M.root_markers_with_field(root_files, { 'package.json', 'package.json5' }, field, fname) + return { ok = ok or false, data = data } end return M diff --git a/paq-lock.json b/paq-lock.json new file mode 100644 index 0000000..ab75310 --- /dev/null +++ b/paq-lock.json @@ -0,0 +1 @@ +{"nvim-ts-autotag":{"name":"nvim-ts-autotag","url":"https://github.com/windwp/nvim-ts-autotag","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/nvim-ts-autotag"},"mason.nvim":{"name":"mason.nvim","url":"https://github.com/mason-org/mason.nvim","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/opt/mason.nvim"},"paq-nvim":{"name":"paq-nvim","url":"https://github.com/savq/paq-nvim.git","hash":"971344d1fe1fd93580961815e7b7c8853c3605e4","status":0,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/opt/paq-nvim"},"nvim-autopairs":{"name":"nvim-autopairs","url":"https://github.com/windwp/nvim-autopairs","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/nvim-autopairs"},"nvim-tree.lua":{"name":"nvim-tree.lua","url":"https://gitea.tomastm.com/tomas.mirchev/nvim-tree.lua","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/nvim-tree.lua"},"nvim-treesitter":{"name":"nvim-treesitter","url":"https://github.com/nvim-treesitter/nvim-treesitter","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/nvim-treesitter"},"conform.nvim":{"name":"conform.nvim","url":"https://github.com/stevearc/conform.nvim","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/conform.nvim"},"invero.nvim":{"name":"invero.nvim","url":"https://github.com/triimd/invero.nvim","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/invero.nvim"},"nvim-lint":{"name":"nvim-lint","url":"https://github.com/mfussenegger/nvim-lint","hash":"","status":1,"dir":"/home/tomas/.local/share/nvim/site/pack/paqs/start/nvim-lint"}} \ No newline at end of file