Skip to content

Commit 4033d58

Browse files
authored
Merge pull request #165 from lambdalisue/add-ginbame
feat: Add GinBlame command for interactive git blame navigation
2 parents a75dfa2 + 9a803ec commit 4033d58

File tree

20 files changed

+2869
-0
lines changed

20 files changed

+2869
-0
lines changed

CLAUDE.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Coding Guidelines for vim-gin
2+
3+
## Comments
4+
5+
**All code comments MUST be written in English.**
6+
7+
This applies to:
8+
9+
- Function/class documentation comments
10+
- Inline comments explaining logic
11+
- TODO/FIXME/NOTE comments
12+
- Any other comment in the codebase
13+
14+
### Rationale
15+
16+
- Ensures consistency across the codebase
17+
- Makes the code accessible to international contributors
18+
- Follows common open-source project conventions
19+
20+
### Examples
21+
22+
Good example:
23+
24+
```
25+
// Disable visual features that affect line number display
26+
await disableVisualLineModifications(denops, bufnr);
27+
```
28+
29+
Bad example (using non-English comments):
30+
31+
```
32+
// Disable visual line modifications
33+
await disableVisualLineModifications(denops, bufnr);
34+
```
35+
36+
Good example (JSDoc):
37+
38+
```
39+
/**
40+
* Parse git blame porcelain output
41+
* @param content - Raw output from git blame --porcelain
42+
* @returns Parsed blame result with commits and lines
43+
*/
44+
```
45+
46+
Bad example (non-English comments):
47+
48+
```
49+
/**
50+
* Git blame の porcelain 出力を解析する
51+
* @param content - git blame --porcelain の出力
52+
* @returns パースされた blame 結果
53+
*/
54+
```

