feat(file-source): add path-based task recognition strategy

Implements path-based recognition for FileSource, allowing users to
designate specific directories as task directories. Files within these
paths are automatically recognized as tasks.

Features:
- Three matching modes: prefix, glob, and regex patterns
- Configurable task paths via settings UI
- Dynamic examples based on selected matching mode
- Support for multiple path patterns

The glob mode supports standard patterns:
- * matches any chars except /
- ** matches any chars including /
- ? matches single char

This completes the Phase 2 path strategy implementation for FileSource.
This commit is contained in:
Quorafind 2025-08-22 13:08:21 +08:00
parent 55fbc630ad
commit 5fc1ad0dba
3 changed files with 232 additions and 11 deletions

View file

@ -220,14 +220,104 @@ function createRecognitionStrategiesSection(
);
}
// Template and Path strategies (Phase 2 placeholders)
const futureContainer = containerEl.createDiv(
// Path strategy
const pathContainer = containerEl.createDiv(
"file-source-strategy-container",
);
new Setting(pathContainer)
.setName(t("Path-based Recognition"))
.setDesc(t("Recognize files as tasks based on their file path"))
.addToggle((toggle) =>
toggle
.setValue(config.recognitionStrategies.paths.enabled)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.paths.enabled = value;
await plugin.saveSettings();
// Refresh settings interface
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
}),
);
if (config.recognitionStrategies.paths.enabled) {
new Setting(pathContainer)
.setName(t("Task Paths"))
.setDesc(
t("Paths that contain task files. One per line. Examples: Projects/, Tasks/2024/, Work/TODO/")
)
.addTextArea((text) => {
text
.setPlaceholder("Projects/\nTasks/\nWork/TODO/")
.setValue(config.recognitionStrategies.paths.taskPaths.join("\n"))
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.paths.taskPaths =
value
.split("\n")
.map((s) => s.trim())
.filter((s) => s.length > 0);
await plugin.saveSettings();
});
text.inputEl.rows = 4;
text.inputEl.cols = 40;
});
new Setting(pathContainer)
.setName(t("Path Matching Mode"))
.setDesc(t("How paths should be matched"))
.addDropdown((dropdown) =>
dropdown
.addOption("prefix", t("Prefix (e.g., Projects/ matches Projects/App.md)"))
.addOption("glob", t("Glob pattern (e.g., Projects/**/*.md)"))
.addOption("regex", t("Regular expression (advanced)"))
.setValue(config.recognitionStrategies.paths.matchMode)
.onChange(async (value: "prefix" | "regex" | "glob") => {
plugin.settings.fileSource.recognitionStrategies.paths.matchMode = value;
await plugin.saveSettings();
// Refresh to show updated examples
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
}),
);
// Add examples based on current mode
const examples = pathContainer.createDiv("setting-item-description");
examples.style.marginTop = "10px";
const currentMode = config.recognitionStrategies.paths.matchMode;
let exampleText = "";
switch (currentMode) {
case "prefix":
exampleText = t("Examples:") + "\n" +
"• Projects/ → " + t("matches all files under Projects folder") + "\n" +
"• Tasks/2024/ → " + t("matches all files under Tasks/2024 folder");
break;
case "glob":
exampleText = t("Examples:") + "\n" +
"• Projects/**/*.md → " + t("all .md files in Projects and subfolders") + "\n" +
"• Tasks/*.task.md → " + t("files ending with .task.md in Tasks folder") + "\n" +
"• Work/*/TODO.md → " + t("TODO.md in any direct subfolder of Work");
break;
case "regex":
exampleText = t("Examples:") + "\n" +
"• ^Projects/.*\\.md$ → " + t("all .md files in Projects folder") + "\n" +
"• ^Tasks/\\d{4}-\\d{2}-\\d{2} → " + t("files starting with date in Tasks");
break;
}
examples.createEl("pre", {
text: exampleText,
attr: { style: "font-size: 0.9em; color: var(--text-muted);" }
});
}
// Template strategy placeholder (keep for future)
const templateContainer = containerEl.createDiv(
"file-source-future-strategies",
);
futureContainer.createEl("p", {
text: t(
"Template-based and Path-based recognition strategies will be available in a future update.",
),
templateContainer.createEl("p", {
text: t("Template-based recognition strategy will be available in a future update."),
attr: { style: "color: var(--text-muted); font-style: italic;" },
});
}

View file

@ -13,7 +13,8 @@ import type {
FileSourceStats,
UpdateDecision,
FileTaskCache,
RecognitionStrategy
RecognitionStrategy,
PathRecognitionConfig
} from "../../types/file-source";
import { Events, emit, Seq, on } from "../events/Events";
@ -319,18 +320,86 @@ export class FileSource {
}
/**
* Check if file matches path strategy (stub for Phase 2)
* Check if file matches path strategy
*/
private matchesPathStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
config: PathRecognitionConfig
): boolean {
// TODO: Implement in Phase 2
if (!config.enabled || !config.taskPaths || config.taskPaths.length === 0) {
return false;
}
// Normalize path (use forward slashes)
const normalizedPath = filePath.replace(/\\/g, '/');
// Check each configured path pattern
for (const pattern of config.taskPaths) {
const normalizedPattern = pattern.replace(/\\/g, '/');
switch (config.matchMode) {
case 'prefix':
if (normalizedPath.startsWith(normalizedPattern)) {
console.log(`[FileSource] Path matches prefix pattern: ${pattern} for ${filePath}`);
return true;
}
break;
case 'regex':
try {
const regex = new RegExp(normalizedPattern);
if (regex.test(normalizedPath)) {
console.log(`[FileSource] Path matches regex pattern: ${pattern} for ${filePath}`);
return true;
}
} catch (e) {
console.warn(`[FileSource] Invalid regex pattern: ${pattern}`, e);
}
break;
case 'glob':
if (this.matchGlobPattern(normalizedPath, normalizedPattern)) {
console.log(`[FileSource] Path matches glob pattern: ${pattern} for ${filePath}`);
return true;
}
break;
}
}
return false;
}
/**
* Match a path against a glob pattern
* Supports: * (any chars except /), ** (any chars), ? (single char)
*/
private matchGlobPattern(path: string, pattern: string): boolean {
// Convert glob pattern to regular expression
let regexPattern = pattern
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special chars
.replace(/\*\*/g, '§§§') // Temporary placeholder for **
.replace(/\*/g, '[^/]*') // * matches any chars except /
.replace(/§§§/g, '.*') // ** matches any chars
.replace(/\?/g, '[^/]'); // ? matches single char
// If pattern ends with /, match all files in that directory
if (pattern.endsWith('/')) {
regexPattern = `^${regexPattern}.*`;
} else {
regexPattern = `^${regexPattern}$`;
}
try {
const regex = new RegExp(regexPattern);
return regex.test(path);
} catch (e) {
console.warn(`[FileSource] Failed to compile glob pattern: ${pattern}`, e);
return false;
}
}
/**
* Create a new file task
*/
@ -466,7 +535,13 @@ export class FileSource {
return { name: "tag", criteria: "file-tags" };
}
// TODO: Add template and path strategies in Phase 2
// Check path strategy
if (config.recognitionStrategies.paths.enabled &&
this.matchesPathStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.paths)) {
return { name: "path", criteria: config.recognitionStrategies.paths.taskPaths.join(", ") };
}
// TODO: Add template strategy in Phase 2
return null;
}

