mirror of
https://github.com/foambubble/foam.git
synced 2026-01-07 21:24:06 -05:00
Add FOAM_CURRENT_DIR template variable (#1507)
* Added FOAM_CURRENT_DIR template variable * Added /research-issue Claude command * Added integration test to create note using FOAM_CURRENT_DIR * Updated documentation * fixed comment * Fail FOAM_CURRENT_DIR resolution if no editor nor workspace is open
This commit is contained in:
61
.claude/commands/research-issue.md
Normal file
61
.claude/commands/research-issue.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# Research Issue Command
|
||||
|
||||
Research a GitHub issue by analyzing the issue details and codebase to generate a comprehensive task analysis file.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
/research-issue <issue-number>
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
||||
- `issue-number` (required): The GitHub issue number to research
|
||||
|
||||
## Description
|
||||
|
||||
This command performs comprehensive research on a GitHub issue by:
|
||||
|
||||
1. **Fetching Issue Details**: Uses `gh issue view` to get issue title, description, labels, comments, and related information
|
||||
2. **Codebase Analysis**: Searches the codebase for relevant files, patterns, and components mentioned in the issue
|
||||
3. **Root Cause Analysis**: Identifies possible technical causes based on the issue description and codebase findings
|
||||
4. **Solution Planning**: Proposes two solution approaches ranked by preference
|
||||
5. **Documentation**: Creates a structured task file in `.agent/tasks/<issue-id>-<sanitized-title>.md`
|
||||
|
||||
If there is already a `.agent/tasks/<issue-id>-<sanitized-title>.md` file, use it for context and update it accordingly.
|
||||
If at any time during these steps you need clarifying information from me, please ask.
|
||||
|
||||
## Output Format
|
||||
|
||||
Creates a markdown file with:
|
||||
|
||||
- Issue summary and key details
|
||||
- Research findings from codebase analysis
|
||||
- Identified possible root causes
|
||||
- Two ranked solution approaches with pros/cons
|
||||
- Technical considerations and dependencies
|
||||
|
||||
## Examples
|
||||
|
||||
```
|
||||
/research-issue 1234
|
||||
/research-issue 567
|
||||
```
|
||||
|
||||
## Implementation
|
||||
|
||||
The command will:
|
||||
|
||||
1. Validate the issue number and check if it exists
|
||||
2. Fetch issue details using GitHub CLI
|
||||
3. Search codebase for relevant patterns, files, and components
|
||||
4. Analyze findings to identify root causes
|
||||
5. Generate structured markdown file with research results
|
||||
6. Save to `.agent/tasks/` directory with standardized naming
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Invalid issue numbers
|
||||
- GitHub CLI authentication issues
|
||||
- Network connectivity problems
|
||||
- File system write permissions
|
||||
@@ -119,6 +119,8 @@ This allows features to:
|
||||
## Development Workflow
|
||||
|
||||
We build production code together. I handle implementation details while you guide architecture and catch complexity early.
|
||||
When working on an issue, check if a `.agent/tasks/<issue-id>-<sanitized-title>.md` exists. If not, suggest whether we should start by doing a research on it (using the `/research-issue <issue-id>`) command.
|
||||
Whenever we work together on a task, feel free to challenge my assumptions and ideas and be critical if useful.
|
||||
|
||||
## Core Workflow: Research → Plan → Implement → Validate
|
||||
|
||||
|
||||
@@ -246,6 +246,7 @@ In addition, you can also use variables provided by Foam:
|
||||
| `FOAM_TITLE` | The title of the note. If used, Foam will prompt you to enter a title for the note. |
|
||||
| `FOAM_TITLE_SAFE` | The title of the note in a file system safe format. If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_SLUG` | The sluggified title of the note (using the default github slug method). If used, Foam will prompt you to enter a title for the note unless `FOAM_TITLE` has already caused the prompt. |
|
||||
| `FOAM_CURRENT_DIR` | The current editor's directory path. Resolves to the directory of the currently active file, or falls back to workspace root if no editor is active. Useful for creating notes in the current directory context. |
|
||||
| `FOAM_DATE_*` | `FOAM_DATE_YEAR`, `FOAM_DATE_MONTH`, `FOAM_DATE_WEEK` etc. Foam-specific versions of [VS Code's datetime snippet variables](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables). Prefer these versions over VS Code's. |
|
||||
|
||||
### `FOAM_DATE_*` variables
|
||||
@@ -306,6 +307,30 @@ foam_template:
|
||||
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE Daily Notes
|
||||
```
|
||||
|
||||
##### Creating notes in the current directory
|
||||
|
||||
To create notes in the same directory as your currently active file, use the `FOAM_CURRENT_DIR` variable in your template's `filepath`:
|
||||
|
||||
```markdown
|
||||
---
|
||||
foam_template:
|
||||
name: Current Directory Note
|
||||
filepath: '$FOAM_CURRENT_DIR/$FOAM_SLUG.md'
|
||||
---
|
||||
|
||||
# $FOAM_TITLE
|
||||
|
||||
$FOAM_SELECTED_TEXT
|
||||
```
|
||||
|
||||
**Best practices for filepath patterns:**
|
||||
|
||||
- **Explicit current directory:** `$FOAM_CURRENT_DIR/$FOAM_SLUG.md` - Creates notes in the current editor's directory
|
||||
- **Workspace root:** `/$FOAM_SLUG.md` - Always creates notes in workspace root
|
||||
- **Subdirectories:** `$FOAM_CURRENT_DIR/meetings/$FOAM_SLUG.md` - Creates notes in subdirectories relative to current location
|
||||
|
||||
The `FOAM_CURRENT_DIR` approach is recommended over relative paths (like `./file.md`) because it makes the template's behavior explicit and doesn't depend on configuration settings.
|
||||
|
||||
#### `name` and `description` attributes
|
||||
|
||||
These attributes provide a human readable name and description to be shown in the template picker (e.g. When a user uses the `Foam: Create New Note From Template` command):
|
||||
|
||||
@@ -309,4 +309,64 @@ foam_template:
|
||||
expect(doc.getText()).toEqual(`this is my [[hello-world]]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template filepath with FOAM_CURRENT_DIR', () => {
|
||||
it('should create note in current directory using FOAM_CURRENT_DIR variable', async () => {
|
||||
// Create a test subdirectory and a file in it
|
||||
const noteInSubdir = await createFile('Test content', [
|
||||
'subdir',
|
||||
'existing-note.md',
|
||||
]);
|
||||
|
||||
// Create a template with FOAM_CURRENT_DIR variable
|
||||
const template = await createFile(
|
||||
`---
|
||||
foam_template:
|
||||
filepath: \${FOAM_CURRENT_DIR}/\${FOAM_SLUG}.md
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
Template content using FOAM_CURRENT_DIR`,
|
||||
['.foam', 'templates', 'foam-current-dir-template.md']
|
||||
);
|
||||
|
||||
// Switch to the file in the subdirectory to set current editor context
|
||||
await showInEditor(noteInSubdir.uri);
|
||||
|
||||
// Create a note using the template - FOAM_CURRENT_DIR should resolve to current editor directory
|
||||
const resultInSubdir = await createNote(
|
||||
{
|
||||
templatePath: template.uri.path,
|
||||
title: 'My New Note',
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
// The note should be created in the subdir because FOAM_CURRENT_DIR resolves to current editor directory
|
||||
expect(resultInSubdir.uri).toEqual(
|
||||
noteInSubdir.uri.getDirectory().joinPath('my-new-note.md')
|
||||
);
|
||||
|
||||
await closeEditors();
|
||||
// Create a note using the template - FOAM_CURRENT_DIR should resolve to current editor directory
|
||||
const resultInRoot = await createNote(
|
||||
{
|
||||
templatePath: template.uri.path,
|
||||
title: 'My New Note',
|
||||
},
|
||||
{} as any
|
||||
);
|
||||
// The note should be created in the workspace root because FOAM_CURRENT_DIR resolves to workspace root when no editor is active
|
||||
expect(resultInRoot.uri).toEqual(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri).joinPath(
|
||||
'my-new-note.md'
|
||||
)
|
||||
);
|
||||
|
||||
// Clean up
|
||||
await deleteFile(template.uri);
|
||||
await deleteFile(noteInSubdir.uri);
|
||||
await deleteFile(resultInRoot.uri);
|
||||
await deleteFile(resultInSubdir.uri);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,7 +11,6 @@ import { TemplateLoader } from '../../services/template-loader';
|
||||
import { Template } from '../../services/note-creation-types';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
import { asAbsoluteWorkspaceUri, fileExists } from '../../services/editor';
|
||||
import { isSome } from '../../core/utils';
|
||||
import { CommandDescriptor } from '../../utils/commands';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { Location } from '../../core/model/location';
|
||||
|
||||
@@ -10,8 +10,7 @@ import {
|
||||
isPlaceholderTrigger,
|
||||
} from './note-creation-types';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { isAbsolute } from 'path';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
/**
|
||||
* Unified engine for creating notes from both Markdown and JavaScript templates
|
||||
@@ -57,9 +56,6 @@ export class NoteCreationEngine {
|
||||
template: Template & { type: 'javascript' },
|
||||
resolver: Resolver
|
||||
): Promise<NoteCreationResult> {
|
||||
// Convert resolver's variables back to extraParams for backward compatibility
|
||||
const extraParams = resolver.getVariables();
|
||||
|
||||
const templateContext: TemplateContext = {
|
||||
trigger,
|
||||
resolver,
|
||||
|
||||
@@ -240,6 +240,51 @@ describe('variable-resolver, variable resolution', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('FOAM_CURRENT_DIR', () => {
|
||||
it('should resolve to workspace root when no active editor', async () => {
|
||||
const resolver = new Resolver(new Map<string, string>(), new Date());
|
||||
const result = await resolver.resolve(new Variable('FOAM_CURRENT_DIR'));
|
||||
|
||||
// Should resolve to some directory path
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should resolve to current directory when editor is active', async () => {
|
||||
// Create a test file in a subdirectory
|
||||
const testFile = await createFile('Test content', [
|
||||
'test-dir',
|
||||
'test-file.md',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Open the file to make it the active editor
|
||||
await showInEditor(testFile.uri);
|
||||
|
||||
const resolver = new Resolver(new Map<string, string>(), new Date());
|
||||
const result = await resolver.resolve(new Variable('FOAM_CURRENT_DIR'));
|
||||
|
||||
// Should resolve to the test-dir directory
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result).toContain('test-dir');
|
||||
} finally {
|
||||
// Clean up
|
||||
await deleteFile(testFile.uri);
|
||||
}
|
||||
});
|
||||
|
||||
it('should be included in known foam variables', async () => {
|
||||
const input = '${FOAM_CURRENT_DIR}';
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const result = await resolver.resolveText(input);
|
||||
|
||||
// Should resolve to a directory path, not remain as ${FOAM_CURRENT_DIR}
|
||||
expect(result).not.toEqual(input);
|
||||
expect(typeof result).toBe('string');
|
||||
expect(result.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('variable-resolver, resolveText', () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { findSelectionContent } from './editor';
|
||||
import { window } from 'vscode';
|
||||
import { findSelectionContent, getCurrentEditorDirectory } from './editor';
|
||||
import { window, workspace } from 'vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { UserCancelledOperation } from './errors';
|
||||
import { toSlug } from '../utils/slug';
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ const knownFoamVariables = new Set([
|
||||
'FOAM_TITLE_SAFE',
|
||||
'FOAM_SLUG',
|
||||
'FOAM_SELECTED_TEXT',
|
||||
'FOAM_CURRENT_DIR',
|
||||
'FOAM_DATE_YEAR',
|
||||
'FOAM_DATE_YEAR_SHORT',
|
||||
'FOAM_DATE_MONTH',
|
||||
@@ -157,6 +159,9 @@ export class Resolver implements VariableResolver {
|
||||
case 'FOAM_SELECTED_TEXT':
|
||||
value = Promise.resolve(resolveFoamSelectedText());
|
||||
break;
|
||||
case 'FOAM_CURRENT_DIR':
|
||||
value = Promise.resolve(resolveFoamCurrentDir());
|
||||
break;
|
||||
case 'FOAM_DATE_YEAR':
|
||||
value = Promise.resolve(String(this.foamDate.getFullYear()));
|
||||
break;
|
||||
@@ -262,6 +267,21 @@ function resolveFoamSelectedText() {
|
||||
return findSelectionContent()?.content ?? '';
|
||||
}
|
||||
|
||||
function resolveFoamCurrentDir() {
|
||||
try {
|
||||
// Try to get the directory of the currently active editor
|
||||
const currentDir = getCurrentEditorDirectory();
|
||||
return currentDir.toFsPath();
|
||||
} catch (error) {
|
||||
// Fall back to workspace root if no active editor
|
||||
if (workspace.workspaceFolders && workspace.workspaceFolders.length > 0) {
|
||||
return fromVsCodeUri(workspace.workspaceFolders[0].uri).toFsPath();
|
||||
}
|
||||
// If no workspace is open, raise
|
||||
throw new Error('No workspace is open');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Common chars that is better to avoid in file names.
|
||||
* Inspired by:
|
||||
|
||||
@@ -9,7 +9,6 @@ import { Logger } from '../core/utils/log';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { randomString, wait } from './test-utils';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
Reference in New Issue
Block a user