mirror of
https://github.com/zoriya/jj.nvim.git
synced 2026-08-05 02:36:07 +00:00
Feat: Annotation by file and line (#55)
This commit is contained in:
@@ -435,6 +435,43 @@ diff.open_hsplit() -- Horizontal split diff
|
||||
diff.open_hsplit({ rev = "@-2" }) -- Horizontal split against @-2
|
||||
```
|
||||
|
||||
### Annotations
|
||||
|
||||
View file blame and line history using the annotate module. Can be invoked via command or Lua API.
|
||||
|
||||
**Via `:J` command:**
|
||||
|
||||
```sh
|
||||
:J annotate " Show blame/annotations for entire file in vertical split
|
||||
:J annotate_line " Show annotation for current line in floating buffer
|
||||
```
|
||||
|
||||
**Via Lua API:**
|
||||
|
||||
```lua
|
||||
local annotate = require("jj.annotate")
|
||||
annotate.file() -- Show blame/annotations for entire file in vertical split
|
||||
annotate.line() -- Show annotation for current line in a tooltip
|
||||
```
|
||||
|
||||
The file annotation displays a vertical split showing:
|
||||
|
||||
- Change ID (colored uniquely per commit)
|
||||
- Author name
|
||||
- Timestamp
|
||||
|
||||
Press `<CR>` on any annotation line to view the diff for that change.
|
||||
|
||||
The line annotation displays a floating tooltip with the current line's annotation and the commit description.
|
||||
|
||||
Example keymaps:
|
||||
|
||||
```lua
|
||||
local annotate = require("jj.annotate")
|
||||
vim.keymap.set("n", "<leader>ja", annotate.file, { desc = "JJ annotate file" })
|
||||
vim.keymap.set("n", "<leader>jA", annotate.line, { desc = "JJ annotate line" })
|
||||
```
|
||||
|
||||
## Example config
|
||||
|
||||
```lua
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
local M = {}
|
||||
|
||||
local utils = require("jj.utils")
|
||||
local runner = require("jj.core.runner")
|
||||
local buffer = require("jj.core.buffer")
|
||||
local parser = require("jj.core.parser")
|
||||
|
||||
--TODO: Maybe if annotating on the file is slow we could cache the annotations.
|
||||
|
||||
-- Track the last annotation tooltip buffer
|
||||
local last_tooltip_buf = nil
|
||||
|
||||
--- Sets the highlights for the blame bufer
|
||||
--- @param buf integer
|
||||
--- @param annotations string[]
|
||||
local function setup_blame_highlighting(buf, annotations)
|
||||
local ns = vim.api.nvim_create_namespace("jj_annotate")
|
||||
local seen = {}
|
||||
|
||||
-- Define base highlight groups (link to standard groups)
|
||||
vim.api.nvim_set_hl(0, "JJAnnotateDelimiter", { link = "Delimiter" })
|
||||
vim.api.nvim_set_hl(0, "JJAnnotateName", { link = "String" })
|
||||
vim.api.nvim_set_hl(0, "JJAnnotateDate", { link = "PreProc" })
|
||||
|
||||
for i, line in ipairs(annotations) do
|
||||
-- Parse: "wmkslu | NicolasGB | 2025-11-23"
|
||||
local id_start, id_end, change_id = line:find("^(%S+)")
|
||||
local name_start, _ = line:find("|%s*(. -)%s*|")
|
||||
local date_start, date_end = line:find("%d%d%d%d%-%d%d%-%d%d%s%d%d:%d%d:%d%d%s[%+%-]%d%d:%d%d")
|
||||
|
||||
if change_id then
|
||||
-- Dynamic color for change_id
|
||||
if not seen[change_id] then
|
||||
seen[change_id] = true
|
||||
local hash = vim.fn.sha256(change_id):sub(1, 6)
|
||||
local hl_group = "JJAnnotateId" .. change_id
|
||||
vim.api.nvim_set_hl(0, hl_group, { fg = "#" .. hash })
|
||||
end
|
||||
|
||||
-- Highlight change ID
|
||||
vim.api.nvim_buf_set_extmark(buf, ns, i - 1, id_start - 1, {
|
||||
end_col = id_end,
|
||||
hl_group = "JJAnnotateId" .. change_id,
|
||||
})
|
||||
end
|
||||
|
||||
-- Highlight delimiters
|
||||
for delim_start in line:gmatch("()| ") do
|
||||
vim.api.nvim_buf_set_extmark(buf, ns, i - 1, delim_start - 1, {
|
||||
end_col = delim_start,
|
||||
hl_group = "JJAnnotateDelimiter",
|
||||
})
|
||||
end
|
||||
|
||||
-- Highlight name (between first and second |)
|
||||
if name_start then
|
||||
local actual_name_start = line:find("|") + 2
|
||||
local actual_name_end = line:find("|", actual_name_start) - 2
|
||||
vim.api.nvim_buf_set_extmark(buf, ns, i - 1, actual_name_start - 1, {
|
||||
end_col = actual_name_end + 1,
|
||||
hl_group = "JJAnnotateName",
|
||||
})
|
||||
end
|
||||
|
||||
-- Highlight date
|
||||
if date_start then
|
||||
vim.api.nvim_buf_set_extmark(buf, ns, i - 1, date_start - 1, {
|
||||
end_col = date_end,
|
||||
hl_group = "JJAnnotateDate",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Pads correctly the annotations so that all are the same length
|
||||
--- @param lines string[] The lines to format
|
||||
--- @return string[]
|
||||
local function align_annotations(lines)
|
||||
local parsed = {}
|
||||
local max_id, max_name, max_date = 0, 0, 0
|
||||
|
||||
for i, line in ipairs(lines) do
|
||||
if line ~= "" then
|
||||
local parsed_line = parser.parse_annotation_line(line)
|
||||
if parsed_line then
|
||||
local rev = parsed_line.rev.value
|
||||
local name = parsed_line.name.value
|
||||
local date = parsed_line.date.value
|
||||
|
||||
parsed[i] = { rev = rev, name = name, date = date }
|
||||
max_id = math.max(max_id, #rev)
|
||||
max_name = math.max(max_name, #name)
|
||||
max_date = math.max(max_date, #date)
|
||||
else
|
||||
parsed[i] = { raw = line }
|
||||
end
|
||||
else
|
||||
parsed[i] = { raw = "" }
|
||||
end
|
||||
end
|
||||
|
||||
-- Now that it has been parsed align with max values each line
|
||||
local result = {}
|
||||
for i, p in ipairs(parsed) do
|
||||
if p.rev then
|
||||
result[i] = string.format(
|
||||
"%-" .. max_id .. "s | %-" .. max_name .. "s | %-" .. max_date .. "s",
|
||||
p.rev,
|
||||
p.name,
|
||||
p.date
|
||||
)
|
||||
else
|
||||
result[i] = p.raw
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
local function handle_enter()
|
||||
-- Parse the current line to extract the revset
|
||||
local line = vim.api.nvim_get_current_line()
|
||||
local parts = parser.parse_annotation_line(line)
|
||||
if not parts or parts.rev.value == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the local name
|
||||
local filename = vim.b[0].jj_annotation_file
|
||||
|
||||
local cmd = string.format("jj diff --git -r %s %s", parts.rev.value, filename)
|
||||
|
||||
-- Run the command
|
||||
local output, success = runner.execute_command(cmd, "Could not run diff from annotation")
|
||||
if not success or not output or output == "" then
|
||||
return
|
||||
end
|
||||
|
||||
-- Create a new buffer with the filetype gitdiff and the output
|
||||
local buf, win = buffer.create({
|
||||
name = "jj-diff://" .. vim.fn.fnamemodify(filename, ":t") .. "//" .. parts.rev.value,
|
||||
split = "tab",
|
||||
filetype = "gitdiff",
|
||||
bufhidden = "wipe",
|
||||
})
|
||||
|
||||
-- Set the lines
|
||||
local lines = vim.split(output, "\n", { trimempty = true })
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
buffer.set_modified(buf, false)
|
||||
|
||||
-- Autoclose the tab when leaving it to another buffer
|
||||
vim.api.nvim_create_autocmd("BufLeave", {
|
||||
buffer = buf,
|
||||
callback = function()
|
||||
-- Only close if actually leaving to a different buffer
|
||||
if vim.api.nvim_get_current_buf() ~= buf then
|
||||
if vim.api.nvim_win_is_valid(win) then
|
||||
vim.cmd("tabclose")
|
||||
end
|
||||
end
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
--- Annotates the current file
|
||||
function M.file()
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
local template =
|
||||
'join(" | ", commit.change_id().short(6), commit.author().name(), commit.author().timestamp().format("%Y-%m-%d %H:%M:%S %Z")) ++ "\n"'
|
||||
|
||||
local filename = vim.api.nvim_buf_get_name(0)
|
||||
if filename == "" then
|
||||
utils.notify("Could extract file from buffer", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local raw_output, success = runner.execute_command(
|
||||
string.format("jj file annotate %s -T '%s'", filename, template),
|
||||
"Failed to annotate file"
|
||||
)
|
||||
if not success or not raw_output then
|
||||
return
|
||||
end
|
||||
|
||||
local annotations = vim.split(raw_output, "\n", { trimempty = true })
|
||||
|
||||
local size = 0
|
||||
for _, line in ipairs(annotations) do
|
||||
if line ~= "" then
|
||||
size = math.max(size, vim.fn.strdisplaywidth(line))
|
||||
end
|
||||
end
|
||||
|
||||
-- Capture source window/buffer state BEFORE creating split
|
||||
local source_win = vim.api.nvim_get_current_win()
|
||||
local source_buf = vim.api.nvim_get_current_buf()
|
||||
local source_topline = vim.fn.line("w0")
|
||||
local source_had_scrollbind = vim.wo[source_win].scrollbind
|
||||
local source_winbar = vim.wo[source_win].winbar
|
||||
|
||||
local buf = buffer.create({
|
||||
split = "vertical",
|
||||
direction = "left",
|
||||
bufhidden = "wipe",
|
||||
size = size + 2,
|
||||
win_options = {
|
||||
wrap = true,
|
||||
number = false,
|
||||
relativenumber = false,
|
||||
cursorline = false,
|
||||
signcolumn = "no",
|
||||
scrollbind = true,
|
||||
winbar = source_winbar,
|
||||
},
|
||||
})
|
||||
|
||||
-- Set local variables to the buffer
|
||||
vim.b[buf].jj_annotation_file = filename
|
||||
|
||||
-- Align annotations and set the text into the buffer
|
||||
annotations = align_annotations(annotations)
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, annotations)
|
||||
|
||||
setup_blame_highlighting(buf, annotations)
|
||||
buffer.set_modifiable(buf, false)
|
||||
|
||||
-- Get annotation window (current after buffer. create)
|
||||
local annotation_win = vim.api.nvim_get_current_win()
|
||||
|
||||
-- Set annotation window to match source scroll position
|
||||
vim.api.nvim_win_set_cursor(annotation_win, { source_topline, 0 })
|
||||
vim.cmd("normal! zt")
|
||||
|
||||
-- Enable scrollbind on source window
|
||||
vim.wo[source_win].scrollbind = true
|
||||
|
||||
-- Sync them
|
||||
vim.cmd("syncbind")
|
||||
|
||||
-- Create an augroup for this annotation session so we can clean up all autocmds together
|
||||
local augroup = vim.api.nvim_create_augroup("JJAnnotate" .. buf, { clear = true })
|
||||
|
||||
-- When source buffer leaves its window (fuzzy finder, : e, etc.)
|
||||
vim.api.nvim_create_autocmd("BufLeave", {
|
||||
group = augroup,
|
||||
buffer = source_buf,
|
||||
callback = function()
|
||||
if vim.api.nvim_win_is_valid(source_win) then
|
||||
vim.wo[source_win].scrollbind = false
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- When source buffer comes back to a window
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = augroup,
|
||||
buffer = source_buf,
|
||||
callback = function()
|
||||
-- Only re-enable if annotation window still exists
|
||||
if vim.api.nvim_win_is_valid(annotation_win) and vim.api.nvim_buf_is_valid(buf) then
|
||||
vim.wo[source_win].scrollbind = true
|
||||
vim.cmd("syncbind")
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Clean up everything when annotation buffer is destroyed
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = augroup,
|
||||
buffer = buf,
|
||||
once = true,
|
||||
callback = function()
|
||||
-- Restore original scrollbind state
|
||||
if vim.api.nvim_win_is_valid(source_win) and not source_had_scrollbind then
|
||||
vim.wo[source_win].scrollbind = false
|
||||
end
|
||||
-- Clear the augroup (removes all autocmds in it)
|
||||
vim.api.nvim_del_augroup_by_id(augroup)
|
||||
end,
|
||||
})
|
||||
|
||||
-- Set the keymap enter
|
||||
buffer.set_keymaps(buf, {
|
||||
{ modes = { "n", "v" }, lhs = "<CR>", rhs = handle_enter, { desc = "Show diff" } },
|
||||
})
|
||||
end
|
||||
|
||||
--- Annotates the current line
|
||||
function M.line()
|
||||
if not utils.ensure_jj() then
|
||||
return
|
||||
end
|
||||
|
||||
-- If tooltip exists and is valid, just focus it
|
||||
if last_tooltip_buf and vim.api.nvim_buf_is_valid(last_tooltip_buf) then
|
||||
local win = vim.fn.bufwinid(last_tooltip_buf)
|
||||
if win ~= -1 then
|
||||
vim.api.nvim_set_current_win(win)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
local template =
|
||||
'join(" | ", commit.change_id().short(6), commit.author().name(), commit.author().timestamp().format("%Y-%m-%d %H:%M:%S %Z")) ++ "\n"'
|
||||
|
||||
local filename = vim.api.nvim_buf_get_name(0)
|
||||
if filename == "" then
|
||||
utils.notify("Could not extract file from buffer", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local line_num = vim.fn.line(".")
|
||||
local raw_output, success = runner.execute_command(
|
||||
string.format("jj file annotate %s -T '%s'", filename, template),
|
||||
"Failed to annotate line"
|
||||
)
|
||||
if not success or not raw_output then
|
||||
return
|
||||
end
|
||||
|
||||
local all_annotations = vim.split(raw_output, "\n", { trimempty = true })
|
||||
local annotation_line = all_annotations[line_num]
|
||||
if not annotation_line or annotation_line == "" then
|
||||
utils.notify("Could not get annotation for line", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Get the description of the current rev
|
||||
local parsed_line = parser.parse_annotation_line(annotation_line)
|
||||
if not parsed_line then
|
||||
utils.notify("Could not extract revset from annotation line", vim.log.levels.ERROR)
|
||||
return
|
||||
end
|
||||
|
||||
local desc, ok = runner.execute_command(
|
||||
string.format("jj log -r %s -T 'self.description()' --no-graph", parsed_line.rev.value),
|
||||
"Failed getting description"
|
||||
)
|
||||
if not ok or not desc then
|
||||
return
|
||||
end
|
||||
|
||||
local text = annotation_line
|
||||
if desc ~= "" then
|
||||
text = vim.trim(annotation_line .. ":" .. "\n" .. desc)
|
||||
end
|
||||
|
||||
local buf, _ = buffer.create_tooltip({
|
||||
text = text,
|
||||
})
|
||||
|
||||
setup_blame_highlighting(buf, { text })
|
||||
|
||||
-- Store the buffer to enter it if re-executed
|
||||
last_tooltip_buf = buf
|
||||
-- Add autcmd to clean the cache when the buffer gets closed
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
buffer = buf,
|
||||
once = true,
|
||||
callback = function()
|
||||
last_tooltip_buf = nil
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -5,6 +5,7 @@ local utils = require("jj.utils")
|
||||
local runner = require("jj.core.runner")
|
||||
local parser = require("jj.core.parser")
|
||||
local terminal = require("jj.ui.terminal")
|
||||
|
||||
local diff = require("jj.diff")
|
||||
local log_module = require("jj.cmd.log")
|
||||
local describe_module = require("jj.cmd.describe")
|
||||
@@ -738,6 +739,12 @@ function M.j(args)
|
||||
terminal.run(cmd, M.terminal_keymaps())
|
||||
end
|
||||
end,
|
||||
annotate = function()
|
||||
require("jj.annotate").file()
|
||||
end,
|
||||
annotate_line = function()
|
||||
require("jj.annotate").line()
|
||||
end,
|
||||
}
|
||||
|
||||
if handlers[subcommand] then
|
||||
@@ -782,6 +789,8 @@ function M.register_command()
|
||||
"status",
|
||||
"undo",
|
||||
"open_pr",
|
||||
"annotate",
|
||||
"annotate_line",
|
||||
}
|
||||
local matches = {}
|
||||
for _, cmd in ipairs(subcommands) do
|
||||
|
||||
+105
-9
@@ -4,12 +4,15 @@ local M = {}
|
||||
--- @class jj.core.buffer.opts
|
||||
--- @field name? string Buffer name
|
||||
--- @field split? "horizontal"|"vertical"|"tab"|"current" Split type (default: "horizontal")
|
||||
--- @field direction? "left"|"right"|"top"|"bottom" Split direction (left/right for vertical, top/bottom for horizontal)
|
||||
--- @field size? number Split size in lines/columns
|
||||
--- @field modifiable? boolean Whether buffer is modifiable (default: true)
|
||||
--- @field filetype? string Filetype to set
|
||||
--- @field buftype? string Buffer type (e.g., "nofile", "acwrite", etc. - optional, defaults to scratch buffer)
|
||||
--- @field bufhidden? string Buffer hidden behavior (default: "hide")
|
||||
--- @field on_exit? fun(buf: number) Callback when buffer is closed
|
||||
--- @field keymaps? jj.core.buffer.keymap[] Keymaps to set on the buffer
|
||||
--- @field win_options? table Window-specific options to set
|
||||
|
||||
--- @class jj.core.buffer.keymap
|
||||
--- @field modes? string|string[] Modes for the keymap (default: "n")
|
||||
@@ -47,26 +50,39 @@ function M.create(opts)
|
||||
local win = nil
|
||||
|
||||
-- Handle window/split creation
|
||||
local buf
|
||||
if opts.split == "vertical" then
|
||||
local direction = opts.direction or "right"
|
||||
local width = opts.size or math.floor(vim.o.columns / 2)
|
||||
vim.cmd(string.format("vsplit | vertical resize %d", width))
|
||||
if direction == "left" then
|
||||
vim.cmd("leftabove vsplit")
|
||||
else
|
||||
vim.cmd("vsplit")
|
||||
end
|
||||
vim.cmd(string.format("vertical resize %d", width))
|
||||
win = vim.api.nvim_get_current_win()
|
||||
buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
elseif opts.split == "tab" then
|
||||
vim.cmd("tabnew")
|
||||
win = vim.api.nvim_get_current_win()
|
||||
-- Use the buffer that tabnew created
|
||||
buf = vim.api.nvim_get_current_buf()
|
||||
elseif opts.split == "current" then
|
||||
win = vim.api.nvim_get_current_win()
|
||||
buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
else -- horizontal (default)
|
||||
local direction = opts.direction or "bottom"
|
||||
local height = opts.size or math.floor(vim.o.lines / 2)
|
||||
vim.cmd(string.format("split | resize %d", height))
|
||||
if direction == "top" then
|
||||
vim.cmd("topleft split")
|
||||
else
|
||||
vim.cmd("split")
|
||||
end
|
||||
vim.cmd(string.format("horizontal resize %d", height))
|
||||
win = vim.api.nvim_get_current_win()
|
||||
end
|
||||
|
||||
-- Create buffer
|
||||
local buf = vim.api.nvim_create_buf(false, true)
|
||||
|
||||
-- Set buffer in window if we created/got a window
|
||||
if win then
|
||||
buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.api.nvim_win_set_buf(win, buf)
|
||||
end
|
||||
|
||||
@@ -83,6 +99,10 @@ function M.create(opts)
|
||||
vim.bo[buf].swapfile = false
|
||||
vim.bo[buf].buflisted = false
|
||||
|
||||
if opts.bufhidden then
|
||||
vim.bo[buf].bufhidden = opts.bufhidden
|
||||
end
|
||||
|
||||
-- Set filetype if provided
|
||||
if opts.filetype then
|
||||
vim.bo[buf].filetype = opts.filetype
|
||||
@@ -93,6 +113,13 @@ function M.create(opts)
|
||||
M.set_keymaps(buf, opts.keymaps)
|
||||
end
|
||||
|
||||
-- Set window options
|
||||
if opts.win_options then
|
||||
for option, value in pairs(opts.win_options) do
|
||||
vim.wo[win][option] = value
|
||||
end
|
||||
end
|
||||
|
||||
-- Set up cleanup autocmd if on_exit callback provided
|
||||
if opts.on_exit then
|
||||
vim.api.nvim_create_autocmd({ "BufWipeout", "BufDelete" }, {
|
||||
@@ -209,6 +236,65 @@ function M.create_float(opts)
|
||||
return buf, win
|
||||
end
|
||||
|
||||
--- Creates a tooltip floating buffer
|
||||
--- @param opts {text: string, timeout?: number, title?: string} Tooltip options
|
||||
--- @return number buf Buffer handle
|
||||
--- @return number win Window handle
|
||||
function M.create_tooltip(opts)
|
||||
opts = opts or {}
|
||||
|
||||
-- Calculate size based on text
|
||||
local text = opts.text or ""
|
||||
local lines = vim.split(text, "\n", { trimempty = false })
|
||||
local max_width = 0
|
||||
for _, line in ipairs(lines) do
|
||||
max_width = math.max(max_width, vim.fn.strdisplaywidth(line))
|
||||
end
|
||||
|
||||
-- Create float with tooltip defaults
|
||||
local buf, win = M.create_float({
|
||||
width = math.min(max_width + 2, vim.o.columns - 4),
|
||||
height = #lines,
|
||||
relative = "cursor",
|
||||
row = 1,
|
||||
col = 0,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = opts.title,
|
||||
modifiable = false,
|
||||
buftype = "nofile",
|
||||
bufhidden = "wipe",
|
||||
})
|
||||
|
||||
-- Set the text
|
||||
vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines)
|
||||
vim.bo[buf].modified = false
|
||||
|
||||
-- Close when cursor moves in other windows
|
||||
vim.api.nvim_create_autocmd("CursorMoved", {
|
||||
callback = function()
|
||||
if vim.api.nvim_win_is_valid(win) and vim.api.nvim_get_current_win() ~= win then
|
||||
M.close(buf, true)
|
||||
end
|
||||
end,
|
||||
})
|
||||
|
||||
-- Add Esc/q keymap to close tooltip
|
||||
vim.keymap.set("n", "<Esc>", function()
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
M.close(buf, true)
|
||||
end
|
||||
end, { buffer = buf, silent = true })
|
||||
|
||||
vim.keymap.set("n", "q", function()
|
||||
if vim.api.nvim_buf_is_valid(buf) then
|
||||
M.close(buf, true)
|
||||
end
|
||||
end, { buffer = buf, silent = true })
|
||||
|
||||
return buf, win
|
||||
end
|
||||
|
||||
--- Close/wipe a buffer safely
|
||||
--- @param buf number Buffer handle
|
||||
--- @param force? boolean Force close (default: true)
|
||||
@@ -272,6 +358,16 @@ function M.set_modifiable(buf, modifiable)
|
||||
vim.bo[buf].modifiable = modifiable
|
||||
end
|
||||
|
||||
--- Set buffer as modified or not
|
||||
--- @param buf number Buffer handle
|
||||
--- @param modified boolean Whether buffer should be modifiable
|
||||
function M.set_modified(buf, modified)
|
||||
if not vim.api.nvim_buf_is_valid(buf) then
|
||||
return
|
||||
end
|
||||
vim.bo[buf].modified = modified
|
||||
end
|
||||
|
||||
--- Stop insert mode if in the given buffer if the cursor is currently in that buffer
|
||||
--- @param buf number Buffer handle
|
||||
function M.stop_insert(buf)
|
||||
|
||||
+32
-1
@@ -52,7 +52,7 @@ end
|
||||
|
||||
--- Parse the current line in the jj status buffer to extract file information.
|
||||
--- Handles renamed files and regular status lines.
|
||||
--- @return table|nil A table with {old_path = string, new_path = string, is_rename = boolean}, or nil if parsing fails
|
||||
--- @return {old_path : string, new_path : string, is_rename : boolean}|nil A table with , or nil if parsing fails
|
||||
function M.parse_file_info_from_status_line(line)
|
||||
-- Handle renamed files: "R path/{old_name => new_name}" or "R old_path => new_path"
|
||||
local rename_pattern_curly = "^R (.*)/{(.*) => ([^}]+)}"
|
||||
@@ -147,4 +147,35 @@ function M.get_rev_from_log_line(line)
|
||||
return revset
|
||||
end
|
||||
|
||||
--- Given an annotation line, parses and returns its components with positions
|
||||
--- @param line string The annotation line to parse
|
||||
--- @return table A table with {rev = {value = string|nil, pos = {start, end}|nil}, name = {value = string|nil, pos = {start, end}|nil}, date = {value = string|nil, pos = {start, end}|nil}}
|
||||
function M.parse_annotation_line(line)
|
||||
local rev, name, date = line:match("^(%S+)%s*|%s*(.-)%s*|%s*(.+)$")
|
||||
|
||||
local result = {
|
||||
rev = { value = rev },
|
||||
name = { value = name },
|
||||
date = { value = date },
|
||||
}
|
||||
|
||||
if rev then
|
||||
local id_start, id_end = line:find("^(%S+)")
|
||||
result.rev.pos = { id_start, id_end }
|
||||
end
|
||||
|
||||
if name then
|
||||
local name_start = line:find("|") + 2
|
||||
local name_end = line:find("|", name_start) - 2 -- Unsure about this one since it can have N whitespaces but we'll see
|
||||
result.name.pos = { name_start, name_end + 1 }
|
||||
end
|
||||
|
||||
if date then
|
||||
local date_start, date_end = line:find("%d%d%d%d%-%d%d%-%d%d%s%d%d:%d%d:%d%d%s[%+%-]%d%d:%d%d")
|
||||
result.date.pos = { date_start, date_end }
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -80,4 +80,4 @@ function M.execute_command_async(cmd, on_success, error_prefix, input, silent)
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
return M
|
||||
|
||||
Reference in New Issue
Block a user