Skip to content

Commit 6ae2408

Browse files
lambdalisueclaude
andcommitted
feat: add vimgrep source for vim's :vimgrep command results
Implements a new source that executes vim's :vimgrep command and generates items from the results. Unlike :grep, :vimgrep uses vim's internal pattern matching. Supports quickfix integration and pattern search across files. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 94a9305 commit 6ae2408

File tree

1 file changed

+210
-0
lines changed

1 file changed

+210
-0
lines changed

builtin/source/vimgrep.ts

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import * as fn from "@denops/std/function";
2+
3+
import { defineSource, type Source } from "../../source.ts";
4+
5+
type Detail = {
6+
/**
7+
* File path
8+
*/
9+
path: string;
10+
11+
/**
12+
* Line number
13+
*/
14+
line: number;
15+
16+
/**
17+
* Column number
18+
*/
19+
column: number;
20+
21+
/**
22+
* Matched text
23+
*/
24+
text: string;
25+
26+
/**
27+
* The pattern that was searched
28+
*/
29+
pattern: string;
30+
};
31+
32+
export type VimgrepOptions = {
33+
/**
34+
* The pattern to search for.
35+
* If not provided, uses the last search pattern.
36+
*/
37+
pattern?: string;
38+
39+
/**
40+
* Files to search in.
41+
* Defaults to current file if not specified.
42+
*/
43+
files?: string[];
44+
45+
/**
46+
* Vimgrep flags.
47+
* g: all matches in a line
48+
* j: don't jump to first match
49+
* @default "j"
50+
*/
51+
flags?: string;
52+
53+
/**
54+
* Whether to use the quickfix list.
55+
* If false, returns results directly without populating quickfix.
56+
* @default true
57+
*/
58+
useQuickfix?: boolean;
59+
};
60+
61+
/**
62+
* Creates a Source that generates items from Vim's :vimgrep command results.
63+
*
64+
* This Source executes Vim's :vimgrep command and generates items from the results.
65+
* Unlike :grep, :vimgrep uses Vim's internal pattern matching.
66+
*
67+
* @param options - Options to customize vimgrep execution.
68+
* @returns A Source that generates items representing vimgrep results.
69+
*/
70+
export function vimgrep(
71+
options: Readonly<VimgrepOptions> = {},
72+
): Source<Detail> {
73+
const pattern = options.pattern;
74+
const files = options.files ?? ["%"];
75+
const flags = options.flags ?? "j";
76+
const useQuickfix = options.useQuickfix ?? true;
77+
78+
return defineSource(async function* (denops, _params, { signal }) {
79+
// Get the pattern to search for
80+
let searchPattern = pattern;
81+
if (!searchPattern) {
82+
// Use last search pattern
83+
searchPattern = await fn.getreg(denops, "/") as string;
84+
if (!searchPattern) {
85+
return;
86+
}
87+
}
88+
89+
signal?.throwIfAborted();
90+
91+
// Escape the pattern for vimgrep
92+
const escapedPattern = searchPattern.replace(/[\\\/]/g, "\\$&");
93+
94+
// Build vimgrep command
95+
const vimgrepCmd = `:vimgrep /${escapedPattern}/${flags} ${
96+
files.map((f) => `'${f.replace(/'/g, "''")}'`).join(" ")
97+
}`;
98+
99+
try {
100+
if (useQuickfix) {
101+
// Save current quickfix list
102+
const savedQflist = await fn.getqflist(denops);
103+
104+
// Execute vimgrep command
105+
await denops.cmd(`silent! ${vimgrepCmd}`);
106+
signal?.throwIfAborted();
107+
108+
// Get results from quickfix list
109+
const qflist = await fn.getqflist(denops) as Array<{
110+
bufnr: number;
111+
lnum: number;
112+
col: number;
113+
text: string;
114+
valid: number;
115+
}>;
116+
117+
// Restore original quickfix list
118+
await denops.call("setqflist", savedQflist);
119+
120+
let id = 0;
121+
for (const item of qflist) {
122+
if (!item.valid) {
123+
continue;
124+
}
125+
126+
// Get filename from buffer number
127+
let filename = "";
128+
if (item.bufnr > 0) {
129+
filename = await fn.bufname(denops, item.bufnr) as string;
130+
}
131+
132+
if (!filename) {
133+
continue;
134+
}
135+
136+
// Format display value
137+
const locationStr = `${filename}:${item.lnum}:${item.col}`;
138+
const value = `${locationStr}: ${item.text}`;
139+
140+
yield {
141+
id: id++,
142+
value,
143+
detail: {
144+
path: filename,
145+
line: item.lnum,
146+
column: item.col,
147+
text: item.text,
148+
pattern: searchPattern,
149+
},
150+
};
151+
}
152+
} else {
153+
// For non-quickfix mode, we still need to use quickfix internally
154+
// but we'll clean it up immediately
155+
const savedQflist = await fn.getqflist(denops);
156+
157+
await denops.cmd(`silent! ${vimgrepCmd}`);
158+
signal?.throwIfAborted();
159+
160+
const qflist = await fn.getqflist(denops) as Array<{
161+
bufnr: number;
162+
lnum: number;
163+
col: number;
164+
text: string;
165+
valid: number;
166+
}>;
167+
168+
// Immediately restore
169+
await denops.call("setqflist", savedQflist);
170+
171+
let id = 0;
172+
for (const item of qflist) {
173+
if (!item.valid) {
174+
continue;
175+
}
176+
177+
let filename = "";
178+
if (item.bufnr > 0) {
179+
filename = await fn.bufname(denops, item.bufnr) as string;
180+
}
181+
182+
if (!filename) {
183+
continue;
184+
}
185+
186+
const locationStr = `${filename}:${item.lnum}:${item.col}`;
187+
const value = `${locationStr}: ${item.text}`;
188+
189+
yield {
190+
id: id++,
191+
value,
192+
detail: {
193+
path: filename,
194+
line: item.lnum,
195+
column: item.col,
196+
text: item.text,
197+
pattern: searchPattern,
198+
},
199+
};
200+
}
201+
}
202+
} catch (error) {
203+
// Vimgrep might fail if no matches found
204+
// This is not an error condition
205+
if (error instanceof Error && !error.message.includes("E480")) {
206+
throw error;
207+
}
208+
}
209+
});
210+
}

0 commit comments

Comments
 (0)