Skip to content

Commit 0f0a8f2

Browse files
committed
feat(renderer): add smart grep renderer for formatting grep-like results
1 parent 8090011 commit 0f0a8f2

File tree

2 files changed

+206
-0
lines changed

2 files changed

+206
-0
lines changed

builtin/renderer/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export * from "./helptag.ts";
66
export * from "./nerdfont.ts";
77
export * from "./noop.ts";
88
export * from "./relative_path.ts";
9+
export * from "./smart_grep.ts";
910
export * from "./smart_path.ts";

builtin/renderer/smart_grep.ts

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import { defineRenderer, type Renderer } from "../../renderer.ts";
2+
import { dirname } from "@std/path/dirname";
3+
4+
type Detail = {
5+
/**
6+
* File path
7+
*/
8+
path: string;
9+
10+
/**
11+
* Line number
12+
*/
13+
line?: number;
14+
15+
/**
16+
* Column number
17+
*/
18+
column?: number;
19+
20+
/**
21+
* Matched text or line content
22+
*/
23+
text?: string;
24+
};
25+
26+
export type SmartGrepOptions = {
27+
/**
28+
* Maximum length for displayed text.
29+
* @default 80
30+
*/
31+
maxTextLength?: number;
32+
33+
/**
34+
* Whether to show line and column numbers.
35+
* @default true
36+
*/
37+
showLineNumbers?: boolean;
38+
39+
/**
40+
* Whether to group results by directory.
41+
* @default false
42+
*/
43+
groupByDirectory?: boolean;
44+
45+
/**
46+
* Whether to use relative paths.
47+
* @default true
48+
*/
49+
useRelativePaths?: boolean;
50+
51+
/**
52+
* Whether to colorize output (using ANSI codes).
53+
* @default false
54+
*/
55+
colorize?: boolean;
56+
57+
/**
58+
* Whether to align columns.
59+
* @default true
60+
*/
61+
alignColumns?: boolean;
62+
};
63+
64+
/**
65+
* Creates a Renderer that formats grep-like results in a smart way.
66+
*
67+
* This Renderer reformats items that contain file paths, line numbers,
68+
* and matched text into a more readable format with proper alignment
69+
* and optional grouping.
70+
*
71+
* @param options - Options to customize smart grep display.
72+
* @returns A Renderer that reformats grep-like results.
73+
*/
74+
export function smartGrep(
75+
options: Readonly<SmartGrepOptions> = {},
76+
): Renderer<Detail> {
77+
const maxTextLength = options.maxTextLength ?? 80;
78+
const showLineNumbers = options.showLineNumbers ?? true;
79+
const groupByDirectory = options.groupByDirectory ?? false;
80+
const useRelativePaths = options.useRelativePaths ?? true;
81+
const colorize = options.colorize ?? false;
82+
const alignColumns = options.alignColumns ?? true;
83+
84+
return defineRenderer((_denops, { items }) => {
85+
if (items.length === 0) {
86+
return;
87+
}
88+
89+
// Calculate maximum widths for alignment
90+
let maxPathWidth = 0;
91+
let maxLineWidth = 0;
92+
let maxColumnWidth = 0;
93+
94+
if (alignColumns) {
95+
for (const item of items) {
96+
const { path, line, column } = item.detail;
97+
const displayPath = useRelativePaths ? path : path;
98+
maxPathWidth = Math.max(maxPathWidth, displayPath.length);
99+
if (line !== undefined) {
100+
maxLineWidth = Math.max(maxLineWidth, line.toString().length);
101+
}
102+
if (column !== undefined) {
103+
maxColumnWidth = Math.max(maxColumnWidth, column.toString().length);
104+
}
105+
}
106+
}
107+
108+
// Group items by directory if requested
109+
if (groupByDirectory) {
110+
const groups = new Map<string, typeof items>();
111+
112+
for (const item of items) {
113+
const dir = dirname(item.detail.path);
114+
if (!groups.has(dir)) {
115+
groups.set(dir, []);
116+
}
117+
groups.get(dir)!.push(item);
118+
}
119+
120+
// Process each group
121+
let currentDir = "";
122+
for (const item of items) {
123+
const dir = dirname(item.detail.path);
124+
125+
// Add directory header if changed
126+
if (dir !== currentDir) {
127+
currentDir = dir;
128+
// Modify the first item in each directory group to include header
129+
const dirHeader = `=== ${dir} ===`;
130+
if (colorize) {
131+
item.label = `\x1b[36m${dirHeader}\x1b[0m\n${formatItem(item)}`;
132+
} else {
133+
item.label = `${dirHeader}\n${formatItem(item)}`;
134+
}
135+
} else {
136+
item.label = formatItem(item);
137+
}
138+
}
139+
} else {
140+
// Format each item individually
141+
for (const item of items) {
142+
item.label = formatItem(item);
143+
}
144+
}
145+
146+
function formatItem(item: typeof items[0]): string {
147+
const { path, line, column, text } = item.detail;
148+
const parts: string[] = [];
149+
150+
// Format path
151+
const displayPath = useRelativePaths ? path : path;
152+
const pathPart = alignColumns
153+
? displayPath.padEnd(maxPathWidth)
154+
: displayPath;
155+
156+
if (colorize) {
157+
parts.push(`\x1b[35m${pathPart}\x1b[0m`); // Magenta for path
158+
} else {
159+
parts.push(pathPart);
160+
}
161+
162+
// Format line and column numbers
163+
if (showLineNumbers && line !== undefined) {
164+
const lineStr = alignColumns
165+
? line.toString().padStart(maxLineWidth)
166+
: line.toString();
167+
168+
if (column !== undefined) {
169+
const colStr = alignColumns
170+
? column.toString().padStart(maxColumnWidth)
171+
: column.toString();
172+
173+
if (colorize) {
174+
parts.push(`\x1b[32m${lineStr}:${colStr}\x1b[0m`); // Green for numbers
175+
} else {
176+
parts.push(`${lineStr}:${colStr}`);
177+
}
178+
} else {
179+
if (colorize) {
180+
parts.push(`\x1b[32m${lineStr}\x1b[0m`); // Green for line number
181+
} else {
182+
parts.push(lineStr);
183+
}
184+
}
185+
}
186+
187+
// Format text
188+
if (text) {
189+
let displayText = text.trim();
190+
if (displayText.length > maxTextLength) {
191+
displayText = displayText.substring(0, maxTextLength - 3) + "...";
192+
}
193+
194+
if (colorize) {
195+
// Try to highlight the matched part (simple approach)
196+
parts.push(`\x1b[37m${displayText}\x1b[0m`); // White for text
197+
} else {
198+
parts.push(displayText);
199+
}
200+
}
201+
202+
return parts.join(" ");
203+
}
204+
});
205+
}

0 commit comments

Comments
 (0)