mirror of
https://github.com/taskgenius/taskgenius-plugin.git
synced 2026-07-22 06:40:25 +00:00
feat(filter): enhance file filter manager and settings UI
- Add comprehensive file filter manager with pattern, path, and folder rules - Implement filter statistics tracking (matched/excluded files) - Add file filter settings tab with rule management UI - Support whitelist/blacklist modes for filtering - Remove deprecated advanced.excludePatterns in favor of filter rules
This commit is contained in:
parent
d4d9d02ba5
commit
c7db2b5c65
4 changed files with 105 additions and 70 deletions
|
|
@ -63,6 +63,10 @@ export function renderFileFilterSettingsTab(
|
|||
})
|
||||
);
|
||||
|
||||
// Filter scope selection has been deprecated; use per-rule scope instead
|
||||
// This block intentionally left as a no-op to maintain layout spacing.
|
||||
|
||||
|
||||
// Filter rules section
|
||||
new Setting(containerEl)
|
||||
.setName(t("File Filter Rules"))
|
||||
|
|
@ -106,6 +110,20 @@ export function renderFileFilterSettingsTab(
|
|||
renderRules();
|
||||
});
|
||||
|
||||
// Rule scope dropdown (per-rule)
|
||||
const scopeContainer = ruleContainer.createDiv({ cls: "file-filter-rule-scope" });
|
||||
scopeContainer.createEl("label", { text: t("Scope:") });
|
||||
new DropdownComponent(scopeContainer)
|
||||
.addOption("both", t("Both"))
|
||||
.addOption("inline", t("Inline"))
|
||||
.addOption("file", t("File"))
|
||||
.setValue((rule as any).scope || "both")
|
||||
.onChange(async (value: "both" | "inline" | "file") => {
|
||||
(rule as any).scope = value;
|
||||
await settingTab.plugin.saveSettings();
|
||||
debouncedUpdateStats();
|
||||
});
|
||||
|
||||
// Path input
|
||||
const pathContainer = ruleContainer.createDiv({
|
||||
cls: "file-filter-rule-path",
|
||||
|
|
@ -238,6 +256,8 @@ export function renderFileFilterSettingsTab(
|
|||
);
|
||||
|
||||
// Manual refresh button for statistics
|
||||
|
||||
|
||||
new Setting(containerEl)
|
||||
.setName(t("Refresh Statistics"))
|
||||
.setDesc(t("Manually refresh filter statistics to see current data"))
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@ import type {
|
|||
FileTaskPropertiesConfig,
|
||||
RelationshipsConfig,
|
||||
PerformanceConfig,
|
||||
AdvancedConfig,
|
||||
StatusMappingConfig
|
||||
} from "../../types/file-source";
|
||||
|
||||
|
|
@ -51,7 +50,8 @@ export const DEFAULT_FILE_TASK_PROPERTIES: FileTaskPropertiesConfig = {
|
|||
contentSource: "filename",
|
||||
stripExtension: true,
|
||||
defaultStatus: " ",
|
||||
defaultPriority: undefined
|
||||
defaultPriority: undefined,
|
||||
preferFrontmatterTitle: true
|
||||
};
|
||||
|
||||
/** Default configuration for relationships */
|
||||
|
|
@ -68,12 +68,6 @@ export const DEFAULT_PERFORMANCE_CONFIG: PerformanceConfig = {
|
|||
cacheTTL: 300000 // 5 minutes
|
||||
};
|
||||
|
||||
/** Default configuration for advanced features */
|
||||
export const DEFAULT_ADVANCED_CONFIG: AdvancedConfig = {
|
||||
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
|
||||
maxFileSize: 1048576 // 1MB
|
||||
};
|
||||
|
||||
/** Default status mapping configuration */
|
||||
export const DEFAULT_STATUS_MAPPING_CONFIG: StatusMappingConfig = {
|
||||
enabled: true,
|
||||
|
|
@ -157,7 +151,6 @@ export const DEFAULT_FILE_SOURCE_CONFIG: FileSourceConfiguration = {
|
|||
fileTaskProperties: DEFAULT_FILE_TASK_PROPERTIES,
|
||||
relationships: DEFAULT_RELATIONSHIPS_CONFIG,
|
||||
performance: DEFAULT_PERFORMANCE_CONFIG,
|
||||
advanced: DEFAULT_ADVANCED_CONFIG,
|
||||
statusMapping: DEFAULT_STATUS_MAPPING_CONFIG
|
||||
};
|
||||
|
||||
|
|
@ -286,25 +279,6 @@ export class FileSourceConfig {
|
|||
}
|
||||
}
|
||||
|
||||
// Validate advanced config
|
||||
if (config.advanced) {
|
||||
const advanced = config.advanced;
|
||||
|
||||
if (advanced.maxFileSize && advanced.maxFileSize < 1024) {
|
||||
errors.push("Maximum file size must be at least 1KB");
|
||||
}
|
||||
|
||||
if (advanced.customRecognitionFunction) {
|
||||
// Basic validation for custom function
|
||||
try {
|
||||
// Check if it's valid JavaScript function syntax
|
||||
new Function('filePath', 'fileContent', 'fileCache', advanced.customRecognitionFunction);
|
||||
} catch (error) {
|
||||
errors.push("Custom recognition function has invalid syntax");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate status mapping config
|
||||
if (config.statusMapping?.enabled) {
|
||||
const mapping = config.statusMapping;
|
||||
|
|
@ -357,10 +331,6 @@ export class FileSourceConfig {
|
|||
...DEFAULT_PERFORMANCE_CONFIG,
|
||||
...partial.performance
|
||||
},
|
||||
advanced: {
|
||||
...DEFAULT_ADVANCED_CONFIG,
|
||||
...partial.advanced
|
||||
},
|
||||
statusMapping: {
|
||||
...DEFAULT_STATUS_MAPPING_CONFIG,
|
||||
...partial.statusMapping
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ export interface FilterRule {
|
|||
type: "file" | "folder" | "pattern";
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
scope?: "both" | "inline" | "file"; // per-rule scope
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -112,11 +113,19 @@ class PathTrie {
|
|||
*/
|
||||
export class FileFilterManager {
|
||||
private config: FileFilterConfig;
|
||||
private folderTrie: PathTrie = new PathTrie();
|
||||
private fileSet: Set<string> = new Set();
|
||||
private patternRegexes: RegExp[] = [];
|
||||
private folderTrie: PathTrie = new PathTrie(); // global (legacy)
|
||||
private fileSet: Set<string> = new Set(); // global (legacy)
|
||||
private patternRegexes: RegExp[] = []; // global (legacy)
|
||||
private cache: Map<string, boolean> = new Map();
|
||||
|
||||
// Scoped indexes for per-rule scope control
|
||||
private folderTrieInline: PathTrie = new PathTrie();
|
||||
private folderTrieFile: PathTrie = new PathTrie();
|
||||
private fileSetInline: Set<string> = new Set();
|
||||
private fileSetFile: Set<string> = new Set();
|
||||
private patternRegexesInline: RegExp[] = [];
|
||||
private patternRegexesFile: RegExp[] = [];
|
||||
|
||||
constructor(config: FileFilterConfig) {
|
||||
this.config = config;
|
||||
this.rebuildIndexes();
|
||||
|
|
@ -134,7 +143,7 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Check if a file should be included in indexing
|
||||
*/
|
||||
shouldIncludeFile(file: TFile): boolean {
|
||||
shouldIncludeFile(file: TFile, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
if (!this.config.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -146,7 +155,7 @@ export class FileFilterManager {
|
|||
return this.cache.get(filePath)!;
|
||||
}
|
||||
|
||||
const result = this.evaluateFile(filePath);
|
||||
const result = this.evaluateFile(filePath, scope);
|
||||
this.cache.set(filePath, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -154,7 +163,7 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Check if a folder should be included in indexing
|
||||
*/
|
||||
shouldIncludeFolder(folder: TFolder): boolean {
|
||||
shouldIncludeFolder(folder: TFolder, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
if (!this.config.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -166,7 +175,7 @@ export class FileFilterManager {
|
|||
return this.cache.get(folderPath)!;
|
||||
}
|
||||
|
||||
const result = this.evaluateFolder(folderPath);
|
||||
const result = this.evaluateFolder(folderPath, scope);
|
||||
this.cache.set(folderPath, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -174,7 +183,7 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Check if a path should be included (generic method)
|
||||
*/
|
||||
shouldIncludePath(path: string): boolean {
|
||||
shouldIncludePath(path: string, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
if (!this.config.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -184,7 +193,7 @@ export class FileFilterManager {
|
|||
return this.cache.get(path)!;
|
||||
}
|
||||
|
||||
const result = this.evaluatePath(path);
|
||||
const result = this.evaluatePath(path, scope);
|
||||
this.cache.set(path, result);
|
||||
return result;
|
||||
}
|
||||
|
|
@ -210,8 +219,8 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Evaluate if a file should be included
|
||||
*/
|
||||
private evaluateFile(filePath: string): boolean {
|
||||
const matches = this.pathMatches(filePath);
|
||||
private evaluateFile(filePath: string, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
const matches = this.pathMatches(filePath, scope);
|
||||
|
||||
if (this.config.mode === FilterMode.WHITELIST) {
|
||||
return matches;
|
||||
|
|
@ -223,8 +232,8 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Evaluate if a folder should be included
|
||||
*/
|
||||
private evaluateFolder(folderPath: string): boolean {
|
||||
const matches = this.pathMatches(folderPath);
|
||||
private evaluateFolder(folderPath: string, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
const matches = this.pathMatches(folderPath, scope);
|
||||
|
||||
if (this.config.mode === FilterMode.WHITELIST) {
|
||||
return matches;
|
||||
|
|
@ -236,8 +245,8 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Evaluate if a path should be included (generic)
|
||||
*/
|
||||
private evaluatePath(path: string): boolean {
|
||||
const matches = this.pathMatches(path);
|
||||
private evaluatePath(path: string, scope: "both" | "inline" | "file" = "both"): boolean {
|
||||
const matches = this.pathMatches(path, scope);
|
||||
|
||||
if (this.config.mode === FilterMode.WHITELIST) {
|
||||
return matches;
|
||||
|
|
@ -249,21 +258,26 @@ export class FileFilterManager {
|
|||
/**
|
||||
* Check if a path matches any filter rule
|
||||
*/
|
||||
private pathMatches(path: string): boolean {
|
||||
private pathMatches(path: string, scope: "both" | "inline" | "file"): boolean {
|
||||
const normalizedPath = this.normalizePath(path);
|
||||
|
||||
// Pick the right indexes based on scope
|
||||
const fileSet = scope === "file" ? this.fileSetFile : scope === "inline" ? this.fileSetInline : this.fileSet;
|
||||
const folderTrie = scope === "file" ? this.folderTrieFile : scope === "inline" ? this.folderTrieInline : this.folderTrie;
|
||||
const patternRegexes = scope === "file" ? this.patternRegexesFile : scope === "inline" ? this.patternRegexesInline : this.patternRegexes;
|
||||
|
||||
// Check exact file matches
|
||||
if (this.fileSet.has(normalizedPath)) {
|
||||
if (fileSet.has(normalizedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check folder matches (including parent folders)
|
||||
if (this.folderTrie.contains(normalizedPath)) {
|
||||
if (folderTrie.contains(normalizedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check pattern matches
|
||||
for (const regex of this.patternRegexes) {
|
||||
for (const regex of patternRegexes) {
|
||||
if (regex.test(normalizedPath)) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -276,32 +290,62 @@ export class FileFilterManager {
|
|||
* Rebuild internal indexes when configuration changes
|
||||
*/
|
||||
private rebuildIndexes(): void {
|
||||
// Clear legacy and scoped indexes
|
||||
this.folderTrie.clear();
|
||||
this.fileSet.clear();
|
||||
this.patternRegexes = [];
|
||||
this.folderTrieInline.clear();
|
||||
this.folderTrieFile.clear();
|
||||
this.fileSetInline.clear();
|
||||
this.fileSetFile.clear();
|
||||
this.patternRegexesInline = [];
|
||||
this.patternRegexesFile = [];
|
||||
|
||||
for (const rule of this.config.rules) {
|
||||
if (!rule.enabled) continue;
|
||||
const scope = rule.scope || "both";
|
||||
|
||||
switch (rule.type) {
|
||||
case "file":
|
||||
this.fileSet.add(this.normalizePath(rule.path));
|
||||
break;
|
||||
case "folder":
|
||||
this.folderTrie.insert(rule.path, true);
|
||||
break;
|
||||
case "pattern":
|
||||
try {
|
||||
// Convert glob pattern to regex
|
||||
const regexPattern = this.globToRegex(rule.path);
|
||||
this.patternRegexes.push(new RegExp(regexPattern, "i"));
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Invalid pattern rule: ${rule.path}`,
|
||||
error
|
||||
);
|
||||
}
|
||||
break;
|
||||
const addTo = (bucket: "both" | "inline" | "file") => {
|
||||
switch (rule.type) {
|
||||
case "file":
|
||||
(bucket === "file"
|
||||
? this.fileSetFile
|
||||
: bucket === "inline"
|
||||
? this.fileSetInline
|
||||
: this.fileSet
|
||||
).add(this.normalizePath(rule.path));
|
||||
break;
|
||||
case "folder":
|
||||
(bucket === "file"
|
||||
? this.folderTrieFile
|
||||
: bucket === "inline"
|
||||
? this.folderTrieInline
|
||||
: this.folderTrie
|
||||
).insert(rule.path, true);
|
||||
break;
|
||||
case "pattern":
|
||||
try {
|
||||
const regexPattern = this.globToRegex(rule.path);
|
||||
(bucket === "file"
|
||||
? this.patternRegexesFile
|
||||
: bucket === "inline"
|
||||
? this.patternRegexesInline
|
||||
: this.patternRegexes
|
||||
).push(new RegExp(regexPattern, "i"));
|
||||
} catch (error) {
|
||||
console.warn(`Invalid pattern rule: ${rule.path}`, error);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Add to legacy 'both' indexes if scope is both, else add to specific
|
||||
if (scope === "both") {
|
||||
addTo("both");
|
||||
} else if (scope === "inline") {
|
||||
addTo("inline");
|
||||
} else if (scope === "file") {
|
||||
addTo("file");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
.file-filter-rule-type,
|
||||
.file-filter-rule-path,
|
||||
.file-filter-rule-scope,
|
||||
.file-filter-rule-enabled {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
Loading…
Reference in a new issue