vscode-project-md CLAUDE.md
When working with code assistants, developers often reference project files in Markdown documentation. This extension makes those references **actionable**:
Project.md - Technical Documentation for Claude Code
Project Overview
Project.md is a VSCode extension that transforms Markdown files into interactive project navigation hubs. It provides intelligent file path detection, clickable navigation, and automatic file creation - specifically designed to enhance workflow for code assistants like Claude Code, Gemini CLI, and Codex CLI.
Core Purpose
When working with code assistants, developers often reference project files in Markdown documentation. This extension makes those references actionable:
- File paths become clickable links
- Non-existent files are created automatically
- Navigation works seamlessly with VSCode's native features
Architecture
Extension Structure
project-md/
├── src/
│ ├── extension.ts # Main extension logic
│ └── test/
│ └── extension.test.ts # Test suite (placeholder)
├── dist/ # Compiled output (esbuild)
├── package.json # Extension manifest
└── tsconfig.json # TypeScript configuration
Key Components
1. Activation Events
"activationEvents": ["onLanguage:markdown"]
- Extension activates when any Markdown file opens
- Lazy activation for performance optimization
- No activation on VSCode startup (reduces overhead)
2. Providers Registered
a) Document Link Provider
- Scans document for file path patterns
- Converts paths to clickable VSCode links
- Uses command URI scheme for custom behavior
- Tooltip: "Open path"
b) Definition Provider
- Enables "Go to Definition" (F12) on file paths
- Only works for existing files
- Returns
vscode.Locationpointing to target file
c) Command Handler
- Command ID:
markdownLinks.open - Handles click events on document links
- Implements auto-creation logic
- Directory handling with fallback
Path Detection Strategy
Three regex patterns capture different reference styles:
Pattern 1: Markdown Links
const MD_LINK_RE = /\[[^\]]+\]\(\s*(@?(?:\.{1,2}\/|\/)[^)\s]+)\s*\)/g;
Matches:
[text](./path/file.md)[config](@./config/settings.ts)[root](/absolute/path.ts)
Captures: Path inside parentheses, including optional @ prefix
Pattern 2: Bare References
const BARE_REF_RE = /(^|[\s(`])(@?(?:\.{1,2}\/|\/)[^\s`)\]]+)/g;
Matches:
./src/index.ts(standalone)Check ./config/app.ts for details- Line-beginning paths
Captures: Leading whitespace/delimiter + path
Conflict Resolution: Only adds if not already captured by MD_LINK_RE
Pattern 3: Inline Code References
const INLINE_CODE_RE = /`(@?(?:\.{1,2}\/|\/)[^\s`)\]]+)`/g;
Matches:
`./src/utils.ts``@./config/database.ts`
Captures: Path inside backticks
Conflict Resolution: Checks for range intersection before adding
Path Resolution Logic
const abs = path.resolve(baseDir, rel.replace(/^@/, ""));
Steps:
- Get document's directory (
path.dirname(doc.uri.fsPath)) - Remove
@prefix if present - Resolve relative path to absolute
- Convert to filesystem path
@ Prefix Behavior:
- Stripped before resolution
- Allows aliasing convention (e.g.,
@./for project root contexts) - No special mapping (yet) - treated as
./
Feature Implementation Details
Feature 1: Clickable Links
Implementation:
const linkProvider: vscode.DocumentLinkProvider = {
provideDocumentLinks(doc) {
return getRefRanges(doc).map(({ range, targetFsPath }) => {
const cmdUri = vscode.Uri.parse(
`command:${commandId}?${encodeURIComponent(JSON.stringify({ path: targetFsPath }))}`
);
const link = new vscode.DocumentLink(range, cmdUri);
link.tooltip = "Open path";
return link;
});
}
};
Flow:
- User hovers over detected path
- VSCode renders underline (document link)
- Cmd/Ctrl+Click triggers command URI
- Command handler (
markdownLinks.open) executes openPathLike()processes the path
Feature 2: Auto-Creation
Implementation:
async function openPathLike(targetFsPath: string) {
const stat = await fs.promises.stat(targetFsPath).catch(() => undefined);
if (!stat) {
await fs.promises.mkdir(path.dirname(targetFsPath), { recursive: true });
await fs.promises.writeFile(targetFsPath, "", "utf8");
}
const td = await vscode.workspace.openTextDocument(vscode.Uri.file(targetFsPath));
await vscode.window.showTextDocument(td);
}
Behavior:
- File doesn't exist: Create parent directories + empty file
- Directory: Reveal in Explorer (or Finder/File Explorer as fallback)
- File exists: Open in editor
- Error: Show error message with path
Safety:
- Uses
recursive: trueformkdir(safe if directories exist) - Creates empty UTF-8 file (no data loss risk)
- Catch-all error handling prevents crashes
Feature 3: Go to Definition
Implementation:
const defProvider: vscode.DefinitionProvider = {
provideDefinition(doc, pos) {
const hit = getRefRanges(doc).find(h => h.range.contains(pos));
if (!hit) return;
const stat = fs.existsSync(hit.targetFsPath) ? fs.statSync(hit.targetFsPath) : undefined;
if (stat?.isFile()) {
return new vscode.Location(vscode.Uri.file(hit.targetFsPath), new vscode.Position(0, 0));
}
}
};
Behavior:
- Only works for existing files
- Does NOT work for directories or non-existent paths
- Jumps to line 0, column 0 of target file
- Enables F12, Cmd+Click to jump
Design Decision:
- Separated from
openPathLiketo avoid auto-creation on F12 - Definition = "jump to existing code", not "create new file"
Feature 4: Directory Handling
Implementation:
if (stat?.isDirectory()) {
try {
await vscode.commands.executeCommand("revealInExplorer", vscode.Uri.file(targetFsPath));
} catch {
await vscode.commands.executeCommand("revealFileInOS", vscode.Uri.file(targetFsPath));
}
return;
}
Fallback Strategy:
- Try
revealInExplorer(VSCode Explorer sidebar) - If fails, try
revealFileInOS(system file manager) - No error thrown - best-effort approach
Development Guidelines
Code Style
Type Safety:
- All functions use TypeScript types
vscodetypes from@types/vscode- Explicit return types for public functions
Error Handling:
- Async operations use
.catch(() => undefined)for graceful degradation - Try-catch blocks for user-facing operations
- Error messages include context (file path)
Naming Conventions:
getRefRanges: Pure function, no side effectsopenPathLike: Async action with side effectsHit: Type represents detected path + range
Building & Testing
Build Commands:
npm run compile # Type check + lint + build
npm run watch # Watch mode (esbuild + tsc)
npm run package # Production build
Development Flow:
npm run watchin terminal- Press F5 to launch Extension Development Host
- Open Markdown file in dev window
- Test path detection and navigation
Testing Strategy
Unit Tests (Recommended):
// Test regex patterns
describe('getRefRanges', () => {
it('should detect markdown links', () => {
const doc = createMockDocument('[test](./file.md)');
const ranges = getRefRanges(doc);
expect(ranges).toHaveLength(1);
expect(ranges[0].targetFsPath).toContain('file.md');
});
it('should detect bare references', () => { /* ... */ });
it('should detect inline code paths', () => { /* ... */ });
it('should handle @ prefix', () => { /* ... */ });
it('should prevent duplicate ranges', () => { /* ... */ });
});
Integration Tests (Recommended):
// Test providers
describe('DocumentLinkProvider', () => {
it('should provide clickable links', async () => { /* ... */ });
});
describe('DefinitionProvider', () => {
it('should navigate to existing files', async () => { /* ... */ });
it('should return undefined for non-existent files', async () => { /* ... */ });
});
Manual Testing Checklist:
- [ ] Markdown links with relative paths
- [ ] Markdown links with absolute paths
- [ ] Markdown links with
@prefix - [ ] Bare references in text
- [ ] Inline code references
- [ ] Click to open existing file
- [ ] Click to create non-existent file
- [ ] Click to reveal directory
- [ ] F12 on existing file path
- [ ] F12 on non-existent path (should do nothing)
Future Roadmap
Planned Features
1. Syntax Highlighting
Goal: Visual differentiation for file paths in Markdown
Implementation approach:
- TextMate grammar in package.json contributes
- Scope: source.markdown meta.path.projectmd
- Color themes can customize highlighting
- Distinguish existing vs non-existent files
2. Path Validation
Goal: Warn about broken references
Implementation approach:
- Diagnostic provider for Markdown files
- Check file existence on document change
- Warning severity for non-existent paths
- Quick fix: "Create file" code action
3. Assistant-Specific Tooling
Claude Code Integration:
- Detect claude.md references
- Auto-link to project context files
- Validate @-references against project structure
Gemini CLI Integration:
- Support for .geminirc path conventions
- Validate tool configurations
Codex CLI Integration:
- Support for .codex paths
- Validate context file references
4. Configuration Options
{
"project-md.autoCreate": true,
"project-md.atPrefixAlias": "./",
"project-md.highlightPaths": true,
"project-md.validatePaths": "warning"
}
5. Workspace Support
Goal: Multi-root workspace handling
Implementation approach:
- Resolve paths relative to workspace root
- Support workspace-relative paths (e.g., ${workspaceFolder}/...)
- Handle monorepo structures
Design Decisions & Trade-offs
Why Three Separate Regex Patterns?
Decision: Use 3 patterns instead of one complex regex
Rationale:
- Clarity: Each pattern has single responsibility
- Maintainability: Easy to debug and extend individual patterns
- Conflict handling: Explicit deduplication logic
- Performance: Simpler patterns = faster execution
Trade-off: Potential overlapping matches (mitigated by intersection checks)
Why Auto-Create Files?
Decision: Automatically create referenced files when clicked
Rationale:
- Workflow optimization: Reduces friction when documenting
- Code assistant friendly: LLMs often reference files that don't exist yet
- Markdown-first development: Document structure before implementation
Trade-off: Accidental file creation (mitigated by requiring explicit click)
Why Separate Definition Provider?
Decision: Definition provider only navigates to existing files
Rationale:
- User expectation: F12 = "go to existing code"
- Avoid confusion: Definition ≠ creation
- Explicit intent: Click = create, F12 = navigate
Trade-off: Different behavior for same path (click vs F12)
Why Remove @ Prefix Without Mapping?
Decision: Strip @ but don't resolve to special directory
Rationale:
- Future-proofing: Reserve
@for future aliasing feature - Compatibility: Works with existing paths
- Simplicity: No configuration needed for v0.0.1
Trade-off: @./ behaves identically to ./ (no benefit yet)
Extension Manifest Details
Key Package.json Fields
{
"name": "project-md",
"displayName": "Project.md",
"version": "0.0.1",
"engines": { "vscode": "^1.104.0" },
"activationEvents": ["onLanguage:markdown"],
"main": "./dist/extension.js",
"contributes": {
"commands": [
{
"command": "project-md.helloWorld",
"title": "Hello World"
}
]
}
}
Note: project-md.helloWorld is template command - not used in v0.0.1
Dependencies
Runtime: None (uses only VSCode API + Node.js built-ins)
Development:
esbuild: Fast bundling for productiontypescript: Type checkingeslint: Code linting@types/vscode: VSCode API types
Performance Considerations
Current Performance Characteristics
Document Scanning:
- 3 regex patterns executed per document
- Complexity: O(n) where n = document length
- Runs synchronously in
provideDocumentLinks()
Optimization Opportunities:
- Caching: Cache regex results per document version
- Incremental parsing: Only re-scan changed ranges
- Web worker: Offload regex execution for large files
Current Impact: Negligible for typical Markdown files (<10k lines)
Memory Usage
Per Document:
- Array of
Hitobjects (range + path) - Typical: 10-50 hits per document
- Memory: ~1-5KB per document
Extension Footprint: <500KB (bundled)
Security Considerations
Path Resolution Safety
Vulnerability: Path traversal attacks
Mitigation:
const abs = path.resolve(baseDir, rel.replace(/^@/, ""));
path.resolve()normalizes paths (prevents../../../etc/passwd)- Resolved to absolute path (no ambiguity)
- No shell execution (uses
fsdirectly)
File Creation Safety
Vulnerability: Unintended file creation
Mitigation:
- Requires explicit user click (no automatic creation)
- Creates only empty files (no content injection)
- Uses
utf8encoding (prevents binary corruption)
Remaining Risk: User could click malicious path in untrusted Markdown
Contributing Guidelines
Pull Request Checklist
- [ ] Code follows TypeScript best practices
- [ ] All regex patterns tested with unit tests
- [ ] Manual testing performed (see checklist above)
- [ ] No new dependencies added without justification
- [ ]
npm run lintpasses - [ ]
npm run compilesucceeds - [ ] CHANGELOG.md updated
Code Review Focus Areas
- Regex correctness: Patterns match intended paths only
- Error handling: All async operations have error handling
- Type safety: No
anytypes without justification - Performance: No blocking operations in providers
- UX: Clear error messages and tooltips
Debugging Tips
Enable Extension Development Host Logging
// Add to extension.ts
const outputChannel = vscode.window.createOutputChannel("Project.md");
outputChannel.appendLine(`Detected ${hits.length} paths`);
Inspect Document Links
- Open Command Palette (
Cmd+Shift+P) - Run "Developer: Inspect Editor Tokens and Scopes"
- Hover over path - shows token info
Regex Testing
Use online regex tester with JavaScript flavor:
- https://regex101.com (select ECMAScript/JavaScript)
- Paste pattern + test Markdown content
- Verify capture groups
VSCode Extension API Reference
Key APIs Used
Document Link Provider:
vscode.languages.registerDocumentLinkProvider()vscode.DocumentLink(range, target)command:URI scheme for custom commands
Definition Provider:
vscode.languages.registerDefinitionProvider()vscode.Location(uri, position)
Commands:
vscode.commands.registerCommand()vscode.commands.executeCommand()
File System:
vscode.workspace.openTextDocument()vscode.window.showTextDocument()
Node.js APIs:
fs.promises.stat()/fs.existsSync()/fs.statSync()fs.promises.mkdir()/fs.promises.writeFile()path.resolve()/path.dirname()
Questions & Support
Common Questions
Q: Why aren't my paths being detected?
A: Check if path starts with ./, ../, or /. Paths without these prefixes are not detected.
Q: Can I use workspace-relative paths? A: Not yet in v0.0.1. This is planned for future release.
Q: Why does F12 create files but Definition provider doesn't?
A: They use different code paths. Click uses openPathLike() (auto-creates), F12 uses definition provider (existing files only).
Q: How do I configure the extension? A: No configuration options in v0.0.1. Coming in future releases.
Development Support
For technical questions or contributions:
- Check existing GitHub issues
- Review this documentation
- Open new issue with:
- VSCode version
- Extension version
- Minimal reproduction case
- Expected vs actual behavior
Last Updated: 2025-10-01 Extension Version: 0.0.1 VSCode Engine: ^1.104.0