local M = {} -- get git remote url and extract username/repo local function get_repo_info() local handle = io.popen("git config --get remote.origin.url 2>/dev/null") if not handle then return nil, "Failed to execute git command" end local remote = handle:read("*a") handle:close() if remote == "" then return nil, "No git remote found" end -- Remove trailing whitespace/newline remote = remote:gsub("%s+$", "") -- https://host/username/repo or https://host/username/repo.git local user_repo = remote:match("[:/]([^:/]+/[^/]+)%.?g?i?t?$") if not user_repo then return nil, "Could not parse repository from remote: " .. remote end -- Remove .git suffix if present user_repo = user_repo:gsub("%.git$", "") return user_repo, nil end -- Get current git commit hash local function get_commit_hash() local handle = io.popen("git rev-parse HEAD 2>/dev/null") if not handle then return nil, "Failed to execute git command" end local hash = handle:read("*a") handle:close() if hash == "" then return nil, "Not in a git repository" end return hash:gsub("%s+$", ""), nil end -- Get file path relative to git root local function get_relative_path(absolute_path) local handle = io.popen("git rev-parse --show-toplevel 2>/dev/null") if not handle then return nil, "Failed to execute git command" end local git_root = handle:read("*a") handle:close() if git_root == "" then return nil, "Not in a git repository" end git_root = git_root:gsub("%s+$", "") -- Remove git root from absolute path local rel_path = absolute_path:gsub("^" .. git_root .. "/", "") return rel_path, nil end -- copy text to system clipboard local function copy_to_clipboard(text) vim.fn.setreg("+", text) vim.fn.setreg('"', text) end -- generate and copy permalink function M.copy_permalink() local repo, err = get_repo_info() if err then vim.notify("Error: " .. err, vim.log.levels.ERROR) return end local commit, err = get_commit_hash() if err then vim.notify("Error: " .. err, vim.log.levels.ERROR) return end local filepath = vim.fn.expand("%:p") local rel_path, err = get_relative_path(filepath) if err then vim.notify("Error: " .. err, vim.log.levels.ERROR) return end -- get line numbers local mode = vim.fn.mode() local start_line, end_line if mode == "v" or mode == "V" or mode == "\22" then -- visual modes start_line = vim.fn.line("v") end_line = vim.fn.line(".") -- ensure start_line <= end_line if start_line > end_line then start_line, end_line = end_line, start_line end else start_line = vim.fn.line(".") end_line = start_line end -- construct url local url = string.format( "https://tangled.org/%s/blob/%s/%s#L%d", repo, commit, rel_path, start_line ) -- add end line if selection spans multiple lines if end_line > start_line then url = url .. "-" .. end_line end -- copy to clipboard copy_to_clipboard(url) if end_line > start_line then vim.notify(string.format("Copied permalink for lines %d-%d", start_line, end_line), vim.log.levels.INFO) else vim.notify(string.format("Copied permalink for line %d", start_line), vim.log.levels.INFO) end end -- setup function to configure the plugin function M.setup(opts) opts = opts or {} vim.api.nvim_create_user_command( "TangledPermalink", function() M.copy_permalink() end, { range = true, desc = "Copy tangled.org permalink to clipboard" } ) end -- set up keybindings vim.keymap.set({"n", "v"}, "gl", M.copy_permalink, { desc = "Copy tangled.org permalink", silent = true }) return M