diff --git a/init.lua b/init.lua index 539e0b4..0903a54 100644 --- a/init.lua +++ b/init.lua @@ -347,235 +347,19 @@ vim.api.nvim_create_autocmd('TextYankPost', { require('kanagawa').setup { transparent = true } vim.cmd [[colorscheme kanagawa]] --- Custom native-looking tabline that matches Kanagawa theme +-- Custom native-looking tabline vim.o.showtabline = 1 -_G.tab_offset = 1 -local max_visible_tabs = 8 -- maximum number of tabs visible at once - -function _G.Tabline() - local current_tab = vim.fn.tabpagenr() - local tab_count = vim.fn.tabpagenr '$' - local win_width = vim.o.columns - local tab_width = math.floor(win_width / max_visible_tabs) -- FIXED width - - local tabs = {} - for i = 1, tab_count do - local buflist = vim.fn.tabpagebuflist(i) - local winnr = vim.fn.tabpagewinnr(i) - local bufname = vim.fn.bufname(buflist[winnr]) - if bufname == '' then - bufname = '[No Name]' - end - local modified = vim.fn.getbufvar(buflist[winnr], '&mod') == 1 and ' ●' or '' - local label = i .. ': ' .. vim.fn.fnamemodify(bufname, ':t') .. modified - - -- truncate/pad to fixed tab_width - if #label > tab_width - 2 then - label = label:sub(1, tab_width - 3) .. '…' - end - label = label .. string.rep(' ', tab_width - #label) - - tabs[i] = (i == current_tab and '%#TabLineSel#' or '%#TabLine#') .. label - end - - -- scrolling logic to keep active tab visible - local start_index = _G.tab_offset - if current_tab < start_index then - start_index = current_tab - elseif current_tab >= start_index + max_visible_tabs then - start_index = current_tab - max_visible_tabs + 1 - end - _G.tab_offset = start_index - - -- select visible tabs - local visible_tabs = {} - for i = start_index, math.min(start_index + max_visible_tabs - 1, tab_count) do - table.insert(visible_tabs, tabs[i]) - end - - -- scrolling arrows - local left_arrow = start_index > 1 and '< ' or ' ' - local right_arrow = start_index + max_visible_tabs - 1 < tab_count and ' >' or ' ' - - return '%#TabLine#' .. left_arrow .. table.concat(visible_tabs, '') .. right_arrow .. '%#TabLineFill#' -end - -vim.o.tabline = '%!v:lua.Tabline()' - --- adjust offset if tabs are closed -vim.api.nvim_create_autocmd({ 'TabClosed' }, { - callback = function() - local tab_count = vim.fn.tabpagenr '$' - if _G.tab_offset > tab_count then - _G.tab_offset = math.max(tab_count - max_visible_tabs + 1, 1) - end - end, -}) +local custom_tabline = require 'custom_tabline' +vim.o.tabline = '%!v:lua.require("custom_tabline").tabline()' -- Auto-create config files for formatters (cross-platform) -local uv = vim.loop -local home = uv.os_homedir() -local os_name = uv.os_uname().sysname -local appdata = os.getenv 'APPDATA' or (home .. '/AppData/Roaming') +require('formatters_auto_config').setup() --- List of formatter configs -local formatters = { - { - name = 'clang-format', - path = (os_name:match 'Windows' and home .. '\\.clang-format' or home .. '/.clang-format'), - content = [[ -BasedOnStyle: LLVM -IndentWidth: 4 -TabWidth: 4 -UseTab: Always -]], - }, - { - name = 'prettier', - path = (os_name:match 'Windows' and appdata .. '\\Prettier\\.prettierrc' or home .. '/.prettierrc'), - content = [[ -{ - "tabWidth": 4, - "useTabs": true, - "semi": true, - "singleQuote": true, - "trailingComma": "es5", - "printWidth": 100 -} -]], - }, - { - name = 'yapf', - path = (os_name:match 'Windows' and home .. '\\.style.yapf' or home .. '/.style.yapf'), - content = [[ -[style] -based_on_style = pep8 -indent_width = 4 -use_tabs = True -]], - }, - { - name = 'isort', - path = (os_name:match 'Windows' and home .. '\\.isort.cfg' or home .. '/.isort.cfg'), - content = [[ -[settings] -profile = black -force_single_line = true -]], - }, -} - --- Helper to create file if it doesn't exist -local function ensure_file(path, content) - if not path then - print 'Invalid path, skipping file creation' - return - end - - local stat = uv.fs_stat(path) - if not stat then - -- Make parent directory if needed - local dir = vim.fn.fnamemodify(path, ':h') - if vim.fn.isdirectory(dir) == 0 then - vim.fn.mkdir(dir, 'p') -- recursively create directories - print('Created directory: ' .. dir) - end - - -- Write the file - local fd = uv.fs_open(path, 'w', 420) -- 0644 - if fd then - uv.fs_write(fd, content, -1) - uv.fs_close(fd) - print('Created file: ' .. path) - else - print('Failed to create file: ' .. path) - end - end -end - --- Loop through all formatter configs -for _, fmt in ipairs(formatters) do - ensure_file(fmt.path, fmt.content) -end - --- Auto-clear messages on most user actions -local clear_msg_grp = vim.api.nvim_create_augroup('AutoClearMessages', { clear = true }) - -vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertEnter', 'InsertLeave', 'TextChanged', 'TextChangedI' }, { - group = clear_msg_grp, - callback = function() - vim.cmd 'echo ""' -- Clear the message/command line - end, -}) - --- Auto-clear messages on pressing ESC -vim.keymap.set('n', '', 'echo ""', { noremap = true, silent = true }) -vim.keymap.set('v', '', 'echo ""', { noremap = true, silent = true }) +-- Auto-clear command line messages on most user actions and on pressing ESC +require('message_auto_clear').setup() -- When a file is deleted externally, rename all its buffers to "[file]: file removed" --- List of buffer names or filetypes to skip (UndoTree, Neo-tree, etc.) -local skip_buffers = { 'undotree', 'neo-tree' } --- Helper function to determine if a buffer should be skipped in future -local function should_skip(buf) - if not vim.api.nvim_buf_is_valid(buf) then - return true - end - local ft = vim.api.nvim_buf_get_option(buf, 'filetype') - local bufname = vim.api.nvim_buf_get_name(buf) - if bufname == '' then - return true - end - for _, v in ipairs(skip_buffers) do - if ft == v or bufname:match(v) then - return true - end - end - return false -end -vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost' }, { - callback = function() - local current_buf = vim.api.nvim_get_current_buf() - if should_skip(current_buf) then - return - end - local bufname = vim.api.nvim_buf_get_name(current_buf) - -- If this file no longer exists, mark all buffers showing it - if vim.fn.filereadable(bufname) == 0 then - local filename = vim.fn.fnamemodify(bufname, ':t') - local new_name = string.format('[%s]: file removed', filename) - for _, buf in ipairs(vim.api.nvim_list_bufs()) do - if vim.api.nvim_buf_is_valid(buf) then - local name = vim.api.nvim_buf_get_name(buf) - if name == bufname and not name:match 'file removed' then - -- Temporarily unlist so renaming works cleanly - vim.api.nvim_buf_set_option(buf, 'buflisted', false) - vim.api.nvim_buf_set_name(buf, new_name) - vim.api.nvim_buf_set_option(buf, 'buflisted', true) - end - end - end - end - end, -}) --- Automatically wipe "file removed" buffers when they are closed -vim.api.nvim_create_autocmd('BufWinLeave', { - callback = function(args) - local buf = args.buf - if should_skip(buf) then - return - end - local bufname = vim.api.nvim_buf_get_name(buf) - -- If the buffer was marked for wipe or name indicates it's deleted - local marked = pcall(vim.api.nvim_buf_get_var, buf, 'marked_for_wipe') and vim.api.nvim_buf_get_var(buf, 'marked_for_wipe') - if marked or (bufname ~= '' and bufname:match 'file removed') then - vim.schedule(function() - if vim.api.nvim_buf_is_valid(buf) then - vim.cmd('bwipeout! ' .. buf) - end - end) - end - end, -}) +require 'buffer_deleted' -- The line beneath this is called `modeline`. See `:help modeline` -- vim: ts=2 sts=2 sw=2 et diff --git a/lua/buffer_deleted.lua b/lua/buffer_deleted.lua new file mode 100644 index 0000000..1978f49 --- /dev/null +++ b/lua/buffer_deleted.lua @@ -0,0 +1,73 @@ +-- When a file is deleted externally, rename all its buffers to "[file]: file removed" + +local M = {} + +-- List of buffer names or filetypes to skip (UndoTree, Neo-tree, etc.) +local skip_buffers = { 'undotree', 'neo-tree' } + +-- Helper function to determine if a buffer should be skipped in future +local function should_skip(buf) + if not vim.api.nvim_buf_is_valid(buf) then + return true + end + local ft = vim.api.nvim_buf_get_option(buf, 'filetype') + local bufname = vim.api.nvim_buf_get_name(buf) + if bufname == '' then + return true + end + for _, v in ipairs(skip_buffers) do + if ft == v or bufname:match(v) then + return true + end + end + return false +end + +-- Detect deleted files and rename their buffers +vim.api.nvim_create_autocmd({ 'BufEnter', 'BufWritePost' }, { + callback = function() + local current_buf = vim.api.nvim_get_current_buf() + if should_skip(current_buf) then + return + end + local bufname = vim.api.nvim_buf_get_name(current_buf) + -- If this file no longer exists, mark all buffers showing it + if vim.fn.filereadable(bufname) == 0 then + local filename = vim.fn.fnamemodify(bufname, ':t') + local new_name = string.format('[%s]: file removed', filename) + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_valid(buf) then + local name = vim.api.nvim_buf_get_name(buf) + if name == bufname and not name:match 'file removed' then + -- Temporarily unlist so renaming works cleanly + vim.api.nvim_buf_set_option(buf, 'buflisted', false) + vim.api.nvim_buf_set_name(buf, new_name) + vim.api.nvim_buf_set_option(buf, 'buflisted', true) + end + end + end + end + end, +}) + +-- Automatically wipe "file removed" buffers when they are closed +vim.api.nvim_create_autocmd('BufWinLeave', { + callback = function(args) + local buf = args.buf + if should_skip(buf) then + return + end + local bufname = vim.api.nvim_buf_get_name(buf) + -- If the buffer was marked for wipe or name indicates it's deleted + local marked = pcall(vim.api.nvim_buf_get_var, buf, 'marked_for_wipe') and vim.api.nvim_buf_get_var(buf, 'marked_for_wipe') + if marked or (bufname ~= '' and bufname:match 'file removed') then + vim.schedule(function() + if vim.api.nvim_buf_is_valid(buf) then + vim.cmd('bwipeout! ' .. buf) + end + end) + end + end, +}) + +return M diff --git a/lua/custom_tabline.lua b/lua/custom_tabline.lua new file mode 100644 index 0000000..6e271f4 --- /dev/null +++ b/lua/custom_tabline.lua @@ -0,0 +1,66 @@ +-- Custom native-looking tabline that matches Kanagawa theme +-- This module creates a fixed-width, scrollable tabline that +local M = {} + +local max_visible_tabs = 8 -- maximum number of tabs visible at once +local tab_offset = 1 + +function M.tabline() + local current_tab = vim.fn.tabpagenr() + local tab_count = vim.fn.tabpagenr '$' + local win_width = vim.o.columns + local tab_width = math.floor(win_width / max_visible_tabs) -- FIXED width + + local tabs = {} + for i = 1, tab_count do + local buflist = vim.fn.tabpagebuflist(i) + local winnr = vim.fn.tabpagewinnr(i) + local bufname = vim.fn.bufname(buflist[winnr]) + if bufname == '' then + bufname = '[No Name]' + end + local modified = vim.fn.getbufvar(buflist[winnr], '&mod') == 1 and ' ●' or '' + local label = i .. ': ' .. vim.fn.fnamemodify(bufname, ':t') .. modified + + -- truncate/pad to fixed tab_width + if #label > tab_width - 2 then + label = label:sub(1, tab_width - 3) .. '…' + end + label = label .. string.rep(' ', tab_width - #label) + + tabs[i] = (i == current_tab and '%#TabLineSel#' or '%#TabLine#') .. label + end + + -- scrolling logic to keep active tab visible + local start_index = tab_offset + if current_tab < start_index then + start_index = current_tab + elseif current_tab >= start_index + max_visible_tabs then + start_index = current_tab - max_visible_tabs + 1 + end + tab_offset = start_index + + -- select visible tabs + local visible_tabs = {} + for i = start_index, math.min(start_index + max_visible_tabs - 1, tab_count) do + table.insert(visible_tabs, tabs[i]) + end + + -- scrolling arrows + local left_arrow = start_index > 1 and '< ' or ' ' + local right_arrow = start_index + max_visible_tabs - 1 < tab_count and ' >' or ' ' + + return '%#TabLine#' .. left_arrow .. table.concat(visible_tabs, '') .. right_arrow .. '%#TabLineFill#' +end + +-- adjust offset if tabs are closed +vim.api.nvim_create_autocmd({ 'TabClosed' }, { + callback = function() + local tab_count = vim.fn.tabpagenr '$' + if tab_offset > tab_count then + tab_offset = math.max(tab_count - max_visible_tabs + 1, 1) + end + end, +}) + +return M diff --git a/lua/formatters_auto_config.lua b/lua/formatters_auto_config.lua new file mode 100644 index 0000000..2719198 --- /dev/null +++ b/lua/formatters_auto_config.lua @@ -0,0 +1,93 @@ +-- Auto-create config files for formatters (cross-platform) + +local M = {} +local uv = vim.loop + +-- Determine OS paths +local home = uv.os_homedir() +local os_name = uv.os_uname().sysname +local appdata = os.getenv 'APPDATA' or (home .. '/AppData/Roaming') + +-- List of formatter configs +local formatters = { + { + name = 'clang-format', + path = (os_name:match 'Windows' and home .. '\\.clang-format' or home .. '/.clang-format'), + content = [[ +BasedOnStyle: LLVM +IndentWidth: 4 +TabWidth: 4 +UseTab: Always +]], + }, + { + name = 'prettier', + path = (os_name:match 'Windows' and appdata .. '\\Prettier\\.prettierrc' or home .. '/.prettierrc'), + content = [[ +{ + "tabWidth": 4, + "useTabs": true, + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100 +} +]], + }, + { + name = 'yapf', + path = (os_name:match 'Windows' and home .. '\\.style.yapf' or home .. '/.style.yapf'), + content = [[ +[style] +based_on_style = pep8 +indent_width = 4 +use_tabs = True +]], + }, + { + name = 'isort', + path = (os_name:match 'Windows' and home .. '\\.isort.cfg' or home .. '/.isort.cfg'), + content = [[ +[settings] +profile = black +force_single_line = true +]], + }, +} + +-- Helper function to create a file if it doesn't exist +local function ensure_file(path, content) + if not path then + print 'Invalid path, skipping file creation' + return + end + + local stat = uv.fs_stat(path) + if not stat then + -- Make parent directory if needed + local dir = vim.fn.fnamemodify(path, ':h') + if vim.fn.isdirectory(dir) == 0 then + vim.fn.mkdir(dir, 'p') -- recursively create directories + print('Created directory: ' .. dir) + end + + -- Write the file + local fd = uv.fs_open(path, 'w', 420) -- 0644 + if fd then + uv.fs_write(fd, content, -1) + uv.fs_close(fd) + print('Created file: ' .. path) + else + print('Failed to create file: ' .. path) + end + end +end + +-- Function to run the auto-creation for all formatters +function M.setup() + for _, fmt in ipairs(formatters) do + ensure_file(fmt.path, fmt.content) + end +end + +return M diff --git a/lua/message_auto_clear.lua b/lua/message_auto_clear.lua new file mode 100644 index 0000000..d0f6e02 --- /dev/null +++ b/lua/message_auto_clear.lua @@ -0,0 +1,22 @@ +-- Auto-clear command line messages on most user actions and on pressing ESC + +local M = {} + +function M.setup() + -- Create an augroup for auto-clearing messages + local clear_msg_grp = vim.api.nvim_create_augroup('AutoClearMessages', { clear = true }) + + -- Clear messages on common user actions + vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertEnter', 'InsertLeave', 'TextChanged', 'TextChangedI' }, { + group = clear_msg_grp, + callback = function() + vim.cmd 'echo ""' -- Clear the message/command line + end, + }) + + -- Clear messages when pressing ESC in normal and visual modes + vim.keymap.set('n', '', 'echo ""', { noremap = true, silent = true }) + vim.keymap.set('v', '', 'echo ""', { noremap = true, silent = true }) +end + +return M