after/ftplugin/gin-blame.vim

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
" This file is in after/ftplugin/ to ensure it runs AFTER other filetypes' ftplugin
2+
" For compound filetypes like 'typescript.gin-blame', Vim loads:
3+
" 1. ftplugin/typescript.vim
4+
" 2. ftplugin/gin-blame.vim
5+
" 3. after/ftplugin/typescript.vim
6+
" 4. after/ftplugin/gin-blame.vim <- This ensures gin-blame mappings override others
7+
" This is necessary because some filetypes (e.g. typescript) may define <C-O>/<C-I> mappings
8+
" that would conflict with gin-blame's history navigation.
9+
10+
if exists('b:did_ftplugin_gin_blame_after')
11+
finish
12+
endif
13+
let b:did_ftplugin_gin_blame_after = 1
14+
15+
" Define Plug mappings for this buffer
16+
nnoremap <buffer> <silent> <Plug>(gin-blame-switch-commit) <Cmd>call denops#notify('gin', 'blame:switch_to_commit', [])<CR>
17+
nnoremap <buffer> <silent> <Plug>(gin-blame-navigate-older) <Cmd>call denops#notify('gin', 'blame:navigate_history', ['older'])<CR>
18+
nnoremap <buffer> <silent> <Plug>(gin-blame-navigate-newer) <Cmd>call denops#notify('gin', 'blame:navigate_history', ['newer'])<CR>
19+
20+
" Default mappings (can be disabled with g:gin_blame_disable_default_mappings)
21+
if !get(g:, 'gin_blame_disable_default_mappings', 0)
22+
nmap <buffer> <CR> <Plug>(gin-blame-switch-commit)
23+
nmap <buffer> <C-O> <Plug>(gin-blame-navigate-older)
24+
nmap <buffer> <C-I> <Plug>(gin-blame-navigate-newer)
25+
endif
26+
27+
" Undo ftplugin settings
28+
let b:undo_ftplugin_gin_blame_after = 'silent! nunmap <buffer> <CR> |'
29+
\ . ' silent! nunmap <buffer> <C-O> |'
30+
\ . ' silent! nunmap <buffer> <C-I> |'
31+
\ . ' silent! unmap <buffer> <Plug>(gin-blame-switch-commit) |'
32+
\ . ' silent! unmap <buffer> <Plug>(gin-blame-navigate-older) |'
33+
\ . ' silent! unmap <buffer> <Plug>(gin-blame-navigate-newer)'
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import type { Denops } from "jsr:@denops/std@^7.0.0";
2+
import * as fn from "jsr:@denops/std@^7.0.0/function";
3+
import * as vars from "jsr:@denops/std@^7.0.0/variable";
4+
import {
5+
type BufnameParams,
6+
parse as parseBufname,
7+
} from "jsr:@denops/std@^7.0.0/bufname";
8+
import type { GitBlameLine, GitBlameResult } from "./parser.ts";
9+
10+
/**
11+
* Information about the current blame buffer context
12+
*/
13+
export type BlameBufferContext = {
14+
bufnrCurrent: number;
15+
bufnrNav: number;
16+
bufnrBlame: number;
17+
scheme: string;
18+
expr: string;
19+
params: BufnameParams | undefined;
20+
fileFragment: string;
21+
blameResult: GitBlameResult;
22+
};
23+
24+
/**
25+
* Get blame buffer context from current buffer
26+
*/
27+
export async function getBlameContext(
28+
denops: Denops,
29+
): Promise<BlameBufferContext> {
30+
const bufnrCurrent = await fn.bufnr(denops);
31+
const bufnameCurrent = await fn.bufname(denops, bufnrCurrent);
32+
const { scheme, expr, params } = parseBufname(bufnameCurrent);
33+
34+
// Determine which buffer we're in and get the paired buffers
35+
let bufnrNav: number;
36+
let bufnrBlame: number;
37+
38+
if (scheme === "ginblamenav") {
39+
bufnrNav = bufnrCurrent;
40+
const bufnameBlame = await vars.b.get(denops, "gin_blame_file_bufname") as
41+
| string
42+
| undefined;
43+
if (!bufnameBlame) {
44+
throw new Error("Cannot find associated ginblame buffer");
45+
}
46+
bufnrBlame = await fn.bufnr(denops, bufnameBlame);
47+
} else if (scheme === "ginblame") {
48+
bufnrBlame = bufnrCurrent;
49+
const bufnameNav = await vars.b.get(denops, "gin_blame_nav_bufname") as
50+
| string
51+
| undefined;
52+
if (!bufnameNav) {
53+
throw new Error("Cannot find associated ginblamenav buffer");
54+
}
55+
bufnrNav = await fn.bufnr(denops, bufnameNav);
56+
} else {
57+
throw new Error(
58+
"This command can only be called from ginblame or ginblamenav buffer",
59+
);
60+
}
61+
62+
const fileFragment = await vars.b.get(denops, "gin_blame_file_fragment") as
63+
| string
64+
| undefined;
65+
if (!fileFragment) {
66+
throw new Error("File fragment not found");
67+
}
68+
69+
const blameResult = await vars.b.get(denops, "gin_blame_result") as
70+
| GitBlameResult
71+
| undefined;
72+
if (!blameResult) {
73+
throw new Error("Blame result not found");
74+
}
75+
76+
if (bufnrBlame === -1) {
77+
throw new Error("Ginblame buffer not found");
78+
}
79+
80+
return {
81+
bufnrCurrent,
82+
bufnrNav,
83+
bufnrBlame,
84+
scheme,
85+
expr,
86+
params,
87+
fileFragment,
88+
blameResult,
89+
};
90+
}
91+
92+
/**
93+
* Get blame line from current cursor position using lineMap
94+
* Works for both ginblamenav and ginblame buffers
95+
*/
96+
export async function getBlameLine(
97+
denops: Denops,
98+
_scheme: string,
99+
lnum: number,
100+
_blameResult: GitBlameResult,
101+
): Promise<{ blameLine: GitBlameLine; relativeOffset: number } | null> {
102+
// Get the line map (physical line -> GitBlameLine)
103+
const lineMap = await vars.b.get(denops, "gin_blame_line_map") as
104+
| Record<number, GitBlameLine>
105+
| undefined;
106+
if (!lineMap) {
107+
return null;
108+
}
109+
110+
// Direct lookup by physical line number
111+
const blameLine = lineMap[lnum];
112+
if (!blameLine) {
113+
return null; // Empty line or divider
114+
}
115+
116+
return { blameLine, relativeOffset: 0 };
117+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { Denops } from "jsr:@denops/std@^7.0.0";
2+
import * as path from "jsr:@std/path@^1.0.0";
3+
import * as buffer from "jsr:@denops/std@^7.0.0/buffer";
4+
import * as option from "jsr:@denops/std@^7.0.0/option";
5+
import { format as formatBufname } from "jsr:@denops/std@^7.0.0/bufname";
6+
import { unnullish } from "jsr:@lambdalisue/unnullish@^1.0.0";
7+
import { findWorktreeFromDenops } from "../../git/worktree.ts";
8+
9+
export type ExecOptions = {
10+
worktree?: string;
11+
commitish?: string;
12+
emojify?: boolean;
13+
opener?: string;
14+
cmdarg?: string;
15+
mods?: string;
16+
bang?: boolean;
17+
};
18+
19+
export async function exec(
20+
denops: Denops,
21+
filename: string,
22+
options: ExecOptions = {},
23+
): Promise<buffer.OpenResult> {
24+
const verbose = await option.verbose.get(denops);
25+
26+
const worktree = await findWorktreeFromDenops(denops, {
27+
worktree: options.worktree,
28+
verbose: !!verbose,
29+
});
30+
31+
const relpath = path.isAbsolute(filename)
32+
? path.relative(worktree, filename)
33+
: filename;
34+
35+
const bufname = formatBufname({
36+
scheme: "ginblame",
37+
expr: worktree,
38+
params: {
39+
commitish: options.commitish,
40+
emojify: unnullish(options.emojify, (v) => v ? "" : undefined),
41+
},
42+
fragment: relpath,
43+
});
44+
return await buffer.open(denops, bufname.toString(), {
45+
opener: options.opener ?? "tabedit",
46+
cmdarg: options.cmdarg,
47+
mods: options.mods,
48+
bang: options.bang,
49+
});
50+
}

0 commit comments

Comments
 (0)