Skip to content

Commit e9a3319

Browse files
committed
feat(refiner): add file info refiner for filtering by file properties
1 parent 0f0a8f2 commit e9a3319

File tree

2 files changed

+190
-0
lines changed

2 files changed

+190
-0
lines changed

builtin/refiner/file_info.ts

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import type { IdItem } from "@vim-fall/core/item";
2+
import { defineRefiner, type Refiner } from "../../refiner.ts";
3+
import { extname } from "@std/path/extname";
4+
5+
type Detail = {
6+
/**
7+
* File path
8+
*/
9+
path: string;
10+
};
11+
12+
export type FileInfoRefinerOptions = {
13+
/**
14+
* Filter by file extensions.
15+
* If provided, only files with these extensions will pass.
16+
*/
17+
extensions?: string[];
18+
19+
/**
20+
* Filter by file size.
21+
* Files must be within this range (in bytes).
22+
*/
23+
sizeRange?: {
24+
min?: number;
25+
max?: number;
26+
};
27+
28+
/**
29+
* Filter by modification time.
30+
* Files must be modified within this time range.
31+
*/
32+
modifiedWithin?: {
33+
days?: number;
34+
hours?: number;
35+
minutes?: number;
36+
};
37+
38+
/**
39+
* Whether to include directories.
40+
* @default true
41+
*/
42+
includeDirectories?: boolean;
43+
44+
/**
45+
* Whether to include files.
46+
* @default true
47+
*/
48+
includeFiles?: boolean;
49+
50+
/**
51+
* Whether to include symlinks.
52+
* @default true
53+
*/
54+
includeSymlinks?: boolean;
55+
56+
/**
57+
* Whether to exclude hidden files (starting with dot).
58+
* @default false
59+
*/
60+
excludeHidden?: boolean;
61+
62+
/**
63+
* Patterns to exclude (glob patterns).
64+
*/
65+
excludePatterns?: string[];
66+
};
67+
68+
/**
69+
* Creates a Refiner that filters items based on file information.
70+
*
71+
* This Refiner can filter files based on various criteria such as
72+
* file extension, size, modification time, and file type.
73+
*
74+
* @param options - Options to customize file filtering.
75+
* @returns A Refiner that filters items based on file information.
76+
*/
77+
export function fileInfo(
78+
options: Readonly<FileInfoRefinerOptions> = {},
79+
): Refiner<Detail> {
80+
const extensions = options.extensions;
81+
const sizeRange = options.sizeRange;
82+
const modifiedWithin = options.modifiedWithin;
83+
const includeDirectories = options.includeDirectories ?? true;
84+
const includeFiles = options.includeFiles ?? true;
85+
const includeSymlinks = options.includeSymlinks ?? true;
86+
const excludeHidden = options.excludeHidden ?? false;
87+
const excludePatterns = options.excludePatterns ?? [];
88+
89+
return defineRefiner(async function* (_denops, { items }) {
90+
// Convert async iterable to array first
91+
const itemsArray: IdItem<Detail>[] = [];
92+
for await (const item of items) {
93+
itemsArray.push(item);
94+
}
95+
96+
// Process items in parallel and filter
97+
const results = await Promise.all(
98+
itemsArray.map(async (item) => {
99+
const { path } = item.detail;
100+
101+
// Check if hidden file should be excluded
102+
if (
103+
excludeHidden &&
104+
path.split("/").some((part: string) => part.startsWith("."))
105+
) {
106+
return null;
107+
}
108+
109+
// Check exclude patterns
110+
for (const pattern of excludePatterns) {
111+
// Simple glob pattern matching (could be enhanced)
112+
const regex = new RegExp(
113+
pattern.replace(/\*/g, ".*").replace(/\?/g, "."),
114+
);
115+
if (regex.test(path)) {
116+
return null;
117+
}
118+
}
119+
120+
// Check extension filter
121+
if (extensions && extensions.length > 0) {
122+
const ext = extname(path).toLowerCase();
123+
if (!extensions.includes(ext)) {
124+
return null;
125+
}
126+
}
127+
128+
try {
129+
// Get file stats
130+
const stat = await Deno.stat(path);
131+
132+
// Check file type filters
133+
if (!includeFiles && stat.isFile) {
134+
return null;
135+
}
136+
if (!includeDirectories && stat.isDirectory) {
137+
return null;
138+
}
139+
if (!includeSymlinks && stat.isSymlink) {
140+
return null;
141+
}
142+
143+
// Check size filter
144+
if (sizeRange && stat.isFile) {
145+
if (sizeRange.min !== undefined && stat.size < sizeRange.min) {
146+
return null;
147+
}
148+
if (sizeRange.max !== undefined && stat.size > sizeRange.max) {
149+
return null;
150+
}
151+
}
152+
153+
// Check modification time filter
154+
if (modifiedWithin && stat.mtime) {
155+
const now = new Date();
156+
const mtime = stat.mtime;
157+
const diffMs = now.getTime() - mtime.getTime();
158+
159+
let maxMs = 0;
160+
if (modifiedWithin.days !== undefined) {
161+
maxMs += modifiedWithin.days * 24 * 60 * 60 * 1000;
162+
}
163+
if (modifiedWithin.hours !== undefined) {
164+
maxMs += modifiedWithin.hours * 60 * 60 * 1000;
165+
}
166+
if (modifiedWithin.minutes !== undefined) {
167+
maxMs += modifiedWithin.minutes * 60 * 1000;
168+
}
169+
170+
if (diffMs > maxMs) {
171+
return null;
172+
}
173+
}
174+
175+
return item;
176+
} catch {
177+
// If stat fails, exclude the item
178+
return null;
179+
}
180+
}),
181+
);
182+
183+
// Return only non-null items
184+
const filtered = results.filter((item): item is IdItem<Detail> =>
185+
item !== null
186+
);
187+
yield* filtered;
188+
});
189+
}

builtin/refiner/mod.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
export * from "./absolute_path.ts";
33
export * from "./cwd.ts";
44
export * from "./exists.ts";
5+
export * from "./file_info.ts";
56
export * from "./noop.ts";
67
export * from "./regexp.ts";
78
export * from "./relative_path.ts";

0 commit comments

Comments
 (0)