summaryrefslogtreecommitdiffstats
path: root/plugin
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-06-25 07:44:15 +0200
committerThomas Vanbesien <tvanbesi@proton.me>2026-06-25 07:44:15 +0200
commitfb3587e51e0908c3d43722c55741cf6a386e39ce (patch)
tree248c42999260ab6c9175aea90365aab08bd68c9d /plugin
parentc22fab35706facfa780b800b4dc72559f0ac6c60 (diff)
downloadnvim-config-fb3587e51e0908c3d43722c55741cf6a386e39ce.tar.gz
nvim-config-fb3587e51e0908c3d43722c55741cf6a386e39ce.zip
refactor(nvim): restructure plugin configs, fix OSV launch port
- rewrite the file headers as block comments and regroup the autocompletion and dap files into Settings / Autocommands / Keymaps sections - fix `OSVLaunch`: `dap.configurations.lua.port` was nil (the field is a list), so the server ignored 8086; share an `OSV_PORT` constant between the launch call and the attach configuration - `.luarc.json`: list each plugin directory explicitly so lua_ls loads their type annotations - use `vim.fs.basename` instead of `vim.fn.fnamemodify(…, ":t")`
Diffstat (limited to 'plugin')
-rw-r--r--plugin/00-plugin.lua10
-rw-r--r--plugin/50-autocompletion.lua112
-rw-r--r--plugin/50-autopairs.lua13
-rw-r--r--plugin/50-color.lua114
-rw-r--r--plugin/50-dap.lua57
5 files changed, 155 insertions, 151 deletions
diff --git a/plugin/00-plugin.lua b/plugin/00-plugin.lua
index def11c0..ac34066 100644
--- a/plugin/00-plugin.lua
+++ b/plugin/00-plugin.lua
@@ -1,8 +1,8 @@
---
--- 00-plugin.lua
---
--- * Installs third-party plugins.
---
+--[[ 00-plugin.lua — install third-party plugins via vim.pack.
+
+Plugins are pinned in nvim-pack-lock.json; this file must load first so later plugin/ files can
+require() them.
+]]
vim.pack.add({
"https://github.com/airblade/vim-gitgutter",
diff --git a/plugin/50-autocompletion.lua b/plugin/50-autocompletion.lua
index 853a528..a8b7f4b 100644
--- a/plugin/50-autocompletion.lua
+++ b/plugin/50-autocompletion.lua
@@ -1,18 +1,17 @@
---
--- 50-autocompletion.lua
---
--- * Configures autocompletion for insert mode and command-line mode.
--- In both modes:
--- * The list of completions shows up automatically
--- * Tab/Shift+Tab selects (and inserts) the next/previous item
--- In cmd-line mode:
--- * Tab selects a lone match outright, inserts it, closes the menu
--- (Useful when completing file names)
--- * CTRL+Y inserts the selection; with no selection it dismisses the menu
---
+--[[ 50-autocompletion.lua — automatic completion for insert and command-line mode.
+
+In both modes the menu shows up automatically and `<Tab>`/`<S-Tab>` select the next/previous item.
+In command-line mode `<Tab>` accepts a lone match outright and `<C-y>` accepts the selection (or
+dismisses the menu when nothing is selected).
+
+Keymaps:
+ `<Tab>` / `<S-Tab>` (insert) select next / previous menu item
+ `<Tab>` (cmdline) accept a lone match, else select next
+ `<C-y>` (cmdline) accept the selection, else dismiss
+]]
----------------------------------------------------------------------------------------------------
--- Insert mode autocompletion
+-- Insert mode -------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
local function i_select_next_menu_item()
@@ -24,36 +23,18 @@ local function i_select_previous_menu_item()
end
----------------------------------------------------------------------------------------------------
-
-vim.opt.autocomplete = true -- Show completion menu automatically
-vim.opt.completeopt = {
- "noselect", -- No item selected initially
- "menuone", -- Show matches in a menu, even if there's only one match
- "popup", -- Menu items show extra info in the popup window
- "fuzzy",
-}
--- Completion sources (in order of priority)
-vim.opt.complete = {
- "o", -- 'omnifunc'
-}
-
-vim.keymap.set("i", "<Tab>", i_select_next_menu_item, { expr = true })
-vim.keymap.set("i", "<S-Tab>", i_select_previous_menu_item, { expr = true })
-
-----------------------------------------------------------------------------------------------------
--- Command-line mode autocompletion
+-- Command-line mode -------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
--- `<Space><BS>` is a self-cancelling edit: it closes the menu, then `CmdLineChanged` fires and
--- tries to perform completion on the text before the cursor.
+--[[ `<Space><BS>` is a self-cancelling edit: it closes the menu, then `CmdlineChanged` fires and
+tries to perform completion on the text before the cursor. ]]
local function c_insert_unique_or_wildtrigger()
local complete_info = vim.fn.cmdcomplete_info()
if complete_info.pum_visible == 1 then
return #complete_info.matches == 1 and "<C-n><Space><BS>" or "<C-n>"
end
-- After navigating the command-line history, it takes two `wildtrigger()` calls to show the
- -- menu. The first is performed by the `CmdLineChanged` autocommand, the second is performed
- -- here.
+ -- menu. The first is performed by the `CmdlineChanged` autocommand, the second is performed here.
vim.fn.wildtrigger()
return nil
end
@@ -66,33 +47,54 @@ local function c_accept_selection_or_dismiss()
return "<C-y>"
end
+-- Show the command-line completion menu automatically.
+local function trigger_wildmenu()
+ vim.fn.wildtrigger()
+end
+
+----------------------------------------------------------------------------------------------------
+-- Settings ----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------------------
+
+-- Insert mode.
+vim.opt.autocomplete = true -- Show the completion menu automatically.
+--[[ Completion menu behaviour: do not preselect an item (`noselect`), show a menu even for a single
+match (`menuone`), show extra info in a popup window (`popup`), and match fuzzily (`fuzzy`). ]]
+vim.opt.completeopt = { "noselect", "menuone", "popup", "fuzzy" }
+-- Completion sources, in order of priority: `omnifunc` (`o`).
+vim.opt.complete = { "o" }
+
+-- Command-line mode.
+vim.opt.wildmenu = true -- Show completions in a menu.
+vim.opt.wildchar = 9 -- Char code (`<Tab>`) assigned to command-line wildcard expansion.
+--[[ Wildmenu behaviour: discard regex artifacts in search-pattern completion (`exacttext`), use a
+popup menu (`pum`), show tag kind and file (`tagfile`), and match fuzzily except for files/dirs,
+which use patterns (`fuzzy`). ]]
+vim.opt.wildoptions = { "exacttext", "pum", "tagfile", "fuzzy" }
+--[[ Completion modes triggered in order by `wildtrigger()` or `wildchar`: list matches without
+inserting (`noselect`), then list matches and insert the first full match (`full`). ]]
+vim.opt.wildmode = { "noselect", "full" }
+
+----------------------------------------------------------------------------------------------------
+-- Autocommands ------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
--- Show completion menu automatically
-vim.api.nvim_create_autocmd({ "CmdLineChanged", "CmdlineEnter" }, {
+-- The `pattern` matches `:` commands plus `/` and `?` searches, but not `=` and `@`.
+vim.api.nvim_create_autocmd({ "CmdlineChanged", "CmdlineEnter" }, {
desc = "Autocompletion",
group = vim.g.dotfiles.augroup,
- pattern = "[:\\/\\?]", -- Matches : commands plus / and ? searches, but not = and @
- callback = function()
- vim.fn.wildtrigger()
- end,
+ pattern = "[:\\/\\?]",
+ callback = trigger_wildmenu,
})
-vim.opt.wildmenu = true -- Show completions in a menu
-vim.opt.wildchar = 9 -- Char code (`<Tab>`) assigned to command-line wildcard expansion
-vim.opt.wildoptions = {
- "exacttext", -- Discard regex artifacts when performing search pattern completion
- "pum", -- Show completions in a popup menu
- "tagfile", -- Show tag kind and file
- "fuzzy", -- Fuzzy matching (except for files/dirs, use patterns)
-}
--- Completion modes triggered in order by `wildtrigger()` or 'wildchar'
-vim.opt.wildmode = {
- "noselect", -- List matches without inserting
- "full", -- List matches and insert first full match
-}
+----------------------------------------------------------------------------------------------------
+-- Keymaps -----------------------------------------------------------------------------------------
+----------------------------------------------------------------------------------------------------
--- Converts `vim.opt.wildchar` char code into a key string that can be used by `vim.keymap.set()`.
+-- Convert the `vim.o.wildchar` char code into a key string usable by `vim.keymap.set()`.
local wildchar_key = string.format("%c", vim.o.wildchar)
+
+vim.keymap.set("i", "<Tab>", i_select_next_menu_item, { expr = true })
+vim.keymap.set("i", "<S-Tab>", i_select_previous_menu_item, { expr = true })
vim.keymap.set("c", wildchar_key, c_insert_unique_or_wildtrigger, { expr = true })
vim.keymap.set("c", "<C-y>", c_accept_selection_or_dismiss, { expr = true })
diff --git a/plugin/50-autopairs.lua b/plugin/50-autopairs.lua
index aa19967..64b5609 100644
--- a/plugin/50-autopairs.lua
+++ b/plugin/50-autopairs.lua
@@ -1,13 +1,16 @@
---
--- 50-autopairs.lua
---
--- * Adds autopairs for markdown (`**` and `~~`)
---
+--[[ 50-autopairs.lua — autopairs rules for markdown.
+
+Pairs `*` and `~` so that `**bold**` and `~~strike~~` auto-close, via nvim-autopairs.
+]]
local Rule = require("nvim-autopairs.rule")
local npairs = require("nvim-autopairs")
local cond = require("nvim-autopairs.conds")
+
npairs.setup()
+
+--[[ Auto-close `*` and `~` in markdown, but only when not already before the same char (so `**`/`~~`
+nest cleanly), and never expand them on `<CR>`. ]]
npairs.add_rules({
Rule("*", "*", "markdown"):with_move(cond.not_before_regex("%*")):with_cr(cond.none()),
Rule("~", "~", "markdown"):with_move(cond.not_before_regex("~")):with_cr(cond.none()),
diff --git a/plugin/50-color.lua b/plugin/50-color.lua
index ede41ee..f95711c 100644
--- a/plugin/50-color.lua
+++ b/plugin/50-color.lua
@@ -1,18 +1,15 @@
---
--- 50-color.lua
---
--- * Highlight tweaks for the solarized colorscheme, plus popup-menu and tabpage-title styling.
--- * The lualine statusline (https://github.com/nvim-lualine/lualine.nvim), themed to match.
--- * Registers both into dotfiles.color, which re-applies them on colorscheme / 'background' changes
--- and once after startup.
--- * Sets the default colorscheme (solarized) and background (dark).
---
--- User commands:
--- `ColorsBgToggle`: toggle light/dark background
--- `TabRename`: rename current tab
--- `TabRenameFileName`: rename current tab to the active buffer's file name
--- `LualineConfig`: show the resolved lualine configuration
---
+--[[ 50-color.lua — solarized highlight tweaks, a matching lualine statusline, and the custom tabline.
+
+Registers highlight and statusline appliers into dotfiles.color, which re-applies them on colorscheme
+/ 'background' changes and once after startup. Also styles the popup menu and tabpage titles, and sets
+the default colorscheme (solarized) and background (dark).
+
+User commands:
+ `ColorsBgToggle`: toggle light/dark background
+ `TabRename`: rename the current tab
+ `TabRenameFileName`: rename the current tab to the active buffer's file name
+ `LualineConfig`: show the resolved lualine configuration
+]]
local color = require("dotfiles.color")
local lualine = require("lualine")
@@ -20,41 +17,40 @@ local lualine = require("lualine")
local solarized = { blue = "#268bd2" }
----------------------------------------------------------------------------------------------------
--- Highlights
+-- Highlights --------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
--- Highlight applier: adjust the solarized groups to fit my personal tastes. No-ops unless solarized
--- is the active colorscheme.
+--[[ Highlight applier: adjust the solarized groups to fit my personal tastes. No-ops unless solarized
+is the active colorscheme. ]]
local function apply_solarized_highlights()
if vim.g.colors_name ~= "solarized" then
return
end
- -- Window bar style
+ -- Window bar style.
vim.api.nvim_set_hl(0, "WinBarCwd", { fg = solarized.blue, italic = true })
vim.api.nvim_set_hl(0, "WinBarFilePath", { italic = true })
- -- Matching delimiter
+ -- Matching delimiter.
vim.api.nvim_set_hl(0, "MatchParen", { link = "CurSearch" })
- -- Floating windows and popup menu windows follow the editor background, their borders too so
- -- that the popup menu border draws a clean separation.
+ -- Floating windows and popup menu windows follow the editor background, their borders too so that
+ -- the popup menu border draws a clean separation.
vim.opt.pumborder = "rounded"
- vim.opt.pumwidth = 25 -- Minimum popup menu width
+ vim.opt.pumwidth = 25 -- Minimum popup menu width.
vim.api.nvim_set_hl(0, "NormalFloat", { link = "Normal" })
local normal_bg = vim.api.nvim_get_hl(0, { name = "Normal", link = false }).bg
vim.api.nvim_set_hl(0, "FloatBorder", { bg = normal_bg, update = true })
vim.api.nvim_set_hl(0, "Pmenu", { bg = normal_bg, update = true })
vim.api.nvim_set_hl(0, "PmenuSbar", { bg = normal_bg, update = true })
- -- Currently selected tabpage
+ -- Currently selected tabpage.
vim.api.nvim_set_hl(0, "TabLineSel", { bold = true, italic = true, update = true })
- -- Todo hl group
+ -- `Todo` highlight group.
vim.api.nvim_set_hl(0, "Todo", { bg = "Yellow", fg = "Black", bold = true })
- -- Fold lines being the same color as the cursor line (default) make unfocused windows unreadable
- -- Their background is set to blend in, the folds are visible enough with their dotted lines
+ --[[ Fold lines sharing the cursor-line color (the default) make unfocused windows unreadable, so
+ their background is set to blend in; the folds stay visible enough through their dotted lines. ]]
vim.api.nvim_set_hl(0, "Folded", { bg = normal_bg, underline = true, update = true })
end
--- Highlight applier: create custom highlight groups for styling the tabpage title bars:
--- * TabLineWinCount: number of open windows in the tabpage
--- * TabLineSelWinCount: number of open windows in the currently active tabpage
+--[[ Highlight applier: create custom highlight groups for styling the tabpage title bars.
+`TabLineWinCount` is the window count of a tabpage, `TabLineSelWinCount` that of the active one. ]]
local function apply_tabpage_title_highlights()
local normal = vim.api.nvim_get_hl(0, { name = "Normal", link = false })
local tabline = vim.api.nvim_get_hl(0, { name = "TabLine", link = false })
@@ -69,7 +65,7 @@ local function apply_tabpage_title_highlights()
end
----------------------------------------------------------------------------------------------------
--- Statusline
+-- Statusline --------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- Spell flag: which spell languages are enabled, or nil when 'spelllang' matches none of these.
@@ -83,8 +79,8 @@ local function spell_status()
end
end
--- Custom status flags: "+F" when formatting is on for the buffer, "+H" when LSP inlay hints are on,
--- "+R" when disk-change autoreload is enabled (see 50-autoreload.lua), followed by the spell flag.
+--[[ Custom status flags: `+F` when formatting is on for the buffer, `+H` when LSP inlay hints are on,
+`+R` when disk-change autoreload is enabled (see 50-autoreload.lua), followed by the spell flag. ]]
local function statuses()
return (vim.b.format and "+F" or "")
.. (vim.lsp.inlay_hint.is_enabled({ bufnr = 0 }) and "+H" or "")
@@ -92,10 +88,10 @@ local function statuses()
.. (vim.o.spell and spell_status() or "")
end
--- lualine builds its own section-b-backed highlights for the diff/diagnostics components
--- (lualine_b_diff_added_normal, lualine_b_diagnostics_error_normal, ...). Stamp bold onto them with
--- update=true so only the bold attribute changes — fg and section b's background are left untouched.
--- Must run after lualine (re)creates the groups: initial setup, theme switch, colorscheme.
+--[[ lualine builds its own section-b-backed highlights for the diff/diagnostics components
+(`lualine_b_diff_added_normal`, `lualine_b_diagnostics_error_normal`, ...). Stamp bold onto them with
+`update = true` so only the bold attribute changes — fg and section b's background are left untouched.
+Must run after lualine (re)creates the groups: initial setup, theme switch, colorscheme. ]]
local function bold_diff_diag()
for name in pairs(vim.api.nvim_get_hl(0, {})) do
if name:find("^lualine_b_diff") or name:find("^lualine_b_diagnostics") then
@@ -121,28 +117,28 @@ local function configure_lualine()
lualine.setup({ options = lualine_options, sections = lualine_sections })
end
--- Statusline applier: re-pick the lualine theme for the current 'background', then re-bold the
--- diff/diagnostics groups once lualine has rebuilt them.
+--[[ Statusline applier: re-pick the lualine theme for the current 'background', then re-bold the
+diff/diagnostics groups once lualine has rebuilt them. ]]
local function apply_statusline_highlights()
configure_lualine()
vim.schedule(bold_diff_diag)
end
----------------------------------------------------------------------------------------------------
--- Tabline
+-- Tabline -----------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
--- Builds the window-count segment of a tabpage label, e.g. ` (2)[+] ` (no styling).
--- `is_modified` appends the `[+]` marker when any window in the tab has pending changes.
+--[[ Build the window-count segment of a tabpage label, e.g. ` (2)[+] ` (no styling).
+`is_modified` appends the `[+]` marker when any window in the tab has pending changes. ]]
local function make_tab_info_label(win_count, is_modified)
local tab_win_count = "(" .. (win_count >= 10 and ">9" or tostring(win_count)) .. ")"
local tab_win_count_suffix = is_modified and "[+] " or " "
return " " .. tab_win_count .. tab_win_count_suffix
end
--- Builds the title segment of a tabpage label, padded or truncated to `width` (no styling).
--- Uses the tab's `tabname` if set, else the active buffer's file name, else `[No Name]`, prefixed
--- with `*` for the current tab and `-` for the last-accessed tab.
+--[[ Build the title segment of a tabpage label, padded or truncated to `width` (no styling).
+Uses the tab's `tabname` if set, else the active buffer's file name, else `[No Name]`, prefixed with
+`*` for the current tab and `-` for the last-accessed tab. ]]
local function make_tab_label(id, is_current, width)
local is_last_accessed = vim.api.nvim_tabpage_get_number(id) == vim.fn.tabpagenr("#")
local tab_label_prefix = (is_current and "*" or "") .. (is_last_accessed and "-" or "")
@@ -150,7 +146,7 @@ local function make_tab_label(id, is_current, width)
if tab_label == nil or tab_label == "" then
local win = vim.api.nvim_tabpage_get_win(id)
local bufname = vim.api.nvim_buf_get_name(vim.api.nvim_win_get_buf(win))
- tab_label = vim.fn.fnamemodify(bufname, ":t")
+ tab_label = vim.fs.basename(bufname)
end
if tab_label == "" then
tab_label = "[No Name]"
@@ -161,9 +157,9 @@ local function make_tab_label(id, is_current, width)
or (tab_label .. string.rep(" ", width - display_width))
end
--- Returns a string compatible with the 'tabline' option, used to set the tabline content and style.
--- Declared as a global function to be available as `v:lua.GetTabLine` in the Vimscript engine for
--- the 'tabline' option. See `:help 'statusline'`.
+--[[ Return a string compatible with the 'tabline' option, used to set the tabline content and style.
+Declared as a global function to be available as `v:lua.GetTabLine` in the Vimscript engine for the
+'tabline' option. See <nvim-help://%27statusline%27>. ]]
function GetTabLine()
local max_tab_label_width = 30
local s = ""
@@ -178,7 +174,7 @@ function GetTabLine()
end
end
- -- Define start of tabpage label (marks where clickage region begins)
+ -- Mark the start of the tabpage label (where the clickable region begins).
s = s .. "%" .. vim.api.nvim_tabpage_get_number(id) .. "T"
local tab_info_label = make_tab_info_label(#wins, is_modified)
@@ -204,7 +200,7 @@ local function rename_tab_to_cur_filename()
end
----------------------------------------------------------------------------------------------------
--- Commands & init
+-- Commands & init ---------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-- Flip between the light and dark backgrounds (the colorscheme reloads, OptionSet re-themes).
@@ -222,20 +218,14 @@ color.register(apply_solarized_highlights)
color.register(apply_tabpage_title_highlights)
color.register(apply_statusline_highlights)
-vim.api.nvim_create_user_command(
- "ColorsBgToggle",
- toggle_theme,
- { desc = "Toggle light/dark theme" }
-)
+-- stylua: ignore start
+vim.api.nvim_create_user_command("ColorsBgToggle", toggle_theme, { desc = "Toggle light/dark theme" })
vim.api.nvim_create_user_command("TabRename", rename_tab, { nargs = 1 })
vim.api.nvim_create_user_command("TabRenameFileName", rename_tab_to_cur_filename, { nargs = 0 })
-vim.api.nvim_create_user_command(
- "LualineConfig",
- show_lualine_config,
- { desc = "Show lualine configuration" }
-)
+vim.api.nvim_create_user_command("LualineConfig", show_lualine_config, { desc = "Show lualine configuration" })
+-- stylua: ignore end
--- See `:help stl-%!`
+-- See <nvim-help://stl-%25%21>.
vim.o.tabline = "%!v:lua.GetTabLine()"
vim.cmd.colorscheme("solarized")
vim.opt.background = "dark"
diff --git a/plugin/50-dap.lua b/plugin/50-dap.lua
index 7c715ba..4f07214 100644
--- a/plugin/50-dap.lua
+++ b/plugin/50-dap.lua
@@ -1,24 +1,46 @@
---
--- 50-dap.lua
---
--- * Configures OSV:
--- User commands:
--- * OSVLaunch: start the OSV server (on the debuggee)
--- * OSVStatus: show status of OSV server and client
---
+--[[ 50-dap.lua — debug Lua running inside Neovim via OSV (one-small-step-for-vimkind).
+
+Wires nvim-dap to the OSV adapter so the running Neovim instance can be attached to and debugged.
+
+User commands:
+ `OSVLaunch`: start the OSV server (on the debuggee)
+ `OSVStatus`: show the status of the OSV server and client
+]]
local dap = require("dap")
+local osv = require("osv")
+
+-- Port shared by the OSV server and the dap attach configuration.
+local OSV_PORT = 8086
----------------------------------------------------------------------------------------------------
--- OSV (Lua inside Neovim)
+-- OSV (Lua inside Neovim) -------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------
-dap.configurations.lua = {
+-- Start the OSV server so the running Neovim instance can be attached to.
+local function osv_launch()
+ osv.launch({ port = OSV_PORT })
+end
+
+-- Report whether the OSV server is running and whether a client is attached.
+local function osv_status()
+ vim.notify("OSV server running: " .. tostring(osv.is_running()))
+ vim.notify("OSV client attached: " .. tostring(osv.is_attached()))
+end
+
+----------------------------------------------------------------------------------------------------
+-- Configuration & commands ------------------------------------------------------------------------
+----------------------------------------------------------------------------------------------------
+
+--[[ The cast restores dap.configurations' own type: with fzf-lua in the lua_ls library its dap
+provider cross-merges a `configurations` field, which otherwise mistypes this as a function. ]]
+local configurations = dap.configurations --[[@as table<string, dap.Configuration[]>]]
+configurations.lua = {
{
type = "nlua",
request = "attach",
name = "Attach to running Neovim instance",
- port = 8086,
+ port = OSV_PORT,
},
}
@@ -26,18 +48,5 @@ dap.adapters.nlua = function(callback, config)
callback({ type = "server", host = config.host or "127.0.0.1", port = config.port })
end
-local osv = require("osv")
-
-local function osv_launch()
- osv.launch({ port = dap.configurations.lua.port })
-end
-
-local function osv_status()
- vim.notify("OSV server running: " .. tostring(osv.is_running()))
- vim.notify("OSV client attached: " .. tostring(osv.is_attached()))
-end
-
-----------------------------------------------------------------------------------------------------
-
vim.api.nvim_create_user_command("OSVLaunch", osv_launch, { desc = "Launch OSV server" })
vim.api.nvim_create_user_command("OSVStatus", osv_status, { desc = "Show OSV status" })