56
test-path-strategy.md Normal file
View file

@ -0,0 +1,56 @@
# Test Path Strategy
This is a simple test to verify that the Path strategy implementation is working correctly.
## Implementation Summary
### 1. FileSource.ts Updates
- ✅ Added `PathRecognitionConfig` import
- ✅ Implemented `matchesPathStrategy` method with support for:
- **prefix** mode: Simple prefix matching (e.g., `Projects/` matches `Projects/App.md`)
- **regex** mode: Regular expression matching
- **glob** mode: Glob pattern matching (supports `*`, `**`, `?`)
- ✅ Added `matchGlobPattern` helper method for glob pattern conversion
- ✅ Updated strategy evaluation to include path strategy
### 2. FileSourceSettings.ts Updates
- ✅ Replaced placeholder with full Path strategy configuration UI
- ✅ Added TextArea for entering task paths (one per line)
- ✅ Added Dropdown for selecting matching mode
- ✅ Added dynamic examples based on selected mode
- ✅ Kept Template strategy placeholder for future implementation
## Features
### Path Matching Modes
1. **Prefix Mode**
- Simplest mode for basic directory matching
- Example: `Projects/` matches all files under Projects folder
2. **Glob Mode**
- Supports standard glob patterns
- `*` matches any characters except `/`
- `**` matches any characters including `/`
- `?` matches single character
- Examples:
- `Projects/**/*.md` - all .md files in Projects and subfolders
- `Tasks/*.task.md` - files ending with .task.md in Tasks folder
3. **Regex Mode**
- Full regular expression support for advanced users
- Example: `^Tasks/\d{4}-\d{2}-\d{2}` - files starting with date in Tasks
## Configuration
Users can now:
1. Enable Path-based Recognition in Settings > Index > File as Task
2. Enter multiple task paths (one per line)
3. Choose matching mode (prefix, glob, or regex)
4. See context-sensitive examples for the selected mode
## Build Status
✅ Build completed successfully
✅ TypeScript compilation passed (with expected Jest type conflicts)
✅ All functionality integrated into existing codebase