feat/pack (#1)

Reviewed-on: #1
Co-authored-by: Tomas Mirchev <contact@tomastm.com>
Co-committed-by: Tomas Mirchev <contact@tomastm.com>
This commit was merged in pull request #1.
This commit is contained in:
2025-10-26 04:18:14 +00:00
committed by Tomas Mirchev
parent 2b1b3ebbf0
commit 3075a218b8
24 changed files with 735 additions and 513 deletions

View File

@@ -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,
}

View File

@@ -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',
},
},
})

323
lua/plugins/finder.lua Normal file
View File

@@ -0,0 +1,323 @@
-- Minimal fuzzy finder + content search for Neovim 0.11+
-- Optional: `fdfind` or `fd` for file listing, and `rg` (ripgrep) for text search.
local Fuzzy = {}
--------------------------------------------------------------------
-- 🧩 Helpers
--------------------------------------------------------------------
-- Collect all files (try fdfind/fd first, then globpath)
local function get_file_list()
local handle = io.popen('fdfind --type f 2>/dev/null || fd --type f 2>/dev/null')
if handle then
local result = handle:read('*a')
handle:close()
if result and result ~= '' then
return vim.split(result, '\n', { trimempty = true })
end
end
return vim.fn.globpath('.', '**/*', false, true)
end
-- Create floating input + result windows
local function open_float(prompt)
local input_buf = vim.api.nvim_create_buf(false, true)
local result_buf = vim.api.nvim_create_buf(false, true)
-- mark both buffers as scratch/unlisted
for _, b in ipairs({ input_buf, result_buf }) do
vim.bo[b].bufhidden = 'wipe'
vim.bo[b].buflisted = false
vim.bo[b].swapfile = false
end
vim.bo[input_buf].buftype = 'prompt'
vim.bo[result_buf].buftype = 'nofile'
local width = math.floor(vim.o.columns * 0.7)
local height = 20
local row = math.floor((vim.o.lines - height) / 2)
local col = math.floor((vim.o.columns - width) / 2)
local input_win = vim.api.nvim_open_win(input_buf, true, {
relative = 'editor',
row = row,
col = col,
width = width,
height = 1,
style = 'minimal',
border = 'rounded',
})
vim.fn.prompt_setprompt(input_buf, prompt)
local result_win = vim.api.nvim_open_win(result_buf, false, {
relative = 'editor',
row = row + 2,
col = col,
width = width,
height = height - 2,
style = 'minimal',
border = 'single',
})
return input_buf, result_buf, input_win, result_win
end
--------------------------------------------------------------------
-- 🔵 Highlight current selection
--------------------------------------------------------------------
function Fuzzy:highlight_selection()
if not self.result_buf then
return
end
if not self.ns_id then
self.ns_id = vim.api.nvim_create_namespace('FuzzyHighlight')
end
vim.api.nvim_buf_clear_namespace(self.result_buf, self.ns_id, 0, -1)
if self.matches and self.matches[self.cursor] then
local rel_cursor = self.cursor - (self.scroll or 0)
if rel_cursor >= 1 and rel_cursor <= self.page_size then
vim.api.nvim_buf_set_extmark(self.result_buf, self.ns_id, rel_cursor - 1, 0, {
end_line = rel_cursor,
hl_group = 'Visual',
hl_eol = true,
})
end
end
end
--------------------------------------------------------------------
-- 🔴 Close all floating windows
--------------------------------------------------------------------
function Fuzzy.close()
local wins = { Fuzzy.input_win, Fuzzy.result_win }
for _, win in ipairs(wins) do
if win and vim.api.nvim_win_is_valid(win) then
vim.api.nvim_win_close(win, true)
end
end
Fuzzy.active = false
end
--------------------------------------------------------------------
-- 🟢 File finder
--------------------------------------------------------------------
function Fuzzy.open()
if Fuzzy.active then
Fuzzy.close()
end
Fuzzy.active = true
Fuzzy.files = get_file_list()
Fuzzy.matches = Fuzzy.files
Fuzzy.cursor = 1
Fuzzy.scroll = 0
Fuzzy.page_size = 50
Fuzzy.input_buf, Fuzzy.result_buf, Fuzzy.input_win, Fuzzy.result_win = open_float('Search: ')
local function render_results()
local total = #Fuzzy.matches
if total == 0 then
vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- no matches --' })
return
end
local start_idx = Fuzzy.scroll + 1
local end_idx = math.min(start_idx + Fuzzy.page_size - 1, total)
local display = {}
for i = start_idx, end_idx do
display[#display + 1] = Fuzzy.matches[i]
end
vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, display)
Fuzzy:highlight_selection()
end
local function update_results(text)
if text == '' then
Fuzzy.matches = Fuzzy.files
else
Fuzzy.matches = vim.fn.matchfuzzy(Fuzzy.files, text)
end
Fuzzy.cursor, Fuzzy.scroll = 1, 0
render_results()
end
vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChangedP' }, {
buffer = Fuzzy.input_buf,
callback = function()
local text = vim.fn.getline('.'):gsub('^Search:%s*', '')
update_results(text)
end,
})
vim.keymap.set('i', '<C-n>', function()
if Fuzzy.cursor < #Fuzzy.matches then
Fuzzy.cursor = Fuzzy.cursor + 1
if Fuzzy.cursor > Fuzzy.scroll + Fuzzy.page_size then
Fuzzy.scroll = Fuzzy.scroll + 1
end
render_results()
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<C-p>', function()
if Fuzzy.cursor > 1 then
Fuzzy.cursor = Fuzzy.cursor - 1
if Fuzzy.cursor <= Fuzzy.scroll then
Fuzzy.scroll = math.max(Fuzzy.scroll - 1, 0)
end
render_results()
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<CR>', function()
local choice = Fuzzy.matches[Fuzzy.cursor]
if choice then
Fuzzy.close()
vim.cmd.edit(vim.fn.fnameescape(choice))
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<Esc>', Fuzzy.close, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<C-c>', Fuzzy.close, { buffer = Fuzzy.input_buf })
vim.keymap.set('n', '<Esc>', Fuzzy.close, { buffer = Fuzzy.input_buf })
vim.keymap.set('n', 'q', Fuzzy.close, { buffer = Fuzzy.input_buf })
vim.cmd.startinsert()
end
--------------------------------------------------------------------
-- 🟣 Ripgrep-based content search (scrolling + match highlighting)
--------------------------------------------------------------------
function Fuzzy.open_grep()
if Fuzzy.active then
Fuzzy.close()
end
Fuzzy.active = true
Fuzzy.input_buf, Fuzzy.result_buf, Fuzzy.input_win, Fuzzy.result_win = open_float('Grep: ')
Fuzzy.matches, Fuzzy.cursor, Fuzzy.scroll = {}, 1, 0
Fuzzy.page_size = 50
Fuzzy.ns_id = vim.api.nvim_create_namespace('FuzzyHighlight')
local function render_results(query)
local total = #Fuzzy.matches
if total == 0 then
vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- no matches --' })
return
end
local start_idx = Fuzzy.scroll + 1
local end_idx = math.min(start_idx + Fuzzy.page_size - 1, total)
local display = {}
for i = start_idx, end_idx do
display[#display + 1] = Fuzzy.matches[i]
end
vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, display)
vim.api.nvim_buf_clear_namespace(Fuzzy.result_buf, Fuzzy.ns_id, 0, -1)
-- highlight selection
local rel_cursor = math.min(Fuzzy.cursor - Fuzzy.scroll, #display)
vim.api.nvim_buf_set_extmark(Fuzzy.result_buf, Fuzzy.ns_id, rel_cursor - 1, 0, {
end_line = rel_cursor,
hl_group = 'Visual',
hl_eol = true,
})
-- highlight query matches
if query and query ~= '' then
local pattern = vim.pesc(query)
for i, line in ipairs(display) do
for s, e in line:gmatch('()' .. pattern .. '()') do
vim.api.nvim_buf_set_extmark(Fuzzy.result_buf, Fuzzy.ns_id, i - 1, s - 1, {
end_col = e - 1,
hl_group = 'Search',
})
end
end
end
end
local function run_grep(query)
if query == '' then
vim.api.nvim_buf_set_lines(Fuzzy.result_buf, 0, -1, false, { '-- type to search --' })
return
end
local handle = io.popen('rg --vimgrep --hidden --smart-case ' .. vim.fn.shellescape(query))
if not handle then
return
end
local result = handle:read('*a')
handle:close()
Fuzzy.matches = vim.split(result, '\n', { trimempty = true })
Fuzzy.cursor, Fuzzy.scroll = 1, 0
render_results(query)
end
vim.api.nvim_create_autocmd({ 'TextChangedI', 'TextChangedP' }, {
buffer = Fuzzy.input_buf,
callback = function()
local text = vim.fn.getline('.'):gsub('^Grep:%s*', '')
run_grep(text)
end,
})
vim.keymap.set('i', '<C-n>', function()
if Fuzzy.cursor < #Fuzzy.matches then
Fuzzy.cursor = Fuzzy.cursor + 1
if Fuzzy.cursor > Fuzzy.scroll + Fuzzy.page_size then
Fuzzy.scroll = Fuzzy.scroll + 1
end
local query = vim.fn.getline('.'):gsub('^Grep:%s*', '')
render_results(query)
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<C-p>', function()
if Fuzzy.cursor > 1 then
Fuzzy.cursor = Fuzzy.cursor - 1
if Fuzzy.cursor <= Fuzzy.scroll then
Fuzzy.scroll = math.max(Fuzzy.scroll - 1, 0)
end
local query = vim.fn.getline('.'):gsub('^Grep:%s*', '')
render_results(query)
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<CR>', function()
local line = Fuzzy.matches[Fuzzy.cursor]
if line then
local parts = vim.split(line, ':')
local file, lnum = parts[1], tonumber(parts[2]) or 1
Fuzzy.close()
vim.cmd.edit(vim.fn.fnameescape(file))
vim.api.nvim_win_set_cursor(0, { lnum, 0 })
end
end, { buffer = Fuzzy.input_buf })
vim.keymap.set('i', '<Esc>', function()
Fuzzy.close()
end, { buffer = Fuzzy.input_buf })
vim.cmd.startinsert()
end
--------------------------------------------------------------------
-- 🧩 Commands & Keymaps
--------------------------------------------------------------------
vim.api.nvim_create_user_command('FuzzyLive', function()
Fuzzy.open()
end, {})
vim.api.nvim_create_user_command('FuzzyGrep', function()
Fuzzy.open_grep()
end, {})
vim.keymap.set('n', '<leader>f', function()
vim.cmd.FuzzyLive()
end, { desc = 'Open fuzzy file finder' })
vim.keymap.set('n', '<leader>g', function()
vim.cmd.FuzzyGrep()
end, { desc = 'Search file contents with ripgrep' })
return Fuzzy

View File

@@ -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

View File

@@ -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,
},
},
}

