diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..38594f8 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Bash(grep -rn \"nvim_exec_autocmds\\\\|User.*ClaudeCode\\\\|ClaudeCode.*User\" /home/user/.local/share/nvim/lazy/claudecode.nvim/ --include=\"*.lua\" 2>/dev/null | grep -v test | grep -v spec)", + "Bash(grep:*)", + "Bash(python3:*)" + ] + } +} diff --git a/lazy-lock.json b/lazy-lock.json new file mode 100644 index 0000000..3d07c36 --- /dev/null +++ b/lazy-lock.json @@ -0,0 +1,45 @@ +{ + "CopilotChat.nvim": { "branch": "main", "commit": "ecea24a9a067c6f176b8e13eb11251c739caf055" }, + "LazyVim": { "branch": "main", "commit": "fca0af57cc3851b14f96a795a9c9bfafc5096dd1" }, + "blink-copilot": { "branch": "main", "commit": "7ad8209b2f880a2840c94cdcd80ab4dc511d4f39" }, + "blink.cmp": { "branch": "main", "commit": "4b18c32adef2898f95cdef6192cbd5796c1a332d" }, + "bufferline.nvim": { "branch": "main", "commit": "655133c3b4c3e5e05ec549b9f8cc2894ac6f51b3" }, + "catppuccin": { "branch": "main", "commit": "12c004cde3f36cb1d57242f1e6aac46b09a0e5b4" }, + "claudecode.nvim": { "branch": "main", "commit": "aa9a5cebebdbfa449c1c5ff229ba5d98e66bafed" }, + "conform.nvim": { "branch": "master", "commit": "086a40dc7ed8242c03be9f47fbcee68699cc2395" }, + "copilot.lua": { "branch": "master", "commit": "00446a63cba4cc59bb24fc1e210a555a3e4acdfb" }, + "flash.nvim": { "branch": "main", "commit": "fcea7ff883235d9024dc41e638f164a450c14ca2" }, + "friendly-snippets": { "branch": "main", "commit": "6cd7280adead7f586db6fccbd15d2cac7e2188b9" }, + "gitsigns.nvim": { "branch": "main", "commit": "9f3c6dd7868bcc116e9c1c1929ce063b978fa519" }, + "grug-far.nvim": { "branch": "main", "commit": "ac52ee2d87399dfd7b0d59d5b1d2bbaf2a4028f1" }, + "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, + "lazydev.nvim": { "branch": "main", "commit": "5231c62aa83c2f8dc8e7ba957aa77098cda1257d" }, + "lualine.nvim": { "branch": "master", "commit": "47f91c416daef12db467145e16bed5bbfe00add8" }, + "mason-lspconfig.nvim": { "branch": "main", "commit": "a324581a3c83fdacdb9804b79de1cbe00ce18550" }, + "mason.nvim": { "branch": "main", "commit": "44d1e90e1f66e077268191e3ee9d2ac97cc18e65" }, + "mini.ai": { "branch": "main", "commit": "4b0a6207341d895b6cfe9bcb1e4d3e8607bfe4f4" }, + "mini.icons": { "branch": "main", "commit": "5b9076dae1bfbe47ba4a14bc8b967cde0ab5d77e" }, + "mini.pairs": { "branch": "main", "commit": "b7fde3719340946feb75017ef9d75edebdeb0566" }, + "mini.surround": { "branch": "main", "commit": "d205d1741d1fcc1f3117b4e839bf00f74ad72fa2" }, + "noice.nvim": { "branch": "main", "commit": "7bfd942445fb63089b59f97ca487d605e715f155" }, + "nui.nvim": { "branch": "main", "commit": "de740991c12411b663994b2860f1a4fd0937c130" }, + "nvim-lint": { "branch": "master", "commit": "606b823a57b027502a9ae00978ebf4f5d5158098" }, + "nvim-lspconfig": { "branch": "master", "commit": "ead0f5f342d8d323441e7d4b88f0fc436a81ad5f" }, + "nvim-osc52": { "branch": "main", "commit": "04cfaba1865ae5c53b6f887c3ca7304973824fb2" }, + "nvim-treesitter": { "branch": "main", "commit": "5cb05e1b0fa3c469958a2b26f36b3fe930af221c" }, + "nvim-treesitter-textobjects": { "branch": "main", "commit": "4e91b5d0394329a229725b021a8ea217099826ef" }, + "nvim-ts-autotag": { "branch": "main", "commit": "8e1c0a389f20bf7f5b0dd0e00306c1247bda2595" }, + "persistence.nvim": { "branch": "main", "commit": "b20b2a7887bd39c1a356980b45e03250f3dce49c" }, + "plenary.nvim": { "branch": "master", "commit": "b9fd5226c2f76c951fc8ed5923d85e4de065e509" }, + "precognition.nvim": { "branch": "main", "commit": "06e4bfa339ddc55a49fd1adcbb403f6e0855c43b" }, + "render-markdown.nvim": { "branch": "main", "commit": "1641b434bda26e0f4e3610985b3357fc213cf834" }, + "snacks.nvim": { "branch": "main", "commit": "9912042fc8bca2209105526ac7534e9a0c2071b2" }, + "todo-comments.nvim": { "branch": "main", "commit": "31e3c38ce9b29781e4422fc0322eb0a21f4e8668" }, + "toggleterm.nvim": { "branch": "main", "commit": "50ea089fc548917cc3cc16b46a8211833b9e3c7c" }, + "tokyonight.nvim": { "branch": "main", "commit": "5da1b76e64daf4c5d410f06bcb6b9cb640da7dfd" }, + "transparent.nvim": { "branch": "main", "commit": "8ac59883de84e9cd1850ea25cf087031c5ba7d54" }, + "trouble.nvim": { "branch": "main", "commit": "bd67efe408d4816e25e8491cc5ad4088e708a69a" }, + "ts-comments.nvim": { "branch": "main", "commit": "123a9fb12e7229342f807ec9e6de478b1102b041" }, + "typescript-tools.nvim": { "branch": "master", "commit": "c2f5910074103705661e9651aa841e0d7eea9932" }, + "which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" } +} diff --git a/lazyvim.json b/lazyvim.json new file mode 100644 index 0000000..0a28d20 --- /dev/null +++ b/lazyvim.json @@ -0,0 +1,14 @@ +{ + "extras": [ + "lazyvim.plugins.extras.ai.claudecode", + "lazyvim.plugins.extras.ai.copilot", + "lazyvim.plugins.extras.ai.copilot-chat", + "lazyvim.plugins.extras.coding.mini-surround", + "lazyvim.plugins.extras.lang.markdown" + ], + "install_version": 8, + "news": { + "NEWS.md": "11866" + }, + "version": 8 +} \ No newline at end of file diff --git a/lua/config/autocmds.lua b/lua/config/autocmds.lua index 4221e75..f1a5381 100644 --- a/lua/config/autocmds.lua +++ b/lua/config/autocmds.lua @@ -6,3 +6,10 @@ -- -- Or remove existing autocmds by their group name (which is prefixed with `lazyvim_` for the defaults) -- e.g. vim.api.nvim_del_augroup_by_name("lazyvim_wrap_spell") +vim.api.nvim_create_autocmd("BufWritePre", { + pattern = { "*.tsx", "*.ts" }, + callback = function() + local tstools_api = require("typescript-tools.api") + tstools_api.remove_unused_imports(true) -- true = sync + end, +}) diff --git a/lua/config/keymaps.lua b/lua/config/keymaps.lua index 2c134f7..f92611a 100644 --- a/lua/config/keymaps.lua +++ b/lua/config/keymaps.lua @@ -1,3 +1,24 @@ -- Keymaps are automatically loaded on the VeryLazy event -- Default keymaps that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/keymaps.lua -- Add any additional keymaps here + +---- Move lines +vim.keymap.set("v", "J", ":m '>+1gv=gv") +vim.keymap.set("v", "K", ":m '<-2gv=gv") + +-- Cursor in middle when jumping +vim.keymap.set("n", "", "zz") +vim.keymap.set("n", "", "zz") +vim.keymap.set("n", "n", "nzzzv") +vim.keymap.set("n", "N", "Nzzzv") + +-- Precognition +vim.keymap.set("n", "tp", require("precognition").toggle) + +-- set the file picker to always open the cwd instead of root +vim.keymap.set("n", "", function() + require("lazyvim.util").pick("files", { root = false })() +end, { desc = "Find Files (cwd)" }) + +-- Markdown task management +require("config.markdown-tasks").setup() diff --git a/lua/config/lazy.lua b/lua/config/lazy.lua index d73bfa1..9bbdf5f 100644 --- a/lua/config/lazy.lua +++ b/lua/config/lazy.lua @@ -18,6 +18,13 @@ require("lazy").setup({ spec = { -- add LazyVim and import its plugins { "LazyVim/LazyVim", import = "lazyvim.plugins" }, + + -- language extras + { import = "lazyvim.plugins.extras.lang.typescript" }, + + -- formatting extras + { import = "lazyvim.plugins.extras.formatting.prettier" }, + -- import/override with your plugins { import = "plugins" }, }, diff --git a/lua/config/markdown-tasks.lua b/lua/config/markdown-tasks.lua new file mode 100644 index 0000000..02a0133 --- /dev/null +++ b/lua/config/markdown-tasks.lua @@ -0,0 +1,500 @@ +-- Markdown task management +-- Keymaps are registered only in markdown buffers via FileType autocmd. +-- +-- File structure expected: +-- # TODO Lists +-- ## General Section πŸ“œ 1/2 πŸ“œ +-- - [ ] undated task +-- - [x] done task +-- ## DD-MM-YYYY ⏳ 0/2 ⏳ (date sections sorted newest first) +-- - [ ] task +-- - [ ] another task +-- +-- Section stat icons (auto-updated on toggle and InsertLeave): +-- βœ… all tasks done (any section) +-- 🚨 today, 0 done, β‰₯2 total +-- ⏳ today (default) +-- πŸ“… future date +-- ❌ past date +-- πŸ“œ general (non-date heading) +-- +-- Features: +-- Toggle cascades to children; unchecking a child unchecks parent, +-- checking last sibling checks parent. Spacing is auto-normalized +-- (blank lines around headings, none between sibling tasks). +-- Tasks can be moved between date sections and a general section. + +local M = {} + +-- ── Core task operations ───────────────────────────────────────────────────── + +local function new_task() + local line = vim.api.nvim_get_current_line() + if line:match("^%s*$") then + vim.api.nvim_set_current_line("- [ ] ") + vim.cmd("startinsert!") + else + vim.cmd("normal! o- [ ] ") + vim.cmd("startinsert!") + end +end + +local function toggle_task() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = lines[row] + if not line:find("%[[ x]%]") then return end + + -- Toggle current line + local new_state + if line:find("%[ %]") then + lines[row] = line:gsub("%[ %]", "[x]", 1) + new_state = "[x]" + else + lines[row] = line:gsub("%[x%]", "[ ]", 1) + new_state = "[ ]" + end + + local cur_indent = #(line:match("^(%s*)") or "") + + -- Cascade down: set all deeper-indented children to same state + for i = row + 1, #lines do + local l = lines[i] + if not l:match("^%s*%-") then break end + local indent = #(l:match("^(%s*)") or "") + if indent <= cur_indent then break end + if new_state == "[x]" then + lines[i] = l:gsub("%[ %]", "[x]", 1) + else + lines[i] = l:gsub("%[x%]", "[ ]", 1) + end + end + + -- Cascade up: find parent (less indent, is a task) + local parent_row + for i = row - 1, 1, -1 do + local l = lines[i] + if not l:match("^%s*%-") then break end + local indent = #(l:match("^(%s*)") or "") + if indent < cur_indent and l:find("%[[ x]%]") then + parent_row = i + break + end + end + + if parent_row then + if new_state == "[ ]" and lines[parent_row]:find("%[x%]") then + -- Unchecking child: uncheck parent + lines[parent_row] = lines[parent_row]:gsub("%[x%]", "[ ]", 1) + elseif new_state == "[x]" then + -- Checking child: check parent if all siblings are now done + local parent_indent = #(lines[parent_row]:match("^(%s*)") or "") + local all_done = true + for i = parent_row + 1, #lines do + local l = lines[i] + if not l:match("^%s*%-") then break end + local indent = #(l:match("^(%s*)") or "") + if indent <= parent_indent then break end + if indent == cur_indent and l:find("%[ %]") then + all_done = false + break + end + end + if all_done then + lines[parent_row] = lines[parent_row]:gsub("%[ %]", "[x]", 1) + end + end + end + + vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + vim.api.nvim_win_set_cursor(0, { row, vim.api.nvim_win_get_cursor(0)[2] }) + local update = require("config.markdown-tasks")._update_stats + if update then update() end +end + +local function toggle_task_format() + local line = vim.api.nvim_get_current_line() + if line:find("^%s*%- %[.%] ") then + -- "- [ ] text" β†’ "- text" + vim.api.nvim_set_current_line((line:gsub("^(%s*)%- %[.%] ", "%1- ", 1))) + elseif line:find("^%s*%- ") then + -- "- text" β†’ "- [ ] text" + vim.api.nvim_set_current_line((line:gsub("^(%s*)%- ", "%1- [ ] ", 1))) + end +end + +local function jump_next_task() vim.fn.search("- \\[ \\]", "W") end +local function jump_prev_task() vim.fn.search("- \\[ \\]", "bW") end + +local function list_tasks_in_file() + require("snacks").picker.grep({ search = "- \\[ \\]", buffers = true }) +end + +local function search_all_tasks() + require("snacks").picker.grep({ search = "- \\[ \\]" }) +end + +-- ── Section task stats ──────────────────────────────────────────────────────── + +-- Parses a "DD-MM-YYYY" string into a Unix timestamp for comparison. +local function date_to_time(s) + local d, m, y = s:match("(%d+)-(%d+)-(%d+)") + if not d then return 0 end + return os.time({ year = tonumber(y), month = tonumber(m), day = tonumber(d) }) +end + +-- Strips the stats suffix from a heading line, returning the bare heading. +local function strip_stats(line) + return (line + :gsub("%s+βœ… ?%d+/%d+ ?βœ…%s*$", "") + :gsub("%s+❌ ?%d+/%d+ ?❌%s*$", "") + :gsub("%s+πŸ“œ ?%d+/%d+ ?πŸ“œ%s*$", "") + :gsub("%s+⏳ ?%d+/%d+ ?⏳%s*$", "") + :gsub("%s+πŸ“… ?%d+/%d+ ?πŸ“…%s*$", "") + :gsub("%s+🚨 ?%d+/%d+ ?🚨%s*$", "")) +end + +-- Counts [x] and [ ] tasks in a section starting at header_line (1-indexed). +local function count_section_tasks(lines, header_line) + local done, todo = 0, 0 + for i = header_line + 1, #lines do + if lines[i]:match("^## ") then break end + if lines[i]:match("^%s*%- %[x%]") then + done = done + 1 + elseif lines[i]:match("^%s*%- %[ %]") then + todo = todo + 1 + end + end + return done, todo +end + +local function classify_section(base) + local date_str = base:match("^## (%d%d%-%d%d%-%d%d%d%d)$") + if not date_str then return "general" end + local section_time = date_to_time(date_str) + local today_start = date_to_time(os.date("%d-%m-%Y")) + if section_time == today_start then return "today" end + return section_time > today_start and "future" or "past" +end + +local section_icons = { + general = "πŸ“œ", today = "⏳", future = "πŸ“…", past = "❌", +} + +-- Updates every ## heading with a live task count suffix. +local function update_section_stats() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local changed = false + for i, line in ipairs(lines) do + if line:match("^## ") then + local base = strip_stats(line) + local done, todo = count_section_tasks(lines, i) + local total = done + todo + local kind = classify_section(base) + local icon + if total > 0 and done == total then + icon = "βœ…" + elseif kind == "today" and done == 0 and total >= 2 then + icon = "🚨" + else + icon = section_icons[kind] + end + local new_line = base .. " " .. icon .. " " .. done .. "/" .. total .. " " .. icon + if new_line ~= line then + lines[i] = new_line + changed = true + end + end + end + if changed then + local cursor = vim.api.nvim_win_get_cursor(0) + vim.api.nvim_buf_set_lines(0, 0, -1, false, lines) + vim.api.nvim_win_set_cursor(0, cursor) + end +end + +-- Expose so toggle_task can call it without a forward-ref issue. +M._update_stats = update_section_stats + +-- ── Spacing normalizer ─────────────────────────────────────────────────────── + +-- Enforces: one blank line before and after every heading line, no consecutive +-- blank lines, no blank lines between sibling task lines. +local function normalize_spacing() + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + + -- Strip all blank lines first so we can re-insert them deterministically. + local stripped = {} + for _, l in ipairs(lines) do + if not l:match("^%s*$") then table.insert(stripped, l) end + end + + local result = {} + + local function ensure_blank() + if #result > 0 and not result[#result]:match("^%s*$") then + table.insert(result, "") + end + end + + for i, l in ipairs(stripped) do + local is_heading = l:match("^#") + if is_heading and #result > 0 then ensure_blank() end + table.insert(result, l) + if is_heading and i < #stripped then ensure_blank() end + end + + vim.api.nvim_buf_set_lines(0, 0, -1, false, result) + update_section_stats() +end + +-- ── Date helpers ───────────────────────────────────────────────────────────── + +-- Returns date string "DD-MM-YYYY" with an optional day offset. +local function date_string(offset_days) + local t = os.time() + (offset_days or 0) * 86400 + return os.date("%d-%m-%Y", t) +end + +-- Returns 1-indexed line number of the "## DD-MM-YYYY" header, or nil. +local function find_date_section(date_str) + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + for i, line in ipairs(lines) do + if strip_stats(line) == "## " .. date_str then return i end + end +end + +-- Returns the 1-indexed line of the last non-empty content line within a +-- section (from header_line until the next ## header or EOF). +local function section_end(header_line) + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local last = header_line + for i = header_line + 1, #lines do + if lines[i]:match("^## ") then return i - 1 end + if not lines[i]:match("^%s*$") then last = i end + end + return last +end + +-- Inserts a new "## date_str" line in descending date order (newest first). +-- Blank lines are left to normalize_spacing. +local function insert_date_section(date_str) + local new_time = date_to_time(date_str) + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + -- Find the first existing date header that is strictly older β†’ insert before it. + for i, line in ipairs(lines) do + local existing = strip_stats(line):match("^## (%d%d%-%d%d%-%d%d%d%d)$") + if existing and date_to_time(existing) < new_time then + vim.api.nvim_buf_set_lines(0, i - 1, i - 1, false, { "## " .. date_str }) + return + end + end + -- All existing dates are newer (or there are none) β†’ append at end. + vim.api.nvim_buf_set_lines(0, #lines, #lines, false, { "## " .. date_str }) +end + +-- ── Insert today's date header ──────────────────────────────────────────────── + +local function insert_date() + local header = "## " .. date_string(0) + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_get_current_line() + if line:match("^%s*$") then + vim.api.nvim_set_current_line(header) + else + vim.api.nvim_buf_set_lines(0, row, row, false, { header }) + vim.api.nvim_win_set_cursor(0, { row + 1, 0 }) + end + normalize_spacing() +end + + +-- ── Move task helpers ───────────────────────────────────────────────────────── + +-- Removes the task at row (1-indexed), then deletes the owning date header if +-- it is now empty. Returns the task text. +local function remove_task(row) + local line = vim.api.nvim_buf_get_lines(0, row - 1, row, false)[1] + + -- Find the ## header that owns this task before deleting it. + local all = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local src_hdr = nil + for i = row, 1, -1 do + if all[i] and all[i]:match("^##") then + src_hdr = i; break + end + end + + vim.api.nvim_buf_set_lines(0, row - 1, row, false, {}) + + -- If the source was a date section that is now empty, remove its header. + if src_hdr then + local updated = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local hdr = updated[src_hdr] + if hdr and strip_stats(hdr):match("^## %d%d%-%d%d%-%d%d%d%d$") then + local empty = true + for i = src_hdr + 1, #updated do + if updated[i]:match("^## ") then break end + if not updated[i]:match("^%s*$") then + empty = false; break + end + end + if empty then + vim.api.nvim_buf_set_lines(0, src_hdr - 1, src_hdr, false, {}) + end + end + end + + return line +end + +-- ── Move task to a date section ─────────────────────────────────────────────── + +local function move_task_to(date_str) + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_get_current_line() + + if not line:match("^%s*%- %[") then + vim.notify("Not on a task line", vim.log.levels.WARN) + return + end + + remove_task(row) + + local target = find_date_section(date_str) + if not target then + insert_date_section(date_str) + target = find_date_section(date_str) + end + + local insert_at = section_end(target) + vim.api.nvim_buf_set_lines(0, insert_at, insert_at, false, { line }) + + normalize_spacing() +end + +local function move_to_general() + local row = vim.api.nvim_win_get_cursor(0)[1] + local line = vim.api.nvim_get_current_line() + + if not line:match("^%s*%- %[") then + vim.notify("Not on a task line", vim.log.levels.WARN) + return + end + + remove_task(row) + + -- The general section is the first ## heading that isn't a date heading. + local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) + local general = nil + for i, l in ipairs(lines) do + local base = strip_stats(l) + if base:match("^## ") and not base:match("^## %d%d%-%d%d%-%d%d%d%d$") then + general = i; break + end + end + + if not general then + vim.notify("No general section found", vim.log.levels.WARN) + return + end + + local insert_at = section_end(general) + vim.api.nvim_buf_set_lines(0, insert_at, insert_at, false, { line }) + + normalize_spacing() +end + +local function move_to_today() move_task_to(date_string(0)) end +local function move_to_tomorrow() move_task_to(date_string(1)) end +local function move_to_yesterday() move_task_to(date_string(-1)) end + +-- Parses flexible date input into "DD-MM-YYYY". Accepts separators - / or none. +-- If the year is omitted (DDMM or DD-MM or DD/MM), assumes the current year. +local function parse_date_input(input) + if not input or input == "" then return nil end + input = input:match("^%s*(.-)%s*$") -- trim + + local current_year = tonumber(os.date("%Y")) + local d, m, y + + -- With separator: DD-MM-YYYY or DD/MM/YYYY (year optional) + d, m, y = input:match("^(%d%d?)[%-%/](%d%d?)[%-%/](%d%d%d%d)$") + if d then + return string.format("%02d-%02d-%04d", tonumber(d), tonumber(m), tonumber(y)) + end + + d, m = input:match("^(%d%d?)[%-%/](%d%d?)$") + if d then + return string.format("%02d-%02d-%04d", tonumber(d), tonumber(m), current_year) + end + + -- No separator: DDMMYYYY or DDMM + d, m, y = input:match("^(%d%d)(%d%d)(%d%d%d%d)$") + if d then + return string.format("%02d-%02d-%04d", tonumber(d), tonumber(m), tonumber(y)) + end + + d, m = input:match("^(%d%d)(%d%d)$") + if d then + return string.format("%02d-%02d-%04d", tonumber(d), tonumber(m), current_year) + end + + return nil +end + +local function move_to_date() + vim.ui.input({ prompt = "Date (DD-MM-YYYY): " }, function(input) + local parsed = parse_date_input(input) + if parsed then + move_task_to(parsed) + elseif input and input ~= "" then + vim.notify("Unrecognised date: " .. input, vim.log.levels.WARN) + end + end) +end + +-- ── Setup ──────────────────────────────────────────────────────────────────── + +function M.setup() + local group = vim.api.nvim_create_augroup("MarkdownTasks", { clear = true }) + + local function setup_buf(buf) + local o = function(desc) return { buffer = buf, desc = desc } end + vim.keymap.set("n", "mn", new_task, o("New task")) + vim.keymap.set("n", "mx", toggle_task, o("Toggle done")) + vim.keymap.set("n", "mf", toggle_task_format, o("Toggle task format")) + vim.keymap.set("n", "mj", jump_next_task, o("Next task")) + vim.keymap.set("n", "mk", jump_prev_task, o("Prev task")) + vim.keymap.set("n", "ml", list_tasks_in_file, o("List tasks (file)")) + vim.keymap.set("n", "ms", search_all_tasks, o("Search all tasks")) + vim.keymap.set("n", "mt", insert_date, o("Insert date header")) + vim.keymap.set("n", "mmg", move_to_general, o("Move β†’ general")) + vim.keymap.set("n", "mmt", move_to_today, o("Move β†’ today")) + vim.keymap.set("n", "mmT", move_to_tomorrow, o("Move β†’ tomorrow")) + vim.keymap.set("n", "mmy", move_to_yesterday, o("Move β†’ yesterday")) + vim.keymap.set("n", "mmd", move_to_date, o("Move β†’ date…")) + -- Update stats after leaving insert mode (covers new_task completions). + vim.api.nvim_create_autocmd("InsertLeave", { + buffer = buf, + group = group, + callback = update_section_stats, + }) + end + + vim.api.nvim_create_autocmd("FileType", { + pattern = "markdown", + group = group, + callback = function(ev) setup_buf(ev.buf) end, + }) + + -- VeryLazy fires after the initial buffer's FileType event, so apply + -- keymaps to any markdown buffers that are already open. + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + if vim.api.nvim_buf_is_loaded(buf) and vim.bo[buf].filetype == "markdown" then + setup_buf(buf) + end + end +end + +return M diff --git a/lua/config/options.lua b/lua/config/options.lua index 3ea1454..7448d77 100644 --- a/lua/config/options.lua +++ b/lua/config/options.lua @@ -1,3 +1,22 @@ -- Options are automatically loaded before lazy.nvim startup -- Default options that are always set: https://github.com/LazyVim/LazyVim/blob/main/lua/lazyvim/config/options.lua -- Add any additional options here +vim.schedule(function() + vim.o.clipboard = "unnamedplus" +end) + +vim.o.scrolloff = 10 + +-- Semantic highlighting +vim.g.lazyvim_semantic_highlighting = false +vim.g.lazyvim_prettier_needs_config = false + +-- Kill any orphaned tsserver processes when Neovim exits +vim.api.nvim_create_autocmd("ExitPre", { + callback = function() + vim.fn.system("pkill -f 'tsserver.js'") + end, +}) + +-- Leave ai completion to copilot +vim.g.ai_cmp = false diff --git a/lua/plugins/claude-code.lua b/lua/plugins/claude-code.lua new file mode 100644 index 0000000..b18a533 --- /dev/null +++ b/lua/plugins/claude-code.lua @@ -0,0 +1,55 @@ +return { + "coder/claudecode.nvim", + lazy = false, + dependencies = { "folke/snacks.nvim" }, + opts = { + log_level = "info", + focus_after_send = true, + terminal = { + git_repo_cwd = true, + -- split_side = "left", + -- split_width_percentage = 0.25, + -- provider = "toggleterm", -- "snacks" or "toggleterm"... + -- auto_close = true, + snacks_win_opts = { + -- Floating window options + position = "float", + width = 0.85, + height = 0.85, + border = "rounded", + keys = { + hide = { + "jj", + function(self) + self:hide() + end, + mode = "t", + desc = "Hide Claude", + }, + }, + }, + }, + }, + keys = { + { "j", nil, desc = "Claude Code" }, + -- { "", "ClaudeCode", desc = "Toggle Claude" }, -- not working + { "jj", "ClaudeCode", desc = "Toggle Claude" }, + { "jb", "ClaudeCodeAdd %", desc = "Add current buffer" }, + { "js", "ClaudeCodeSend", mode = { "n", "v" }, desc = "Send selection / file" }, + + -- I use snacks_picker_list, not supported yet. Claude Code currently only supports these 5. + -- { + -- "js", + -- "ClaudeCodeTreeAdd", + -- desc = "Add file", + -- ft = { "NvimTree", "neo-tree", "oil", "minifiles", "netrw" }, + -- }, + { "jr", "ClaudeCode --resume", desc = "Resume session" }, + { "jc", "ClaudeCode --continue", desc = "Continue session" }, + { "jm", "ClaudeCodeSelectModel", desc = "Select model" }, + + -- Diff actions + { "ja", "ClaudeCodeDiffAccept", desc = "Accept diff" }, + { "jd", "ClaudeCodeDiffDeny", desc = "Deny diff" }, + }, +} diff --git a/lua/plugins/conform.lua b/lua/plugins/conform.lua new file mode 100644 index 0000000..a1ba45d --- /dev/null +++ b/lua/plugins/conform.lua @@ -0,0 +1,26 @@ +local util = require("conform.util") + +return { + "stevearc/conform.nvim", + opts = { + formatters = { + prettier = { + command = util.from_node_modules(".yarn/sdks/prettier/bin/prettier.cjs"), + }, + eslint = { + -- Run project ESLint through Yarn + command = "yarn", + args = { "eslint", "--fix-dry-run", "--stdin", "--stdin-filename", "$FILENAME" }, + stdin = true, + cwd = util.root_file({ "eslint.config.mjs", ".pnp.cjs", "package.json" }), + timeout = 10000, -- 10s; adjust if needed + }, + }, + formatters_by_ft = { + typescript = { "prettier" }, + typescriptreact = { "prettier" }, + javascript = { "prettier" }, + javascriptreact = { "prettier" }, + }, + }, +} diff --git a/lua/plugins/copilot.lua b/lua/plugins/copilot.lua new file mode 100644 index 0000000..a80edf2 --- /dev/null +++ b/lua/plugins/copilot.lua @@ -0,0 +1,11 @@ +return { + { + "zbirenbaum/copilot.lua", + opts = { + suggestion = { + enabled = true, + auto_trigger = true, + }, + }, + }, +} diff --git a/lua/plugins/git.lua b/lua/plugins/git.lua new file mode 100644 index 0000000..e677e2c --- /dev/null +++ b/lua/plugins/git.lua @@ -0,0 +1,15 @@ +return { + { + "lewis6991/gitsigns.nvim", + keys = { + -- Override LazyVim Git Blame popup with inline toggle + { + "tb", + function() + require("gitsigns").toggle_current_line_blame() + end, + desc = "Toggle Inline Blame", + }, + }, + }, +} diff --git a/lua/plugins/osc52.lua b/lua/plugins/osc52.lua new file mode 100644 index 0000000..2e1ead2 --- /dev/null +++ b/lua/plugins/osc52.lua @@ -0,0 +1,19 @@ +return { + { + "ojroques/nvim-osc52", + config = function() + require("osc52").setup({ + max_length = 0, -- Maximum length of selection (0 for no limit) + silent = false, -- Disable message on successful copy + trim = false, -- Trim surrounding whitespaces before copy + }) + local function copy() + if (vim.v.event.operator == "y" or vim.v.event.operator == "d") and vim.v.event.regname == "" then + require("osc52").copy_register("") + end + end + + vim.api.nvim_create_autocmd("TextYankPost", { callback = copy }) + end, + }, +} diff --git a/lua/plugins/precognition.lua b/lua/plugins/precognition.lua new file mode 100644 index 0000000..a203abb --- /dev/null +++ b/lua/plugins/precognition.lua @@ -0,0 +1,53 @@ +return { + -- Other options + -- nvim-spider + -- leap.nvim + "tris203/precognition.nvim", + event = "VeryLazy", + opts = { + startVisible = false, + showBlankVirtLine = true, + highlightColor = { link = "Comment" }, + hints = { + Caret = { text = "^", prio = 2 }, + Dollar = { text = "$", prio = 1 }, + MatchingPair = { text = "%", prio = 5 }, + + Zero = { text = "0", prio = 1 }, + + w = { text = "w", prio = 10 }, + b = { text = "b", prio = 9 }, + e = { text = "e", prio = 8 }, + ge = { text = "ge", prio = 7 }, + W = { text = "W", prio = 7 }, + B = { text = "B", prio = 6 }, + E = { text = "E", prio = 5 }, + gE = { text = "gE", prio = 4 }, + + f = { text = "f", prio = 10 }, + F = { text = "F", prio = 9 }, + t = { text = "t", prio = 8 }, + T = { text = "T", prio = 7 }, + + ["("] = { text = "(", prio = 3 }, + [")"] = { text = ")", prio = 3 }, + }, + gutterHints = { + G = { text = "G" }, + gg = { text = "gg" }, + PrevParagraph = { text = "{" }, + NextParagraph = { text = "}" }, + + Top = { text = "H" }, + Middle = { text = "M" }, + Bottom = { text = "L" }, + + SearchForward = { text = "/" }, + SearchBackward = { text = "?" }, + NextMatch = { text = "n" }, + PrevMatch = { text = "N" }, + + MarkJump = { text = "'" }, + }, + }, +} diff --git a/lua/plugins/render-markdown.lua b/lua/plugins/render-markdown.lua new file mode 100644 index 0000000..710d543 --- /dev/null +++ b/lua/plugins/render-markdown.lua @@ -0,0 +1,22 @@ +return { + "MeanderingProgrammer/render-markdown.nvim", + opts = { + heading = { + sign = false, + icons = { "> ", "> " }, + width = { "full", "block" }, + left_pad = 1, + right_pad = 2, + }, + checkbox = { + enabled = true, + unchecked = { icon = "σ°„± ", highlight = "RenderMarkdownUnchecked" }, + checked = { icon = "σ°±’ ", highlight = "RenderMarkdownChecked" }, + }, + bullet = { + enabled = true, + icons = { "●", "β—‹", "β—†", "β—‡" }, + }, + anti_conceal = { enabled = false }, + }, +} diff --git a/lua/plugins/snacks.lua b/lua/plugins/snacks.lua new file mode 100644 index 0000000..499cfaf --- /dev/null +++ b/lua/plugins/snacks.lua @@ -0,0 +1,70 @@ +return { + -- Import the snacks explorer extra + { import = "lazyvim.plugins.extras.editor.snacks_explorer" }, + + -- Configure snacks.nvim + { + "folke/snacks.nvim", + -- keys = { + -- { + -- "\\", + -- function() + -- Snacks.explorer() + -- end, + -- desc = "Toggle Snacks Explorer (Picker)", + -- }, + -- { "e", false }, + -- }, + opts = { + dashboard = { + preset = { + header = [[ + +β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— +β•šβ•β•β–ˆβ–ˆβ•”β•β•β•β–ˆβ–ˆβ•”β•β•β•β•β•β–ˆβ–ˆβ–ˆβ–ˆβ•— β–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•—β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•”β•β•β•β•β• + β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ•”β–ˆβ–ˆβ–ˆβ–ˆβ•”β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•— + β–ˆβ–ˆβ•‘ β•šβ•β•β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘β•šβ–ˆβ–ˆβ•”β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•”β•β•β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘ β•šβ•β•β•β•β–ˆβ–ˆβ•‘ + β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β•šβ•β• β–ˆβ–ˆβ•‘β–ˆβ–ˆβ•‘ β–ˆβ–ˆβ•‘β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•—β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ•‘ + β•šβ•β• β•šβ•β•β•β•β•β•β•β•šβ•β• β•šβ•β•β•šβ•β• β•šβ•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β•β•šβ•β•β•β•β•β•β• + + + ]], + }, + }, + scroll = { + enabled = false, -- Disable scrolling animations + }, + picker = { + sources = { + explorer = { + hidden = false, -- Show hidden files by default + layout = { preset = "right" }, -- Position on the right + ignored = false, + git_status = true, -- Show git status + diagnostics = true, -- Show diagnostics + follow_file = true, -- Follow current file + win = { + list = { + keys = { + ["P"] = "toggle_preview", -- Custom key + ["L"] = function() + -- Get current line and try to extract file path + local line = vim.api.nvim_get_current_line() + local filepath = line:match("%s*(.-)%s*$") -- trim whitespace + + if filepath and vim.fn.filereadable(filepath) == 1 then + local explorer_win = vim.api.nvim_get_current_win() + vim.cmd("wincmd p") + vim.cmd("edit " .. vim.fn.fnameescape(filepath)) + vim.api.nvim_set_current_win(explorer_win) + end + end, + }, + }, + }, + }, + }, + }, + }, + }, +} diff --git a/lua/plugins/telescope.lua b/lua/plugins/telescope.lua new file mode 100644 index 0000000..9ebf6b5 --- /dev/null +++ b/lua/plugins/telescope.lua @@ -0,0 +1,3 @@ +return { + { import = "lazyvim.plugins.extras.editor.telescope" }, +} diff --git a/lua/plugins/toggle-term.lua b/lua/plugins/toggle-term.lua new file mode 100644 index 0000000..bf0625d --- /dev/null +++ b/lua/plugins/toggle-term.lua @@ -0,0 +1,19 @@ +return { + "akinsho/toggleterm.nvim", + version = "*", + + keys = { + { "tt", "ToggleTerm direction=float", desc = "Toggle Terminal" }, + }, + + opts = { + open_mapping = [[tt]], + direction = "float", + float_opts = { border = "rounded" }, + shell = (os.getenv("SHELL") or vim.fn.exepath("zsh") or vim.fn.exepath("bash")) .. " -l -i", + }, + + config = function(_, opts) + require("toggleterm").setup(opts) + end, +} diff --git a/lua/plugins/transparency.lua b/lua/plugins/transparency.lua new file mode 100644 index 0000000..a771483 --- /dev/null +++ b/lua/plugins/transparency.lua @@ -0,0 +1,61 @@ +local function apply_transparency() + local groups = { + "Normal", + "NormalNC", + "NormalFloat", + "FloatBorder", + "SignColumn", + "LineNr", + "CursorLineNr", + "EndOfBuffer", + "TelescopeNormal", + "TelescopeBorder", + "TelescopePromptBorder", + "TelescopeResultsBorder", + "TelescopePreviewBorder", + "NeoTreeNormal", + "NeoTreeNormalNC", + "NeoTreeWinSeparator", + "NeoTreeEndOfBuffer", + "Pmenu", + "PmenuSel", + } + + for _, group in ipairs(groups) do + vim.api.nvim_set_hl(0, group, { bg = "none" }) + end +end + +return { + { + "LazyVim/LazyVim", + opts = { + colorscheme = function() + vim.cmd("colorscheme tokyonight") -- or your theme + apply_transparency() + end, + }, + }, + + -- Automatically re-apply transparency when UI elements refresh + { + "xiyaowong/transparent.nvim", + event = "VeryLazy", + config = function() + -- when floats open + vim.api.nvim_create_autocmd("User", { + pattern = { + "SnacksPickerOpen", + "SnacksPickerClose", + "LazyVimFloat", + }, + callback = apply_transparency, + }) + + -- when switching colorscheme + vim.api.nvim_create_autocmd("ColorScheme", { + callback = apply_transparency, + }) + end, + }, +} diff --git a/lua/plugins/typescript.lua b/lua/plugins/typescript.lua new file mode 100644 index 0000000..d927dbc --- /dev/null +++ b/lua/plugins/typescript.lua @@ -0,0 +1,40 @@ +return { + -- disable vtsls + { + "neovim/nvim-lspconfig", + opts = { + servers = { + vtsls = false, + tsserver = false, + }, + inlay_hints = { + enabled = false, + }, + }, + }, + + -- enable typescript-tools + { + "pmizio/typescript-tools.nvim", + ft = { + "typescript", + "typescriptreact", + "javascript", + "javascriptreact", + }, + opts = { + settings = { + tsserver_file_preferences = { + disableSuggestions = false, + includeInlayParameterNameHints = "all", + includeCompletionsForModuleExports = true, + }, + tsserver_enable_partial_semantic_mode = true, + tsserver_watch_options = { + watchFile = "useFsEvents", -- recommended for remote/container + watchDirectory = "useFsEvents", + }, + }, + }, + }, +} diff --git a/lua/plugins/which-key.lua b/lua/plugins/which-key.lua new file mode 100644 index 0000000..723c86a --- /dev/null +++ b/lua/plugins/which-key.lua @@ -0,0 +1,9 @@ +return { + "folke/which-key.nvim", + opts = { + spec = { + { "m", group = "markdown" }, + { "mm", group = "move task" }, + }, + }, +}