83
lua/plugins/tabline.lua Normal file
View File

@@ -0,0 +1,83 @@
local M = {}
-- Helper to get label for each tab page
local function tab_label(n)
local buflist = vim.fn.tabpagebuflist(n)
local winnr = vim.fn.tabpagewinnr(n)
local bufname = vim.fn.bufname(buflist[winnr])
if bufname == '' then
bufname = '[No Name]'
else
bufname = vim.fn.fnamemodify(bufname, ':t') -- tail only
end
-- Determine window count reliably
local win_count = vim.fn.tabpagewinnr(n, '$')
-- Check if any buffer in the tab is modified
local modified = false
for _, buf in ipairs(buflist) do
if vim.fn.getbufvar(buf, '&modified') == 1 then
modified = true
break
end
end
-- Construct label according to rules:
-- single clean: File
-- single edited: +:File
-- multi clean: 2:File
-- multi edited: 2+:File
if win_count == 1 then
if modified then
bufname = '+:' .. bufname
end
else
local prefix = tostring(win_count)
if modified then
prefix = prefix .. '+'
end
bufname = prefix .. ':' .. bufname
end
-- Truncate overly long names
if #bufname > 20 then
bufname = bufname:sub(1, 17) .. ''
end
return bufname
end
-- Main tabline builder
function M.tabline()
local s = ''
local num_tabs = vim.fn.tabpagenr('$')
for i = 1, num_tabs do
-- Highlight current tab
if i == vim.fn.tabpagenr() then
s = s .. '%#TabLineSel#'
else
s = s .. '%#TabLine#'
end
-- Mouse click target
s = s .. '%' .. i .. 'T'
-- Label
s = s .. ' ' .. tab_label(i) .. ' '
end
-- Fill empty space
s = s .. '%#TabLineFill#%T'
return s
end
function M.setup()
vim.opt.showtabline = 1
vim.opt.tabline = "%!v:lua.require'plugins.tabline'.tabline()"
end
return M