mirror of
https://github.com/foambubble/foam.git
synced 2026-01-11 06:58:11 -05:00
Compare commits
94 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a562aa0aa | ||
|
|
0bab17c130 | ||
|
|
8121223e30 | ||
|
|
793664ac59 | ||
|
|
4c5430d2b1 | ||
|
|
ebef851f5a | ||
|
|
253ee94b1c | ||
|
|
9ffd465a32 | ||
|
|
ff3dacdbbf | ||
|
|
0a6350464b | ||
|
|
fe0228bdcc | ||
|
|
471260bdd3 | ||
|
|
a22f1b46dc | ||
|
|
318641ae04 | ||
|
|
12a4fd98c3 | ||
|
|
a93360eb1b | ||
|
|
0938de2694 | ||
|
|
a120f368c3 | ||
|
|
c028689012 | ||
|
|
27665154db | ||
|
|
659621e75d | ||
|
|
3ed6c5306c | ||
|
|
ffe7a32886 | ||
|
|
7b99804022 | ||
|
|
d24d4b1e83 | ||
|
|
d1a145545e | ||
|
|
46f3753425 | ||
|
|
4e0f6a5eeb | ||
|
|
6b79a5cad0 | ||
|
|
6756c43ab0 | ||
|
|
bcecb5d9c7 | ||
|
|
71ddc3c4bc | ||
|
|
7a10c45ed8 | ||
|
|
c669e5436b | ||
|
|
afbb40fe9b | ||
|
|
c7cdb509ce | ||
|
|
7b96936926 | ||
|
|
1d223683f1 | ||
|
|
94bf3ea469 | ||
|
|
de9224b5c7 | ||
|
|
6c0064390a | ||
|
|
2903acb34e | ||
|
|
d55b592264 | ||
|
|
e525051617 | ||
|
|
9a49c9ff66 | ||
|
|
b0727307b0 | ||
|
|
a004e61b2a | ||
|
|
1a7e633edc | ||
|
|
fb78e2baff | ||
|
|
9d143394dc | ||
|
|
249e3dd924 | ||
|
|
3398ab08ac | ||
|
|
2a197cfee5 | ||
|
|
e95aa05059 | ||
|
|
dc8df6fd1e | ||
|
|
babcbb1ec1 | ||
|
|
82e46b22ff | ||
|
|
1999b04ea2 | ||
|
|
ebc9c5761e | ||
|
|
f37f2e20a2 | ||
|
|
f11a779132 | ||
|
|
20694278a6 | ||
|
|
6a849d220b | ||
|
|
6001dc0214 | ||
|
|
93cedcc490 | ||
|
|
cad0c38965 | ||
|
|
bdb95a0832 | ||
|
|
e0580d39bf | ||
|
|
279b3b48f1 | ||
|
|
e3c63fca89 | ||
|
|
948b7db5ef | ||
|
|
503a8f5f18 | ||
|
|
8ab4f13543 | ||
|
|
894bf12899 | ||
|
|
a3b375e248 | ||
|
|
c840070a3a | ||
|
|
8a6551f281 | ||
|
|
88ae96cf25 | ||
|
|
acfd2e1fc1 | ||
|
|
6b02a87538 | ||
|
|
1a99e693df | ||
|
|
dd467ce86f | ||
|
|
7d7446ef7e | ||
|
|
6be4f002b8 | ||
|
|
bb6faee06d | ||
|
|
31cfeb3034 | ||
|
|
5d11818ffc | ||
|
|
e7ee143544 | ||
|
|
aa311b2688 | ||
|
|
0fca141a7b | ||
|
|
6a4bd341ab | ||
|
|
764750f591 | ||
|
|
2686b9a365 | ||
|
|
5a6ef644bd |
@@ -1112,6 +1112,69 @@
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "markschaver",
|
||||
"name": "Mark Schaver",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/7584?v=4",
|
||||
"profile": "http://schaver.com/",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "n8layman",
|
||||
"name": "Nathan Layman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/25353944?v=4",
|
||||
"profile": "https://github.com/n8layman",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "emmanuel-ferdman",
|
||||
"name": "Emmanuel Ferdman",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/35470921?v=4",
|
||||
"profile": "https://github.com/emmanuel-ferdman",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Tenormis",
|
||||
"name": "Tenormis",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/61572102?v=4",
|
||||
"profile": "https://github.com/Tenormis",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "djplaner",
|
||||
"name": "David Jones",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/225052?v=4",
|
||||
"profile": "http://djon.es/blog",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "s-jacob-powell",
|
||||
"name": "S. Jacob Powell",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/109111499?v=4",
|
||||
"profile": "https://github.com/s-jacob-powell",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "figdavi",
|
||||
"name": "Davi Figueiredo",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/99026991?v=4",
|
||||
"profile": "https://github.com/figdavi",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
10
.devcontainer/devcontainer.json
Normal file
10
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "Foam Dev Container",
|
||||
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18",
|
||||
"postCreateCommand": "yarn install",
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"]
|
||||
}
|
||||
}
|
||||
}
|
||||
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -74,13 +74,13 @@ body:
|
||||
id: os
|
||||
attributes:
|
||||
label: Operating System Version
|
||||
description: What opearting system are you using?
|
||||
description: What operating system are you using?
|
||||
placeholder: |
|
||||
- OS: [e.g. macOS, Windows, Linux]
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: vscode_version
|
||||
id: vscode_version
|
||||
attributes:
|
||||
label: Visual Studio Code Version
|
||||
description: |
|
||||
@@ -92,6 +92,6 @@ body:
|
||||
id: additional
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
description: |
|
||||
Add any other context about the problem here.
|
||||
The Foam log output for VSCode can be found here: https://github.com/foambubble/foam/blob/master/docs/features/foam-logging-in-vscode.md
|
||||
The Foam log output for VSCode can be found here: https://github.com/foambubble/foam/blob/main/docs/user/tools/foam-logging-in-vscode.md
|
||||
|
||||
6
.github/workflows/ci.yml
vendored
6
.github/workflows/ci.yml
vendored
@@ -3,10 +3,10 @@ name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
|
||||
jobs:
|
||||
typos-check:
|
||||
@@ -73,4 +73,4 @@ jobs:
|
||||
- name: Run Tests
|
||||
uses: GabrielBB/xvfb-action@v1.4
|
||||
with:
|
||||
run: yarn test --stream
|
||||
run: yarn test
|
||||
|
||||
2
.github/workflows/update-docs.yml
vendored
2
.github/workflows/update-docs.yml
vendored
@@ -3,7 +3,7 @@ name: Update Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- main
|
||||
paths:
|
||||
- docs/user/**/*
|
||||
- docs/.vscode/**/*
|
||||
|
||||
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -24,9 +24,8 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"jest.autoRun": "off",
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn jest",
|
||||
"jest.jestCommandLine": "yarn test:unit-with-specs",
|
||||
"gitdoc.enabled": false,
|
||||
"search.mode": "reuseEditor",
|
||||
"[typescript]": {
|
||||
|
||||
23
.vscode/tasks.json
vendored
23
.vscode/tasks.json
vendored
@@ -7,7 +7,28 @@
|
||||
"label": "watch: foam-vscode",
|
||||
"type": "npm",
|
||||
"script": "watch",
|
||||
"problemMatcher": "$tsc-watch",
|
||||
"problemMatcher": {
|
||||
"owner": "typescript",
|
||||
"fileLocation": ["relative", "${workspaceFolder}"],
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "^(.*?)\\((\\d+),(\\d+)\\):\\s+(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"message": 4
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": {
|
||||
"regexp": ".*"
|
||||
},
|
||||
"endsPattern": {
|
||||
"regexp": ".*"
|
||||
}
|
||||
}
|
||||
},
|
||||
"isBackground": true,
|
||||
"presentation": {
|
||||
"reveal": "always"
|
||||
|
||||
241
CLAUDE.md
Normal file
241
CLAUDE.md
Normal file
@@ -0,0 +1,241 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project overview
|
||||
|
||||
Foam is a personal knowledge management and sharing system, built on Visual Studio Code and GitHub. It allows users to organize research, keep re-discoverable notes, write long-form content, and optionally publish it to the web. The main goals are to help users create relationships between thoughts and information, supporting practices like building a "Second Brain" or a "Zettelkasten". Foam is free, open-source, and extensible, giving users ownership and control over their information. The target audience includes individuals interested in personal knowledge management, note-taking, and content creation, particularly those familiar with VS Code and GitHub.
|
||||
|
||||
## Quick Commands
|
||||
|
||||
All the following commands are to be executed from the `packages/foam-vscode` directory
|
||||
|
||||
### Development
|
||||
|
||||
- `yarn install` - Install dependencies
|
||||
- `yarn build` - Build all packages
|
||||
- `yarn watch` - Watch mode for development
|
||||
- `yarn clean` - Clean build outputs
|
||||
- `yarn reset` - Full clean, install, and build
|
||||
|
||||
### Testing
|
||||
|
||||
- `yarn test` - Run all tests (unit + integration)
|
||||
- `yarn test:unit-with-specs` - Run only unit tests (\*.test.ts files and the .spec.ts files marked a vscode-mock friendly)
|
||||
- `yarn test:e2e` - Run only integration tests (\*.spec.ts files)
|
||||
- `yarn lint` - Run linting
|
||||
- `yarn test-reset-workspace` to clean test workspace
|
||||
|
||||
Unit tests run in Node.js environment using Jest
|
||||
Integration tests require VS Code extension host
|
||||
|
||||
Unit tests are named `*.test.ts` and integration tests are `*.spec.ts`. These test files live alongside the code in the `src` directory. An integration test is one that has a direct or indirect dependency on `vscode` module.
|
||||
There is a mock `vscode` module that can be used to run most integration tests without starting VS Code. Tests that can use this mock are start with the line `/* @unit-ready */`.
|
||||
|
||||
- If you are interested in a test inside a `*.test.ts` file, run `yarn test:unit`
|
||||
- If you are interested in a test inside a `*.spec.ts` file that starts with `/* @unit-ready */` run `yarn test:unit-with-specs`
|
||||
- If you are interested in a test inside a `*.spec.ts` file that does not include `/* @unit-ready */` run `yarn test`
|
||||
|
||||
While in development we mostly want to use `yarn test:unit-with-specs`.
|
||||
When multiple tests are failing, look at all of them, but only focus on fixing the first one. Once that is fixed, run the test suite again and repeat the process.
|
||||
|
||||
When writing tests keep mocking to a bare minimum. Code should be written in a way that is easily testable and if I/O is necessary, it should be done in appropriate temporary directories.
|
||||
Never mock anything that is inside `packages/foam-vscode/src/core/`.
|
||||
|
||||
Use the utility functions from `test-utils.ts` and `test-utils-vscode.ts` and `test-datastore.ts`.
|
||||
|
||||
To improve readability of the tests, set up the test and tear it down within the test case (as opposed to use other functions like `beforeEach` unless it's much better to do it that way)
|
||||
|
||||
Never fix a test by adjusting the expectation if the expectation is correct, test must be fixed by addressing the issue with the code.
|
||||
|
||||
## Repository Structure
|
||||
|
||||
This is a monorepo using Yarn workspaces with the main VS Code extension in `packages/foam-vscode/`.
|
||||
|
||||
### Key Directories
|
||||
|
||||
- `packages/foam-vscode/src/core/` - Platform-agnostic business logic (NO vscode dependencies)
|
||||
- `packages/foam-vscode/src/features/` - VS Code-specific features and UI
|
||||
- `packages/foam-vscode/src/services/` - service implementations, might have VS Code dependency, but we try keep that to a minimum
|
||||
- `packages/foam-vscode/src/test/` - Test utilities and mocks
|
||||
- `docs/` - Documentation and user guides
|
||||
|
||||
### File Naming Patterns
|
||||
|
||||
Test files follow `*.test.ts` for unit tests and `*.spec.ts` for integration tests, living alongside the code in `src`. An integration test is one that has a direct or indirect dependency on `vscode` package.
|
||||
|
||||
### Important Constraint
|
||||
|
||||
Code in `packages/foam-vscode/src/core/` MUST NOT depend on the `vscode` library or any files outside the core directory. This maintains platform independence.
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
### Core Abstractions
|
||||
|
||||
**FoamWorkspace** - Central repository managing all resources (notes, attachments)
|
||||
|
||||
- Uses reversed trie for efficient resource lookup
|
||||
- Event-driven updates (onDidAdd, onDidUpdate, onDidDelete)
|
||||
- Handles identifier resolution for short-form linking
|
||||
|
||||
**FoamGraph** - Manages relationship graph between resources
|
||||
|
||||
- Tracks links and backlinks between resources
|
||||
- Real-time updates when workspace changes
|
||||
- Handles placeholder resources for broken links
|
||||
|
||||
**ResourceProvider Pattern** - Pluggable architecture for different file types
|
||||
|
||||
- `MarkdownProvider` for .md files
|
||||
- `AttachmentProvider` for other file types
|
||||
- Extensible for future resource types
|
||||
|
||||
**DataStore Interface** - Abstract file system operations
|
||||
|
||||
- Platform-agnostic file access with configurable filtering
|
||||
- Supports both local and remote file systems
|
||||
|
||||
### Feature Integration Pattern
|
||||
|
||||
Features are registered as functions receiving:
|
||||
|
||||
```typescript
|
||||
(context: ExtensionContext, foamPromise: Promise<Foam>) => void
|
||||
```
|
||||
|
||||
This allows features to:
|
||||
|
||||
- Register VS Code commands, providers, and event handlers
|
||||
- Access the Foam workspace when ready
|
||||
- Extend markdown-it for preview rendering
|
||||
|
||||
### Testing Conventions
|
||||
|
||||
- `*.test.ts` - Unit tests using Jest
|
||||
- `*.spec.ts` - Integration tests requiring VS Code extension host
|
||||
- Tests live alongside source code in `src/`
|
||||
- Test cases should be phrased in terms of aspects of the feature being tested (expected behaviors), as they serve both as validation of the code as well as documentation of what the expected behavior for the code is in different situations. They should include the happy paths and edge cases.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
We build production code together. I handle implementation details while you guide architecture and catch complexity early.
|
||||
|
||||
## Core Workflow: Research → Plan → Implement → Validate
|
||||
|
||||
**Start every feature with:** "Let me research the codebase and create a plan before implementing."
|
||||
|
||||
1. **Research** - Understand existing patterns and architecture
|
||||
2. **Plan** - Propose approach and verify with you
|
||||
3. **Implement** - Build with tests and error handling
|
||||
4. **Validate** - ALWAYS run formatters, linters, and tests after implementation
|
||||
|
||||
- Whenever working on a feature or issue, let's always come up with a plan first, then save it to a file called `/.agent/current-plan.md`, before getting started with code changes. Update this file as the work progresses.
|
||||
- Let's use pure functions where possible to improve readability and testing.
|
||||
|
||||
### Adding New Features
|
||||
|
||||
1. Create feature in `src/features/` directory
|
||||
2. Register feature in `src/features/index.ts`
|
||||
3. Add tests (both unit and integration as needed)
|
||||
4. Update configuration in `package.json` if needed
|
||||
|
||||
### Working on an issue
|
||||
|
||||
1. Get the issue information from github
|
||||
2. Define a step by step plan for addressing the issue
|
||||
3. Create tests for the feature
|
||||
4. Starting from the first test case, implement the feature so the test passes
|
||||
|
||||
### Core Logic Changes
|
||||
|
||||
1. Modify code in `src/core/` (ensure no vscode dependencies)
|
||||
2. Add comprehensive unit tests
|
||||
3. Update integration tests in features that use the core logic
|
||||
|
||||
## Configuration
|
||||
|
||||
The extension uses VS Code's configuration system with the `foam.*` namespace.
|
||||
You can find all the settings in `/packages/foam-vscode/package.json`
|
||||
|
||||
## Common Development Tasks
|
||||
|
||||
### Extending Core Functionality
|
||||
|
||||
When adding to `src/core/`:
|
||||
|
||||
- Keep platform-agnostic (no vscode imports)
|
||||
- Add comprehensive unit tests
|
||||
- Consider impact on graph and workspace state
|
||||
- Update relevant providers if needed
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **Runtime**: VS Code API, markdown parsing, file watching
|
||||
- **Development**: TypeScript, Jest, ESLint, esbuild
|
||||
- **Key Libraries**: remark (markdown parsing), lru-cache, lodash
|
||||
|
||||
The extension supports both Node.js and browser environments via separate build targets.
|
||||
|
||||
# GitHub CLI Integration
|
||||
|
||||
To interact with the github repo we will be using the `gh` command.
|
||||
ALWAYS ask before performing a write operation on Github.
|
||||
|
||||
## Common Commands for Claude Code Integration
|
||||
|
||||
### Issues
|
||||
|
||||
```bash
|
||||
# List all issues
|
||||
gh issue list
|
||||
|
||||
# Filter issues by milestone
|
||||
gh issue list --milestone "v1.0.0"
|
||||
|
||||
# Filter issues by assignee
|
||||
gh issue list --assignee @me
|
||||
gh issue list --assignee username
|
||||
|
||||
# Filter issues by label
|
||||
gh issue list --label "bug"
|
||||
gh issue list --label "enhancement,priority-high"
|
||||
|
||||
# Filter issues by state
|
||||
gh issue list --state open
|
||||
gh issue list --state closed
|
||||
gh issue list --state all
|
||||
|
||||
# Combine filters
|
||||
gh issue list --milestone "v1.0.0" --label "bug" --assignee @me
|
||||
|
||||
# View specific issue
|
||||
gh issue view 123
|
||||
|
||||
# Create issue
|
||||
gh issue create --title "Bug fix" --body "Description"
|
||||
|
||||
# Add comment to issue
|
||||
gh issue comment 123 --body "Update comment"
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
||||
```bash
|
||||
# List all PRs
|
||||
gh pr list
|
||||
|
||||
# Filter PRs the same way as for filters (for example, here is by milestone)
|
||||
gh pr list --milestone "v1.0.0"
|
||||
|
||||
# View PR details
|
||||
gh pr view 456
|
||||
|
||||
# Create PR
|
||||
gh pr create --title "Feature" --body "Description"
|
||||
|
||||
# Check out PR locally
|
||||
gh pr checkout 456
|
||||
|
||||
# Add review comment
|
||||
gh pr comment 456 --body "LGTM"
|
||||
```
|
||||
@@ -1,10 +1,11 @@
|
||||
---
|
||||
tags: todo, good-first-task
|
||||
---
|
||||
|
||||
# Contribution Guide
|
||||
|
||||
Foam is open to contributions of any kind, including but not limited to code, documentation, ideas, and feedback.
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase. For a comprehensive guide about contributing to open-source projects in general, [see here](https://sqldbawithabeard.com/2019/11/29/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/).
|
||||
This guide aims to help guide new and seasoned contributors getting around the Foam codebase. For a comprehensive guide about contributing to open-source projects in general, [see here](https://blog.robsewell.com/blog/how-to-fork-a-github-repository-and-contribute-to-an-open-source-project/).
|
||||
|
||||
## Getting Up To Speed
|
||||
|
||||
@@ -13,6 +14,8 @@ Before you start contributing we recommend that you read the following links:
|
||||
- [[principles]] - This document describes the guiding principles behind Foam.
|
||||
- [[code-of-conduct]] - Rules we hope every contributor aims to follow, allowing everyone to participate in our community!
|
||||
|
||||
To get yourself familiar with the codebase you can also browse [this repo](https://app.komment.ai/wiki/github/foambubble/foam)
|
||||
|
||||
## Diving In
|
||||
|
||||
We understand that diving in an unfamiliar codebase may seem scary,
|
||||
@@ -23,7 +26,8 @@ Finally, the easiest way to help, is to use it and provide feedback by [submitti
|
||||
|
||||
## Contributing
|
||||
|
||||
If you're interested in contributing, this short guide will help you get things set up locally (assuming [node.js >= v16](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
|
||||
If you're interested in contributing, this short guide will help you get things set up locally (assuming [node.js >= v18](https://nodejs.org/) and [yarn](https://yarnpkg.com/) are already installed on your system).
|
||||
You can also use the provided [[devcontainers]] to avoid installing dependencies locally. With the Dev Containers extension installed, open the repository in VS Code and run **Dev Containers: Reopen in Container**.
|
||||
|
||||
1. Fork the project to your Github account by clicking the "Fork" button on the top right hand corner of the project's [home repository page](https://github.com/foambubble/foam).
|
||||
2. Clone your newly forked repo locally:
|
||||
@@ -44,19 +48,19 @@ You should now be ready to start working!
|
||||
|
||||
Foam code and documentation live in the monorepo at [foambubble/foam](https://github.com/foambubble/foam/).
|
||||
|
||||
- [/docs](https://github.com/foambubble/foam/tree/master/docs): documentation and [[recipes]].
|
||||
- [/docs](https://github.com/foambubble/foam/tree/main/docs): documentation and [[recipes]].
|
||||
|
||||
Exceptions to the monorepo are:
|
||||
|
||||
- The starter template at [foambubble/foam-template](https://github.com/foambubble/)
|
||||
- All other [[recommended-extensions]] live in their respective GitHub repos
|
||||
|
||||
This project uses [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/).
|
||||
This project uses [Yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/).
|
||||
|
||||
Originally Foam had:
|
||||
|
||||
- [/packages/foam-core](https://github.com/foambubble/foam/tree/ee7a8919761f168d3931079adf21c5ad4d63db59/packages/foam-core) - Powers the core functionality in Foam across all platforms.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) - The core VS Code plugin.
|
||||
- [/packages/foam-vscode](https://github.com/foambubble/foam/tree/main/packages/foam-vscode) - The core VS Code plugin.
|
||||
|
||||
To improve DX we have moved the `foam-core` module into `packages/foam-vscode/src/core`, but from a development point of view it's useful to think of the `foam-vscode/src/core` "submodule" as something that might be extracted in the future.
|
||||
|
||||
@@ -81,9 +85,9 @@ Tests live alongside the code in `src`.
|
||||
|
||||
This guide assumes you read the previous instructions and you're set up to work on Foam.
|
||||
|
||||
1. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/master/.vscode/launch.json) to start a new extension host of VS Code. Open the "Run and Debug" Activity (the icon with the bug on the far left) and select "Run VSCode Extension" in the pop-up menu. Now hit F5 or click the green arrow "play" button to fire up a new copy of VS Code with your extension installed.
|
||||
1. Now we'll use the launch configuration defined at [`.vscode/launch.json`](https://github.com/foambubble/foam/blob/main/.vscode/launch.json) to start a new extension host of VS Code. Open the "Run and Debug" Activity (the icon with the bug on the far left) and select "Run VSCode Extension" in the pop-up menu. Now hit F5 or click the green arrow "play" button to fire up a new copy of VS Code with your extension installed.
|
||||
|
||||
2. In the new extension host of VS Code that launched, open a Foam workspace (e.g. your personal one, or a test-specific one created from [foam-template](https://github.com/foambubble/foam-template)). This is strictly not necessary, but the extension won't auto-run unless it's in a workspace with a `.vscode/foam.json` file.
|
||||
2. In the new extension host of VS Code that launched, open a Foam workspace (e.g. your personal one, or a test-specific one created from [foam-template](https://github.com/foambubble/foam-template)).
|
||||
|
||||
3. Test a command to make sure it's working as expected. Open the Command Palette (Ctrl/Cmd + Shift + P) and select "Foam: Update Markdown Reference List". If you see no errors, it's good to go!
|
||||
|
||||
@@ -108,6 +112,7 @@ Feel free to modify and submit a PR if this guide is out-of-date or contains err
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[principles]: ../principles.md "Principles"
|
||||
[code-of-conduct]: code-of-conduct.md "Code of Conduct"
|
||||
[devcontainers]: devcontainers.md "Using Dev Containers"
|
||||
[recipes]: ../user/recipes/recipes.md "Recipes"
|
||||
[recommended-extensions]: ../user/getting-started/recommended-extensions.md "Recommended Extensions"
|
||||
[//end]: # "Autogenerated link references"
|
||||
|
||||
13
docs/dev/devcontainers.md
Normal file
13
docs/dev/devcontainers.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Using Dev Containers
|
||||
|
||||
Foam provides a [devcontainer](https://devcontainer.ai/) configuration to make it easy to contribute without installing Node and Yarn locally.
|
||||
|
||||
## Quick start
|
||||
|
||||
1. Install [VS Code](https://code.visualstudio.com/) and the [Dev Containers](https://aka.ms/vscode-remote/download/extension) extension.
|
||||
2. Open the Foam repository in VS Code.
|
||||
3. Run **Dev Containers: Reopen in Container** from the command palette.
|
||||
|
||||
This will build a Docker image with Node 18 and install dependencies using `yarn install`. Once ready you can run the usual build and test commands from the integrated terminal.
|
||||
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
# Foam Core 2020-07-11
|
||||
|
||||
Present: @jevakallio, @riccardoferretti
|
||||
|
||||
### Tests
|
||||
|
||||
- How do we know this approach works?
|
||||
- Supports renaming
|
||||
- Supports searching with (attribute-x)
|
||||
- Find dead links
|
||||
|
||||
### Getting started
|
||||
|
||||
- Land work to master
|
||||
- Create a foam-core package
|
||||
|
||||
-
|
||||
|
||||
### Open questions
|
||||
|
||||
- How should writing to files work
|
||||
- What if affected notes have unsaved changes
|
||||
|
||||
### Graph methods
|
||||
|
||||
- get all
|
||||
- search by
|
||||
- tag
|
||||
- free text
|
||||
- [[todo]]: how do vs code search editors work? are they pluggable? what do they need?
|
||||
- find dead links
|
||||
- for linters
|
||||
- serialize/toJSON (for visualizers)
|
||||
- subscribe to changes
|
||||
- find if a link exists (and which link) in a given row / column position + return it's start and end position - this would probably be needed e.g. to CTRL-hovering to work properly
|
||||
|
||||
### Node methods
|
||||
|
||||
- rename node and all links to that node
|
||||
- get links
|
||||
- forward links (for link lists)
|
||||
- backlinks (with surrounding context)
|
||||
|
||||
### Node definition
|
||||
|
||||
What do we need the node (and edge metadata) to contain:
|
||||
|
||||
- `id`: tbd
|
||||
- should be unique, needs some kind of unique gen function
|
||||
- should be reconstructable even if links are not updated every time
|
||||
- what happens during rename? is reparenting the graph going to be hard?
|
||||
- do id's need to be persistent, or can we create them per in-memory session, keep them stable despite renames, and then next session generate a new id?
|
||||
- Ideally should be a path to file, so it's easy to look up from the graph by id for renaming
|
||||
- `type`: Note | Image | etc
|
||||
- `title`: can be read from markdown title or frontmatter metadata
|
||||
- `path`: full path to file, relative to workspace (graph) root
|
||||
- `links`:
|
||||
- `id`: File to link to
|
||||
- `text`: The link label
|
||||
- `type` markdown | mediawiki | image | http
|
||||
- `section`: : Anchor link to a heading in target note, if we want to add support for linking to sections
|
||||
- `block` (ref)
|
||||
- Positional data from AST?
|
||||
- `tags`
|
||||
|
||||
### Markdown layer
|
||||
|
||||
- `source`: raw markdown (rename?)
|
||||
- `ast`: raw markdown ast
|
||||
- `checksum`: if we do caching
|
||||
|
||||
### Link text
|
||||
|
||||
// some-file.md
|
||||
// # Some File
|
||||
|
||||
Write -> Store on disk
|
||||
[[Some File]] -> [Some File](some-file.md)
|
||||
|
||||
Editing
|
||||
[Some File](some-file.md)
|
||||
|
||||
On disk (could be solved by migration)
|
||||
[[some-file]]
|
||||
[[Some File]]
|
||||
|
||||
- docs/index.md -> Index
|
||||
- notes/index.md -> Index
|
||||
|
||||
[[Index]]
|
||||
[[Index | notes/index.md]]
|
||||
|
||||
[Index] docs/index.md
|
||||
[Index | notes/index.md]: notes/index.md
|
||||
|
||||
[[Some File | path/to/some-file.md]]
|
||||
|
||||
Do we apply any constraints:
|
||||
|
||||
- `[[file-name-without-extension]]`
|
||||
- `[[file-name-with-extension.md]]`
|
||||
- `[[Title Cased File Name]]`
|
||||
|
||||
Not supported by Markdown Notes:
|
||||
|
||||
- `[[path/to/file-name.md]]` - Just use markdown links
|
||||
- `[[Target Note Title]]`
|
||||
|
||||
Issues:
|
||||
|
||||
- Name clashes in directories
|
||||
- Name clashes between extensions
|
||||
- Renaming
|
||||
- Change filename/title needs to reflect everywhere
|
||||
- Orphaning
|
||||
|
||||
- If we can't rely on in-memory process to rename things correctly while changes happen (e.g. file is renamed, moved, deleted, or titled) <ref id="1" />
|
||||
|
||||
Solving this issue is necessarily heuristic. We could try to write smart solutions, plus a linter for orphans
|
||||
|
||||
How others solve this:
|
||||
|
||||
- Unique ids -- could support optionally as part of file name or front matter metadata. Should not be required.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
[todo]: ../todo.md "Todo"
|
||||
[Index]: ../../index.md "Foam"
|
||||
[//end]: # "Autogenerated link references"
|
||||
@@ -66,8 +66,9 @@ The potential solution:
|
||||
- For edit-time
|
||||
- Make edit-time link reference definition generation optional via user settings. They should be on by default, and generating valid markdown links with a relative path to a `.md` file.
|
||||
- Make format of the link reference definition configurable (whether to include '.md' or not)
|
||||
- Out of recommended extensions, currently only "markdown links" doesn't support them (?). However even its [code](https://github.com/tchayen/markdown-links/blob/master/src/parsing.ts#L25) seems to include wikilink parser, so it might just be a bug?
|
||||
- Out of recommended extensions, currently only "markdown links" doesn't support them (?). However even its [code](https://github.com/tchayen/markdown-links/blob/main/src/parsing.ts#L25) seems to include wikilink parser, so it might just be a bug?
|
||||
- For build-time
|
||||
|
||||
- To satisfy mutually incompatible constraints between GitHub UI, VSCode UI, and GitHub Pages, we should add a pre-processing/build step for pushing to GitHub Pages.
|
||||
- This would be a GitHub action (or a local script, ran via foam-cli) that outputs publish-friendly markdown format for static site generators and other publishing tools
|
||||
- This build step should be pluggable, so that other transformations could be ran during it
|
||||
@@ -125,6 +126,7 @@ The potential solution:
|
||||
- With Foam repo, just use edit-time link reference definitions with '.md' extension - this makes the links work in the GitHub UI
|
||||
- Have publish target defined for GitHub pages, that doesn't use '.md' extension, but still has the link reference definitions. Generate the output into gh-pages branch (or separate repo) with automation.
|
||||
- This naturally requires first removing the existing link reference definitions during the build
|
||||
|
||||
- Other
|
||||
- To clean up the search results, remove link reference definition section guards (assuming that these are not defined by the markdown spec). Use unifiedjs parse trees to identify if there's missing (or surplus) definitions (check if they are identified properly by the library), and just add the needed definitions to the bottom of the file (without guards) AND remove them if they are not needed (anywhere from the file).
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ If you want to pick up work in this category, you should have a plan on how long
|
||||
|
||||
Everything else, categorised into themes. Just because something is on this list doesn't mean it'll get done. If you're interested in working on items in this category, check the [[contribution-guide]] for how to get started.
|
||||
|
||||
If a roadmap item is a stub, **consider** opening a [GitHub issue](https://github.com/foambubble/foam/issues) to start a conversation to avoid situations where the implementation does not fit long term vision and roadmap. _Note that this is optional. The only centralised governance in Foam is to decide what ends up in the official [template](https://github.com/foambubble/foam-template), [documentation](https://github.com/foambubble/foam) and [extension](https://github.com/foambubble/foam/tree/master/packages/foam-vscode). You are free to build whatever you want for yourself, and we'd love if you shared it with us, but you are by no means obligated to do so!_
|
||||
If a roadmap item is a stub, **consider** opening a [GitHub issue](https://github.com/foambubble/foam/issues) to start a conversation to avoid situations where the implementation does not fit long term vision and roadmap. _Note that this is optional. The only centralised governance in Foam is to decide what ends up in the official [template](https://github.com/foambubble/foam-template), [documentation](https://github.com/foambubble/foam) and [extension](https://github.com/foambubble/foam/tree/main/packages/foam-vscode). You are free to build whatever you want for yourself, and we'd love if you shared it with us, but you are by no means obligated to do so!_
|
||||
|
||||
**When creating GitHub issues to discuss roadmap items, link them here.**
|
||||
|
||||
|
||||
@@ -1,329 +0,0 @@
|
||||
# Templates v2 Proposal <!-- omit in TOC -->
|
||||
|
||||
The current capabilities of templates is limited in some important ways. This document aims to propose a design that addresses these shortcomings.
|
||||
|
||||
**IMPORTANT: This design is merely a proposal of a design that could be implemented. It DOES NOT represent a commitment by `Foam` developers to implement the features outlined in this document. This document is merely a mechanism to facilitate discussion of a possible future direction for `Foam`.**
|
||||
|
||||
- [Introduction](#introduction)
|
||||
- [Limitations of current templating](#limitations-of-current-templating)
|
||||
- [Too much friction to create a new note](#too-much-friction-to-create-a-new-note)
|
||||
- [Manual note creation (Mouse + Keyboard)](#manual-note-creation-mouse--keyboard)
|
||||
- [Manual note creation (Keyboard)](#manual-note-creation-keyboard)
|
||||
- [Foam missing note creation](#foam-missing-note-creation)
|
||||
- [`Markdown Notes: New Note` (Keyboard)](#markdown-notes-new-note-keyboard)
|
||||
- [Foam template note creation (Keyboard)](#foam-template-note-creation-keyboard)
|
||||
- [Templating of daily notes](#templating-of-daily-notes)
|
||||
- [Templating of filepaths](#templating-of-filepaths)
|
||||
- [Goal / Philosophy](#goal--philosophy)
|
||||
- [Proposal](#proposal)
|
||||
- [Summary](#summary)
|
||||
- [Add a `${title}` and `${titleSlug}` template variables](#add-a-title-and-titleslug-template-variables)
|
||||
- [Add a `Foam: Create New Note` command and hotkey](#add-a-foam-create-new-note-command-and-hotkey)
|
||||
- [Case 1: `.foam/templates/new-note.md` doesn't exist](#case-1-foamtemplatesnew-notemd-doesnt-exist)
|
||||
- [Case 2: `.foam/templates/new-note.md` exists](#case-2-foamtemplatesnew-notemd-exists)
|
||||
- [Change missing wikilinks to use the default template](#change-missing-wikilinks-to-use-the-default-template)
|
||||
- [Add a metadata section to templates](#add-a-metadata-section-to-templates)
|
||||
- [Example](#example)
|
||||
- [Add a replacement for `dateFormat`](#add-a-replacement-for-dateformat)
|
||||
- [Add support for daily note templates](#add-support-for-daily-note-templates)
|
||||
- [Eliminate all `foam.openDailyNote` settings](#eliminate-all-foamopendailynote-settings)
|
||||
- [Summary: resulting behaviour](#summary-resulting-behaviour)
|
||||
- [`Foam: Create New Note`](#foam-create-new-note)
|
||||
- [`Foam: Open Daily Note`](#foam-open-daily-note)
|
||||
- [Navigating to missing wikilinks](#navigating-to-missing-wikilinks)
|
||||
- [`Foam: Create Note From Template`](#foam-create-note-from-template)
|
||||
- [Extensions](#extensions)
|
||||
- [More variables in templates](#more-variables-in-templates)
|
||||
- [`defaultFilepath`](#defaultfilepath)
|
||||
- [Arbitrary hotkey -> template mappings?](#arbitrary-hotkey---template-mappings)
|
||||
|
||||
## Introduction
|
||||
|
||||
Creating of new notes in Foam is too cumbersome and slow. Despite their power, Foam templates can currently only be used in very limited scenarios.
|
||||
|
||||
This proposal aims to address these issues by streamlining note creation and by allowing templates to be used everywhere.
|
||||
|
||||
## Limitations of current templating
|
||||
|
||||
### Too much friction to create a new note
|
||||
|
||||
Creating new notes should an incredibly streamlined operation. There should be no friction to creating new notes.
|
||||
|
||||
Unfortunately, all of the current methods for creating notes are cumbersome.
|
||||
|
||||
#### Manual note creation (Mouse + Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. Click the new file button
|
||||
3. Provide a filename
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### Manual note creation (Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. `⌘N` to create a new file
|
||||
3. `⌘S` to save the file and give it a filename
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### Foam missing note creation
|
||||
|
||||
1. Open an existing note in the directory where you want the note
|
||||
2. Use the wikilinks syntax to create a link to the title of the note you want to have
|
||||
3. Use `Ctrl+Click`/`F12` to create the new file
|
||||
4. Manually enter the template contents you want
|
||||
|
||||
#### `Markdown Notes: New Note` (Keyboard)
|
||||
|
||||
1. Navigate to the directory where you want the note
|
||||
2. `Shift+⌘P` to open the command pallette
|
||||
3. Type `New Note` until it appears in the list. Press `Enter/Return` to select it.
|
||||
4. Enter a title for the note
|
||||
5. Manually enter the template contents you want
|
||||
|
||||
#### Foam template note creation (Keyboard)
|
||||
|
||||
1. `Shift+⌘P` to open the command pallette
|
||||
2. Type `Create New Note From Template` until it appears in the list. Press `Enter/Return` to select it.
|
||||
3. Use the arrow keys (or type the template name) to select the template. Press `Enter/Return` to select it.
|
||||
4. Modify the filepath to match the desired directory + filename. Press `Enter/Return` to select it.
|
||||
|
||||
All of these steps are far too cumbersome. And only the last one allows the use of templates.
|
||||
|
||||
### Templating of daily notes
|
||||
|
||||
Currently `Open Daily Note` opens an otherwise empty note, with a title defined by the `foam.openDailyNote.titleFormat` setting.
|
||||
Daily notes should be able to be fully templated as well.
|
||||
|
||||
### Templating of filepaths
|
||||
|
||||
As discussed in ["Template the filepath in `openDailyNote`"](https://github.com/foambubble/foam/issues/523), it would be useful to be able to specify the default filepaths of templates. For example, many people include timestamps in their filepaths.
|
||||
|
||||
## Goal / Philosophy
|
||||
|
||||
In a sentence: **Creating a new note should be a single button press and should use templates.**
|
||||
|
||||
## Proposal
|
||||
|
||||
1. Add a new `Foam: Create New Note` that is the streamlined counterpart to the more flexible `Foam: Create New Note From Template`
|
||||
2. Use templates everywhere
|
||||
3. Add metadata into the actual templates themselves in order to template the filepaths themselves.
|
||||
|
||||
### Summary
|
||||
|
||||
This can be done through a series of changes to the way that templates are implemented:
|
||||
|
||||
1. Add a `${title}` and `${titleSlug}` template variables
|
||||
2. Add a `Foam: Create New Note` command and hotkey
|
||||
3. Change missing wikilinks to use the default template
|
||||
4. Add a metadata section to templates
|
||||
5. Add a replacement for `dateFormat`
|
||||
6. Add support for daily note templates
|
||||
7. Eliminate all `foam.openDailyNote` settings
|
||||
|
||||
I've broken it out into these steps to show that the overall proposal can be implemented piecemeal in independent PRs that build on one another.
|
||||
|
||||
### Add a `${title}` and `${titleSlug}` template variables
|
||||
|
||||
When you use `Markdown Notes: New Note`, and give it a title, the title is formatted as a filename and also used as the title in the resulting note.
|
||||
|
||||
**Example:**
|
||||
|
||||
Given the title `Living in a dream world` to `Markdown Notes: New Note`, the filename is `living-in-a-dream-world.md` and the file contents are:
|
||||
|
||||
```markdown
|
||||
# Living in a dream world
|
||||
```
|
||||
|
||||
When creating a note from a template in Foam, you should be able to use a `${title}` variable. If the template uses the `${title}` variable, the user will be prompted for a title when they create a note from a template.
|
||||
|
||||
Example:
|
||||
|
||||
Given this `.foam/templates/my_template.md` template that uses the `${title}` variable:
|
||||
|
||||
```markdown
|
||||
# ${title}
|
||||
```
|
||||
|
||||
When a user asks for a new note using this template (eg. `Foam: Create New Note From Template`), VSCode will first ask the user for a title then provide it to the template, producing:
|
||||
|
||||
```markdown
|
||||
# Living in a dream world
|
||||
```
|
||||
|
||||
There will also be a `${titleSlug}` variable made available, which will be the "slugified" version of the title (eg. `living-in-a-dream-world`). This will be useful in later steps where we want to template the filepath of a template.
|
||||
|
||||
### Add a `Foam: Create New Note` command and hotkey
|
||||
|
||||
Instead of using `Markdown Notes: New Note`, Foam itself will have a `Create New Note` command that creates notes using templates.
|
||||
|
||||
This would open use the template found at `.foam/templates/new-note.md` to create the new note.
|
||||
|
||||
`Foam: Create New Note` will offer the fastest workflow for creating a note when you don't need customization, while `Foam: Create New Note From Template` will remain to serve a fully customizable (but slower) workflow.
|
||||
|
||||
#### Case 1: `.foam/templates/new-note.md` doesn't exist
|
||||
|
||||
If `.foam/templates/new-note.md` doesn't exist, it behaves the same as `Markdown Notes: New Note`:
|
||||
|
||||
* it would ask for a title and create the note in the current directory. It would open a note with the note containing the title.
|
||||
|
||||
**Note:** this would use an implicit default template, making use of the `${title}` variable.
|
||||
|
||||
#### Case 2: `.foam/templates/new-note.md` exists
|
||||
|
||||
If `.foam/templates/new-note.md` exists:
|
||||
|
||||
* it asks for the note title and creates the note in the current directory
|
||||
|
||||
**Progress:** At this point, we have a faster way to create new notes from templates.
|
||||
|
||||
### Change missing wikilinks to use the default template
|
||||
|
||||
Clicking on a dangling/missing wikilink should be equivalent to calling `Foam: Create New Note` with the contents of the link as the title.
|
||||
That way, creating a note by navigating to a missing note uses the default template.
|
||||
|
||||
### Add a metadata section to templates
|
||||
|
||||
* The `Foam: New Note` command creates a new note in the current directory. This is a sensible default that makes it quick, but lacks flexibility.
|
||||
* The `Foam: Create New Note From Template` asks the user to confirm/customize the filepath. This is more flexible but slower since there are more steps involved.
|
||||
|
||||
Both commands use templates. It would be nice if we could template the filepaths as well as the template contents (See ["Template the filepath in `openDailyNote`"](https://github.com/foambubble/foam/issues/523) for a more in-depth discussion the benefits of filepath templating).
|
||||
|
||||
In order to template the filepath, there needs to be a place where metadata like this can be specified.
|
||||
I think this metadata should be stored alongside the templates themselves. That way, it can make use of all the same template variable available to the templates themselves.
|
||||
|
||||
Conceptually, adding metadata to the templates is similar to Markdown frontmatter, though the choice of exact syntax for adding this metadata will have to be done with care since the templates can contain arbitrary contents including frontmatter.
|
||||
|
||||
#### Example
|
||||
|
||||
A workable syntax is still to be determined.
|
||||
While this syntax probably doesn't work as a solution, for this example I will demonstrate the concept using a second frontmatter block:
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `journal/${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}_${titleSlug}.md`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
---
|
||||
created: ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}T${CURRENT_HOUR}:${CURRENT_MINUTE}:${CURRENT_SECOND}
|
||||
tags: []
|
||||
---
|
||||
|
||||
# ${title}
|
||||
```
|
||||
|
||||
In this example, using this template improves the UX:
|
||||
|
||||
In `Foam: Create New Note` workflow, having `filepath` metadata within `.foam/templates/new-note.md` allows for control over the filepath without having to introduce any more UX steps to create a new note. It's still just a hotkey away and a title.
|
||||
|
||||
As we'll see, when it comes to allowing daily notes to be templated, we don't even need to use `${title}` in our template, in which case we don't we don't even need to prompt for a title.
|
||||
|
||||
In the `Create New Note From Template` workflow, during the step where we allow the user to customize the filepath, it will already templated according to the `filepath` in the template's metadata. This means that the user has to make fewer changes to the path, especially in cases where they want to include things like datetimes in the filenames. This makes it faster (eg. don't have to remember what day it is, and don't have to type it) and less error-prone (eg. when they accidentally type the wrong date).
|
||||
|
||||
### Add a replacement for `dateFormat`
|
||||
|
||||
`foam.openDailyNote.filenameFormat` uses `dateFormat()` to put the current timestamp into the daily notes filename. This is much more flexible than what is available in VSCode Snippet variables. Before daily notes are switched over to use templates, we will have to come up with another mechanism/syntax to allow for calls to `dateFormat()` within template files.
|
||||
|
||||
This would be especially useful in the migration of users to the new daily notes templates. For example, if `.foam/templates/daily-note.md` is unset, then we could generate an implicit template for use by `Foam: Open Daily Note`. Very roughly something like:
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `${foam.openDailyNote.directory}/${foam.openDailyNote.filenameFormat}.${foam.openDailyNote.fileExtension}`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
# ${foam.openDailyNote.titleFormat}
|
||||
```
|
||||
|
||||
### Add support for daily note templates
|
||||
|
||||
With the above features implemented, making daily notes use templates is simple.
|
||||
|
||||
We define a `.foam/templates/daily-note.md` filepath that the `Foam: Open Daily Note` command will always use to find its daily note template.
|
||||
If `.foam/templates/daily-note.md` does not exist, it falls back to a default, implicitly defined daily notes template (which follows the default behaviour of the current `foam.openDailyNote` settings).
|
||||
|
||||
Both `Foam: Open Daily Note` and `Foam: Create New Note` can share all of the implementation code, with the only differences being the hotkeys used and the template filepath used.
|
||||
|
||||
Example daily note template (again using the example syntax of the foam-specific frontmatter block):
|
||||
|
||||
```markdown
|
||||
<!-- The below front-matter block is for foam-specific template settings -->
|
||||
<!-- It is removed when the user creates a new note using this template -->
|
||||
---
|
||||
<!-- The default filepath to use when using this template -->
|
||||
<!-- Relative paths are relative to the workspace, absolute paths are absolute -->
|
||||
<!-- Note that you can include VSCode snippet variables to template the path -->
|
||||
filepath: `journal/${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}.md`
|
||||
---
|
||||
|
||||
<!-- The actual contents of the template begin after the `---` thematic break immediately below this line-->
|
||||
---
|
||||
# ${CURRENT_YEAR}-${CURRENT_MONTH}-${CURRENT_DATE}
|
||||
```
|
||||
|
||||
Since there is no use of the `${title}` variable, opening the daily note behaves exactly as it does today and automatically opens the note with no further user interaction.
|
||||
|
||||
### Eliminate all `foam.openDailyNote` settings
|
||||
|
||||
Now that all of the functionality of the `foam.openDailyNote` settings have been obviated, these settings can be removed:
|
||||
|
||||
* `foam.openDailyNote.directory`, `foam.openDailyNote.filenameFormat`, and `foam.openDailyNote.fileExtension` can be specified in the `filepath` metadata of the daily note template.
|
||||
* `foam.openDailyNote.titleFormat` has been replaced by the ability to fully template the daily note, including the title.
|
||||
|
||||
## Summary: resulting behaviour
|
||||
|
||||
### `Foam: Create New Note`
|
||||
|
||||
A new command optimized for speedy creation of new notes. This will become the default way to create new notes. In its fastest form, it simply opens the new note with no further user interaction.
|
||||
|
||||
### `Foam: Open Daily Note`
|
||||
|
||||
Simplified since it no longer has its custom settings, and re-uses all the same implementation code as `Foam: Create New Note`.
|
||||
Templates can now be used with daily notes.
|
||||
|
||||
### Navigating to missing wikilinks
|
||||
|
||||
Now creates the new notes using the default template. Re-uses all the same implementation code as `Foam: Create New Note`
|
||||
Now uses the contents of the wikilink as the `${title}` parameter for the template.
|
||||
|
||||
### `Foam: Create Note From Template`
|
||||
|
||||
Almost the exact same as it is today. However, with `${title}` and `filepath` templating, users will have less changes to make in the filepath confirmation step.
|
||||
It's the slower but more powerful version of `Foam: Create New Note`, allowing you to pick any template, as well as customize the filepath.
|
||||
|
||||
## Extensions
|
||||
|
||||
In addition to the ideas of this proposal, there are ways we could imagine extending it. These are all "out of scope" for this design, but thinking about them could be useful to guide our thinking about this design.
|
||||
|
||||
### More variables in templates
|
||||
|
||||
`${title}` is necessary in this case to replace the functionality of `Markdown Notes: New Note`.
|
||||
However, one could imagine that this pattern of "Ask the user for a value for missing variable values" could be useful in other situations too.
|
||||
Perhaps users could even define their own (namespaced) template variables, and Foam would ask them for values to use for each when creating a note using a template that used those variables.
|
||||
|
||||
### `defaultFilepath`
|
||||
|
||||
By using `defaultFilepath` instead of `filepath` in the metadata section, you could have more control over the note creation without having to fall back to the full `Create New Note From Template` workflow.
|
||||
|
||||
* `filepath` will not ask the user for the file path, simply use the value provided (as described above)
|
||||
* `defaultFilepath` will ask the user for the file path, pre-populating the file path using `defaultFilepath`
|
||||
|
||||
The first allows "one-click" note creation, the second more customization.
|
||||
This might not be necessary, or this might not be the right way to solve the problem. We'll see.
|
||||
|
||||
### Arbitrary hotkey -> template mappings?
|
||||
|
||||
`Foam: Open Daily Note` and `Foam: Create New Note` only differ by their hotkey and their default template setting.
|
||||
Is there a reason/opportunity to abstract this further and allow for users to define custom `hotkey -> template` mappings?
|
||||
@@ -1,16 +1,16 @@
|
||||
# Releasing Foam
|
||||
|
||||
1. Get to the latest code
|
||||
- `git checkout master && git fetch && git rebase`
|
||||
- `git checkout main && git fetch && git rebase`
|
||||
2. Sanity checks
|
||||
- `yarn reset`
|
||||
- `yarn test`
|
||||
3. Update change log
|
||||
3. Update change log
|
||||
- `./packages/foam-vscode/CHANGELOG.md`
|
||||
- `git add *`
|
||||
- `git commit -m"Preparation for next release"`
|
||||
4. Update version
|
||||
- `$ yarn version-extension <version>` (where `version` is `patch/minor/major`)
|
||||
- `$ yarn version-extension <version>` (where `version` is `patch/minor/major`)
|
||||
5. Package extension
|
||||
- `$ yarn package-extension`
|
||||
6. Publish extension
|
||||
|
||||
130
docs/dev/testing.md
Normal file
130
docs/dev/testing.md
Normal file
@@ -0,0 +1,130 @@
|
||||
# Testing in Foam VS Code Extension
|
||||
|
||||
This document explains the testing strategy and conventions used in the Foam VS Code extension.
|
||||
|
||||
## Test File Types
|
||||
|
||||
We use two distinct types of test files, each serving different purposes:
|
||||
|
||||
### `.test.ts` Files - Pure Unit Tests
|
||||
|
||||
- **Purpose**: Test business logic and algorithms in complete isolation
|
||||
- **Dependencies**: No VS Code APIs dependencies
|
||||
- **Environment**: Pure Jest with Node.js
|
||||
- **Speed**: Very fast execution
|
||||
- **Location**: Throughout the codebase alongside source files
|
||||
|
||||
### `.spec.ts` Files - Integration Tests with VS Code APIs
|
||||
|
||||
- **Purpose**: Test features that integrate with VS Code APIs and user workflows
|
||||
- **Dependencies**: Will likely depend on VS Code APIs (`vscode` module), otherwise avoid incurring the performance hit
|
||||
- **Environment**: Can run in TWO environments:
|
||||
- **Mock Environment**: Jest with VS Code API mocks (fast)
|
||||
- **Real VS Code**: Full VS Code extension host (slow but comprehensive)
|
||||
- **Speed**: Depends on environment (see performance section below)
|
||||
- **Location**: Primarily in `src/features/` and service layers
|
||||
|
||||
## Key Principle: Environment Flexibility for `.spec.ts` Files
|
||||
|
||||
**`.spec.ts` files use VS Code APIs**, but they can run in different environments:
|
||||
|
||||
- **Mock Environment**: Uses our VS Code API mocks for speed
|
||||
- **Real VS Code**: Uses actual VS Code extension host for full integration testing
|
||||
|
||||
This dual-environment capability allows us to:
|
||||
|
||||
- Run specs quickly during development (mock environment)
|
||||
- Verify full integration during CI/CD (real VS Code environment)
|
||||
- Gradually migrate specs to mock-compatible implementations
|
||||
|
||||
## Performance Comparison
|
||||
|
||||
| Test Type | Environment | Typical Duration | VS Code APIs |
|
||||
| --------------------- | ---------------------- | ---------------- | ---------------- |
|
||||
| **`.test.ts`** | Pure Jest | fastest | **No** |
|
||||
| **`.spec.ts` (mock)** | Jest + VS Code Mocks | fast | **Yes** (mocked) |
|
||||
| **`.spec.ts` (real)** | VS Code Extension Host | sloooooow. | **Yes** (real) |
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Available Commands
|
||||
|
||||
- **`yarn test:unit`**: Runs only `.test.ts` files (no VS Code dependencies)
|
||||
- **`yarn test:unit-with-specs`**: Runs `.test.ts` + `@unit-ready` marked `.spec.ts` files using mocks
|
||||
- **`yarn test:e2e`**: Runs all `.spec.ts` files in full VS Code extension host
|
||||
- **`yarn test`**: Runs both unit and e2e test suites sequentially
|
||||
|
||||
## Mock Environment Migration
|
||||
|
||||
We're gradually enabling `.spec.ts` files to run in our fast mock environment while maintaining their ability to run in real VS Code.
|
||||
|
||||
### The `@unit-ready` Annotation
|
||||
|
||||
Spec files marked with `/* @unit-ready */` can run in both environments:
|
||||
|
||||
```typescript
|
||||
/* @unit-ready */
|
||||
import * as vscode from 'vscode';
|
||||
// ... test uses VS Code APIs but works with our mocks
|
||||
```
|
||||
|
||||
### Common Migration Fixes
|
||||
|
||||
**Configuration defaults**: Our mocks don't load package.json defaults
|
||||
|
||||
```typescript
|
||||
// Before
|
||||
const format = getFoamVsCodeConfig('openDailyNote.filenameFormat');
|
||||
|
||||
// After (defensive)
|
||||
const format = getFoamVsCodeConfig(
|
||||
'openDailyNote.filenameFormat',
|
||||
'yyyy-mm-dd'
|
||||
);
|
||||
```
|
||||
|
||||
**File system operations**: Ensure proper async handling
|
||||
|
||||
```typescript
|
||||
// Mock file operations are immediate but still async
|
||||
await vscode.workspace.fs.writeFile(uri, content);
|
||||
```
|
||||
|
||||
### When NOT to Migrate
|
||||
|
||||
Some specs should remain real-VS-Code-only:
|
||||
|
||||
- Tests verifying complex VS Code UI interactions
|
||||
- Tests requiring real file system watching with timing
|
||||
- Tests validating extension packaging or activation
|
||||
- Tests that depend on VS Code's complex internal state management
|
||||
|
||||
## Mock System Capabilities
|
||||
|
||||
Our `vscode-mock.ts` provides comprehensive VS Code API mocking:
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
When adding new tests:
|
||||
|
||||
1. **Choose the right type**:
|
||||
|
||||
- Use `.test.ts` for pure business logic with no VS Code dependencies
|
||||
- Use `.spec.ts` for anything that needs VS Code APIs
|
||||
|
||||
2. **Consider mock compatibility**:
|
||||
|
||||
- When writing `.spec.ts` files, consider if they could run in mock environment
|
||||
- Add `/* @unit-ready */` if the test works with our mocks
|
||||
|
||||
3. **Follow naming conventions**:
|
||||
|
||||
- Test files should be co-located with source files when possible
|
||||
- Use descriptive test names that explain the expected behavior
|
||||
|
||||
4. **Performance awareness**:
|
||||
- Prefer unit tests for business logic (fastest)
|
||||
- Use mock-compatible specs for VS Code integration (fast)
|
||||
- Reserve real VS Code specs for complex integration scenarios (comprehensive)
|
||||
|
||||
This testing strategy gives us the best of both worlds: fast feedback during development and comprehensive integration verification when needed.
|
||||
@@ -10,7 +10,6 @@ Uncategorised thoughts, to be added
|
||||
- Investigate other similar extensions:
|
||||
- [Unotes](https://marketplace.visualstudio.com/items?itemName=ryanmcalister.Unotes)
|
||||
- [vscode-memo](https://github.com/svsool/vscode-memo)
|
||||
- [gistpad wiki](https://github.com/jevakallio/gistpad/tree/master/src/repos/wiki)
|
||||
- Open in Foam
|
||||
- When you want to open a Foam published website in your own VS Code, we could have a "Open in Foam" link that opens the link in VS Code via a url binding (if possible), downloads the github repo locally, and opens it as a Foam workspace.
|
||||
- Every Foam could have a different theme even in the editor, so you'll see it like they see it
|
||||
|
||||
@@ -60,17 +60,17 @@ These instructions assume you have a GitHub account, and you have Visual Studio
|
||||
|
||||
<a class="github-button" href="https://github.com/foambubble/foam-template/generate" data-icon="octicon-repo-template" data-size="large" aria-label="Use this template foambubble/foam-template on GitHub">Use this template</a>
|
||||
|
||||
*If you want to keep your thoughts to yourself, remember to set the repository private, or if you don't want to use GitHub to host your workspace at all, choose [**Download as ZIP**](https://github.com/foambubble/foam-template/archive/master.zip) instead of **Use this template**.*
|
||||
_If you want to keep your thoughts to yourself, remember to set the repository private, or if you don't want to use GitHub to host your workspace at all, choose [**Download as ZIP**](https://github.com/foambubble/foam-template/archive/main.zip) instead of **Use this template**._
|
||||
|
||||
2. [Clone the repository locally](https://help.github.com/en/github/creating-cloning-and-archiving-repositories/cloning-a-repository) and open it in VS Code.
|
||||
|
||||
*Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces).*
|
||||
_Open the repository as a folder using the `File > Open...` menu item. In VS Code, "open workspace" refers to [multi-root workspaces](https://code.visualstudio.com/docs/editor/multi-root-workspaces)._
|
||||
|
||||
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one)
|
||||
|
||||
After setting up the repository, open `.vscode/settings.json` and edit, add or remove any settings you'd like for your Foam workspace.
|
||||
|
||||
* *If using a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) as noted above, make sure that your **Foam** directory is first in the list. There are some settings that will need to be migrated from `.vscode/settings.json` to your `.code-workspace` file.*
|
||||
- _If using a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) as noted above, make sure that your **Foam** directory is first in the list. There are some settings that will need to be migrated from `.vscode/settings.json` to your `.code-workspace` file._
|
||||
|
||||
To learn more about how to use **Foam**, read the [[recipes]].
|
||||
|
||||
@@ -263,6 +263,15 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://www.hegghammer.com"><img src="https://avatars.githubusercontent.com/u/64712218?v=4?s=60" width="60px;" alt="Thomas Hegghammer"/><br /><sub><b>Thomas Hegghammer</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Hegghammer" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/PiotrAleksander"><img src="https://avatars.githubusercontent.com/u/6314591?v=4?s=60" width="60px;" alt="Piotr Mrzygłosz"/><br /><sub><b>Piotr Mrzygłosz</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=PiotrAleksander" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://schaver.com/"><img src="https://avatars.githubusercontent.com/u/7584?v=4?s=60" width="60px;" alt="Mark Schaver"/><br /><sub><b>Mark Schaver</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=markschaver" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/n8layman"><img src="https://avatars.githubusercontent.com/u/25353944?v=4?s=60" width="60px;" alt="Nathan Layman"/><br /><sub><b>Nathan Layman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=n8layman" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/emmanuel-ferdman"><img src="https://avatars.githubusercontent.com/u/35470921?v=4?s=60" width="60px;" alt="Emmanuel Ferdman"/><br /><sub><b>Emmanuel Ferdman</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=emmanuel-ferdman" title="Documentation">📖</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Tenormis"><img src="https://avatars.githubusercontent.com/u/61572102?v=4?s=60" width="60px;" alt="Tenormis"/><br /><sub><b>Tenormis</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Tenormis" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="http://djon.es/blog"><img src="https://avatars.githubusercontent.com/u/225052?v=4?s=60" width="60px;" alt="David Jones"/><br /><sub><b>David Jones</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=djplaner" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/s-jacob-powell"><img src="https://avatars.githubusercontent.com/u/109111499?v=4?s=60" width="60px;" alt="S. Jacob Powell"/><br /><sub><b>S. Jacob Powell</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=s-jacob-powell" title="Code">💻</a></td>
|
||||
<td align="center" valign="top" width="14.28%"><a href="https://github.com/figdavi"><img src="https://avatars.githubusercontent.com/u/99026991?v=4?s=60" width="60px;" alt="Davi Figueiredo"/><br /><sub><b>Davi Figueiredo</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=figdavi" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -38,6 +38,7 @@ A sample configuration object is provided below, you can provide as many or as l
|
||||
"foam.graph.style": {
|
||||
"background": "#202020",
|
||||
"fontSize": 12,
|
||||
"fontFamily": "Sans-Serif",
|
||||
"lineColor": "#277da1",
|
||||
"lineWidth": 0.2,
|
||||
"particleWidth": 1.0,
|
||||
@@ -50,6 +51,7 @@ A sample configuration object is provided below, you can provide as many or as l
|
||||
|
||||
- `background` background color of the graph, adjust to increase contrast
|
||||
- `fontSize` size of the title font for each node
|
||||
- `fontFamily` font of the title font for each node
|
||||
- `lineColor` color of the edges between nodes in the graph
|
||||
- `lineWidth` thickness of the edges between nodes
|
||||
- `particleWidth` size of the particle animation showing link direction when highlighting a node
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
# Link Reference Definitions
|
||||
|
||||
When you use `[[wikilinks]]`, the [foam-vscode](https://github.com/foambubble/foam/tree/master/packages/foam-vscode) extension can automatically generate [Markdown Link Reference Definitions](https://spec.commonmark.org/0.29/#link-reference-definitions) at the bottom of the file. This is not needed to navigate your workspace with foam-vscode, but is useful for files to remain compatible with various Markdown tools (e.g. parsers, static site generators, VS code plugins etc), which don't support `[[wikilinks]]`.
|
||||
When you use `[[wikilinks]]`, the [foam-vscode](https://github.com/foambubble/foam/tree/main/packages/foam-vscode) extension can automatically generate [Markdown Link Reference Definitions](https://spec.commonmark.org/0.29/#link-reference-definitions) at the bottom of the file. This is not needed to navigate your workspace with foam-vscode, but is useful for files to remain compatible with various Markdown tools (e.g. parsers, static site generators, VS code plugins etc), which don't support `[[wikilinks]]`.
|
||||
|
||||
## Example
|
||||
|
||||
The following example:
|
||||
|
||||
```md
|
||||
- [[wikilinks]]
|
||||
- [[github-pages]]
|
||||
```
|
||||
```md
|
||||
- [[wikilinks]]
|
||||
- [[github-pages]]
|
||||
```
|
||||
|
||||
...generates the following link reference definitions to the bottom of the file:
|
||||
|
||||
```md
|
||||
[wikilinks]: wikilinks "Wikilinks"
|
||||
[github-pages]: github-pages "GitHub Pages"
|
||||
```
|
||||
```md
|
||||
[wikilinks]: wikilinks 'Wikilinks'
|
||||
[github-pages]: github-pages 'GitHub Pages'
|
||||
```
|
||||
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
You can open the [raw markdown](https://foambubble.github.io/foam/user/features/link-reference-definitions.md) to see them at the bottom of this file
|
||||
|
||||
## Specification
|
||||
@@ -53,15 +52,15 @@ There are three options for excluding files from your Foam project:
|
||||
|
||||
1. `files.exclude` (from VSCode) will prevent the folder from showing in the file explorer.
|
||||
|
||||
> "Configure glob patterns for excluding files and folders. For example, the file explorer decides which files and folders to show or hide based on this setting. Refer to the Search: Exclude setting to define search-specific excludes."
|
||||
> "Configure glob patterns for excluding files and folders. For example, the file explorer decides which files and folders to show or hide based on this setting. Refer to the Search: Exclude setting to define search-specific excludes."
|
||||
|
||||
2. `files.watcherExclude` (from VSCode) prevents VSCode from constantly monitoring files for changes.
|
||||
|
||||
> "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."
|
||||
> "Configure paths or glob patterns to exclude from file watching. Paths or basic glob patterns that are relative (for example `build/output` or `*.js`) will be resolved to an absolute path using the currently opened workspace. Complex glob patterns must match on absolute paths (i.e. prefix with `**/` or the full path and suffix with `/**` to match files within a path) to match properly (for example `**/build/output/**` or `/Users/name/workspaces/project/build/output/**`). When you experience the file watcher process consuming a lot of CPU, make sure to exclude large folders that are of less interest (such as build output folders)."
|
||||
|
||||
3. `foam.files.ignore` (from Foam) ignores files from being added to the Foam graph.
|
||||
|
||||
> "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`" (requires reloading VSCode to take effect).
|
||||
> "Specifies the list of globs that will be ignored by Foam (e.g. they will not be considered when creating the graph). To ignore the all the content of a given folder, use `<folderName>/**/*`" (requires reloading VSCode to take effect).
|
||||
|
||||
For instance, if you're using a local instance of [Jekyll](https://jekyllrb.com/), you may find that it writes copies of each `.md` file into a `_site` directory, which may lead to Foam generating references to them instead of the original source notes.
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ Some properties have special meaning for Foam:
|
||||
| `title` | will assign the name to the note that you will see in the graph, regardless of the filename or the first heading (also see how to [[write-notes-in-foam]]) |
|
||||
| `type` | can be used to style notes differently in the graph (also see [[graph-visualization]]). The default type for a document is `note` unless otherwise specified with this property. |
|
||||
| `tags` | can be used to add tags to a note (see [[tags]]) |
|
||||
| `alias` | can be used to add aliases to the note. an alias will show up in the link autocompletion |
|
||||
|
||||
For example:
|
||||
|
||||
@@ -40,7 +41,7 @@ For example:
|
||||
title: "Note Title"
|
||||
type: "daily-note"
|
||||
tags: daily, funny, planning
|
||||
|
||||
alias: alias1, alias2
|
||||
---
|
||||
```
|
||||
|
||||
|
||||
@@ -2,45 +2,56 @@
|
||||
|
||||
Foam supports note templates which let you customize the starting content of your notes instead of always starting from an empty note.
|
||||
|
||||
Note templates are `.md` files located in the special `.foam/templates` directory of your workspace.
|
||||
Foam supports two types of templates:
|
||||
|
||||
- **Markdown templates** (`.md` files) - Simple templates with predefined content and variables
|
||||
- **JavaScript templates** (`.js` files) - Smart templates that can adapt based on context and make intelligent decisions
|
||||
|
||||
Both types of templates are located in the special `.foam/templates` directory of your workspace.
|
||||
|
||||
## Quickstart
|
||||
|
||||
Create a template:
|
||||
### Creating templates
|
||||
|
||||
* Run the `Foam: Create New Template` command from the command palette
|
||||
* OR manually create a regular `.md` file in the `.foam/templates` directory
|
||||
**For simple templates:**
|
||||
|
||||
- Run the `Foam: Create New Template` command from the command palette
|
||||
- OR manually create a regular `.md` file in the `.foam/templates` directory
|
||||
|
||||
**For smart templates:**
|
||||
|
||||
- Create a `.js` file in the `.foam/templates` directory (see [JavaScript Templates](#javascript-templates) section below)
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
### Using templates
|
||||
|
||||
To create a note from a template:
|
||||
|
||||
* Run the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new template if none exist.
|
||||
* OR run the `Foam: Create New Note` command, which uses the special default template (`.foam/templates/new-note.md`, if it exists)
|
||||
- Run the `Foam: Create New Note From Template` command and follow the instructions. Don't worry if you've not created a template yet! You'll be prompted to create a new simple template if none exist.
|
||||
- OR run the `Foam: Create New Note` command, which uses the special default template (`.foam/templates/new-note.md` or `.foam/templates/new-note.js`, if it exists)
|
||||
|
||||

|
||||
|
||||
_Theme: Ayu Light_
|
||||
|
||||
## Special templates
|
||||
|
||||
### Default template
|
||||
|
||||
The `.foam/templates/new-note.md` template is special in that it is the template that will be used by the `Foam: Create New Note` command.
|
||||
Customize this template to contain content that you want included every time you create a note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
|
||||
The default template is used by the `Foam: Create New Note` command. Foam will look for these templates in order:
|
||||
|
||||
```markdown
|
||||
---
|
||||
type: basic-note
|
||||
---
|
||||
```
|
||||
1. `.foam/templates/new-note.js` (JavaScript template)
|
||||
2. `.foam/templates/new-note.md` (Markdown template)
|
||||
|
||||
Customize this template to contain content that you want included every time you create a note.
|
||||
|
||||
### Default daily note template
|
||||
|
||||
The `.foam/templates/daily-note.md` template is special in that it is the template that will be used when creating daily notes (e.g. by using `Foam: Open Daily Note`).
|
||||
Customize this template to contain content that you want included every time you create a daily note. To begin it is *recommended* to define the YAML Front-Matter of the template similar to the following:
|
||||
The daily note template is used when creating daily notes (e.g. by using `Foam: Open Daily Note`). Foam will look for these templates in order:
|
||||
|
||||
1. `.foam/templates/daily-note.js` (JavaScript template)
|
||||
2. `.foam/templates/daily-note.md` (Markdown template)
|
||||
|
||||
For a simple markdown template, it is _recommended_ to define the YAML Front-Matter similar to the following:
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -48,18 +59,193 @@ type: daily-note
|
||||
---
|
||||
```
|
||||
|
||||
## Variables
|
||||
## JavaScript Templates
|
||||
|
||||
Templates can use all the variables available in [VS Code Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
|
||||
JavaScript templates are a powerful way to create smart, context-aware note templates that can adapt based on the situation. Unlike static Markdown templates, JavaScript templates can make intelligent decisions about what content to include.
|
||||
|
||||
**Use JavaScript templates when you want to:**
|
||||
|
||||
- Create different note structures based on the day of the week, time, or date
|
||||
- Adapt templates based on where the note is being created from
|
||||
- Automatically find and link related notes in your workspace
|
||||
- Generate content based on existing notes or workspace structure
|
||||
- Implement complex logic that static templates cannot handle
|
||||
|
||||
### Basic JavaScript template structure
|
||||
|
||||
A JavaScript template is a `.js` file that exports a function returning note content, and optionally location:
|
||||
|
||||
```javascript
|
||||
// .foam/templates/daily-note.js
|
||||
async function createNote({ trigger, foam, resolver, foamDate }) {
|
||||
const today = dayjs();
|
||||
// or you could use foamDate for day specific notes, see FOAM_DATE_* variables
|
||||
// const day = dayjs(foamDate)
|
||||
const formattedDay = today.format('YYYY-MM-DD');
|
||||
|
||||
// if you need a variable you can use the resolver
|
||||
// const title = await resolver.resolveFromName('FOAM_TITLE');
|
||||
|
||||
console.log(
|
||||
'Creating note for today: ' + formattedDay,
|
||||
JSON.stringify(trigger)
|
||||
);
|
||||
|
||||
let content = `# Daily Note - ${formattedDay}
|
||||
|
||||
## Today's focus
|
||||
-
|
||||
|
||||
## Notes
|
||||
-
|
||||
`;
|
||||
|
||||
switch (today.day()) {
|
||||
case 1: // Monday
|
||||
content = `# Week Planning - ${formattedDay}
|
||||
|
||||
## This week's goals
|
||||
- [ ] Goal 1
|
||||
- [ ] Goal 2
|
||||
|
||||
## Focus areas
|
||||
-
|
||||
`;
|
||||
break;
|
||||
case 5: // Friday
|
||||
content = `# Week Review - ${formattedDay}
|
||||
|
||||
## What went well
|
||||
-
|
||||
|
||||
## What could be improved
|
||||
-
|
||||
|
||||
## Next week's priorities
|
||||
-
|
||||
`;
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
content,
|
||||
filepath: `/weekly-planning/${formattedDay}.md`,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
**Smart meeting notes:**
|
||||
|
||||
```javascript
|
||||
async function createNote({ trigger, foam, resolver }) {
|
||||
const title = (await resolver.resolveFromName('FOAM_TITLE')) || 'Meeting';
|
||||
const today = dayjs();
|
||||
// Detect meeting type from title
|
||||
const isStandup = title.toLowerCase().includes('standup');
|
||||
const isReview = title.toLowerCase().includes('review');
|
||||
|
||||
let template = `# ${title} - ${today.format('YYYY-MM-DD')}
|
||||
|
||||
`;
|
||||
|
||||
if (isStandup) {
|
||||
template += `## What I did yesterday
|
||||
-
|
||||
|
||||
## What I'm doing today
|
||||
-
|
||||
|
||||
## Blockers
|
||||
-
|
||||
`;
|
||||
} else if (isReview) {
|
||||
template += `## What went well
|
||||
-
|
||||
|
||||
## What could be improved
|
||||
-
|
||||
|
||||
## Action items
|
||||
- [ ]
|
||||
`;
|
||||
} else {
|
||||
template += `## Agenda
|
||||
-
|
||||
|
||||
## Notes
|
||||
-
|
||||
|
||||
## Action items
|
||||
- [ ]
|
||||
`;
|
||||
}
|
||||
|
||||
return {
|
||||
content: template,
|
||||
filepath: `/meetings/${title}.md`,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Template result format
|
||||
|
||||
JavaScript templates must return an object with:
|
||||
|
||||
- `content` (required): The note content as a string
|
||||
- `filepath` (required): Custom file path for the note
|
||||
- NOTE: the path must be within the workspace.
|
||||
- A relative path will be resolved based on the `onRelativePath` command configuration.
|
||||
- An absolute path will be taken as is, if it falls within the workspace. Otherwise it will be considered to be from the workspace root
|
||||
|
||||
```javascript
|
||||
return {
|
||||
content: '# My Note\n\nContent here...',
|
||||
filepath: 'custom-folder/my-note.md',
|
||||
};
|
||||
```
|
||||
|
||||
### Security and limitations
|
||||
|
||||
JavaScript templates run in a best-effort secured environment:
|
||||
|
||||
- ✅ Can only run from trusted VS Code workspaces
|
||||
- ✅ Can access Foam workspace and utilities
|
||||
- ✅ Can use standard JavaScript features
|
||||
- ✅ Have a 30-second execution timeout
|
||||
- ❌ Cannot access the file system directly
|
||||
- ❌ Cannot make network requests
|
||||
- ❌ Cannot access Node.js modules
|
||||
|
||||
This increases the chances that templates stay safe while still being powerful enough for complex logic.
|
||||
|
||||
STILL - PLEASE BE AWARE YOU ARE EXECUTING CODE ON YOUR MACHINE. THIS SANDBOX IS NOT MEANT TO BE THE ULTIMATE SECURITY SOLUTION.
|
||||
|
||||
**YOU MUST TRUST THE REPO CONTRIBUTORS**
|
||||
|
||||
## Markdown templates
|
||||
|
||||
Markdown templates are a simple way to create notes
|
||||
|
||||
**Use Markdown templates when you want to:**
|
||||
|
||||
- Create simple, consistent note structures
|
||||
- Use basic variables and placeholders
|
||||
- Keep templates easy to read and modify
|
||||
|
||||
### Variables
|
||||
|
||||
Markdown templates can use all the variables available in [VS Code Snippets](https://code.visualstudio.com/docs/editor/userdefinedsnippets#_variables).
|
||||
|
||||
In addition, you can also use variables provided by Foam:
|
||||
|
||||
| Name | Description |
|
||||
| -------------------- | ------------ |
|
||||
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new |
|
||||
| `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. |
|
||||
| Name | Description |
|
||||
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `FOAM_SELECTED_TEXT` | Foam will fill it with selected text when creating a new note, if any text is selected. Selected text will be replaced with a wikilink to the new |
|
||||
| `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_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
|
||||
@@ -70,7 +256,7 @@ For example, `FOAM_DATE_YEAR` has the same behaviour as VS Code's `CURRENT_YEAR`
|
||||
|
||||
By default, prefer using the `FOAM_DATE_` versions. The datetime used to compute the values will be the same for both `FOAM_DATE_` and VS Code's variables, with the exception of the creation notes using the daily note template.
|
||||
|
||||
For more nitty-gritty details about the supported date formats, [see here](https://github.com/foambubble/foam/blob/master/packages/foam-vscode/src/services/variable-resolver.ts).
|
||||
For more nitty-gritty details about the supported date formats, [see here](https://github.com/foambubble/foam/blob/main/packages/foam-vscode/src/services/variable-resolver.ts).
|
||||
|
||||
#### Relative daily notes
|
||||
|
||||
@@ -84,8 +270,8 @@ For example, given this daily note template (`.foam/templates/daily-note.md`):
|
||||
|
||||
## Here's what I'm going to do today
|
||||
|
||||
* Thing 1
|
||||
* Thing 2
|
||||
- Thing 1
|
||||
- Thing 2
|
||||
```
|
||||
|
||||
When the `/tomorrow` snippet is used, `FOAM_DATE_` variables will be populated with tomorrow's date, as expected.
|
||||
@@ -93,52 +279,19 @@ If instead you were to use the VS Code versions of these variables, they would b
|
||||
|
||||
When creating notes in any other scenario, the `FOAM_DATE_` values are computed using the same datetime as the VS Code ones, so the `FOAM_DATE_` versions can be used in all scenarios by default.
|
||||
|
||||
## Metadata
|
||||
### Metadata
|
||||
|
||||
Templates can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
|
||||
**Markdown templates** can also contain metadata about the templates themselves. The metadata is defined in YAML "Frontmatter" blocks within the templates.
|
||||
|
||||
| Name | Description |
|
||||
| ------------- | ---------------------- |
|
||||
| Name | Description |
|
||||
| ------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `filepath` | The filepath to use when creating the new note. If the filepath is a relative filepath, it is relative to the current workspace. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
| `name` | A human readable name to show in the template picker. |
|
||||
| `description` | A human readable description to show in the template picker. |
|
||||
|
||||
Foam-specific variables (e.g. `$FOAM_TITLE`) can be used within template metadata. However, VS Code snippet variables are ([currently](https://github.com/foambubble/foam/pull/655)) not supported.
|
||||
|
||||
### `filepath` attribute
|
||||
|
||||
The `filepath` metadata attribute allows you to define a relative or absolute filepath to use when creating a note using the template. If the filepath is a relative filepath, it is relative to the current workspace.
|
||||
|
||||
#### Example of **relative** `filepath`
|
||||
|
||||
For example, `filepath` can be used to customize `.foam/templates/new-note.md`, overriding the default `Foam: Create New Note` behaviour of opening the file in the same directory as the active file:
|
||||
|
||||
```yaml
|
||||
---
|
||||
# This will create the note in the "journal" subdirectory of the current workspace,
|
||||
# regardless of which file is the active file.
|
||||
foam_template:
|
||||
filepath: 'journal/$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of **absolute** `filepath`
|
||||
|
||||
`filepath` can be an absolute filepath, so that the notes get created in the same location, regardless of which file or workspace the editor currently has open.
|
||||
The format of an absolute filepath may vary depending on the filesystem used.
|
||||
|
||||
```yaml
|
||||
---
|
||||
foam_template:
|
||||
# Unix / MacOS filesystems
|
||||
filepath: '/Users/john.smith/foam/journal/$FOAM_TITLE.md'
|
||||
|
||||
# Windows filesystems
|
||||
filepath: 'C:\Users\john.smith\Documents\foam\journal\$FOAM_TITLE.md'
|
||||
---
|
||||
```
|
||||
|
||||
#### Example of **date-based** `filepath`
|
||||
#### `filepath` attribute
|
||||
|
||||
It is possible to vary the `filepath` value based on the current date using the `FOAM_DATE_*` variables. This is especially useful for the [[daily-notes]] template if you wish to organize by years, months, etc. Below is an example of a daily-note template metadata section that will create new daily notes under the `journal/YEAR/MONTH-MONTH_NAME/` filepath. For example, when a note is created on November 15, 2022, a new file will be created at `C:\Users\foam_user\foam_notes\journal\2022\11-Nov\2022-11-15-daily-note.md`. This method also respects the creation of daily notes relative to the current date (i.e. `/+1d`).
|
||||
|
||||
@@ -146,27 +299,24 @@ It is possible to vary the `filepath` value based on the current date using the
|
||||
---
|
||||
type: daily-note
|
||||
foam_template:
|
||||
description: Daily Note for $FOAM_TITLE
|
||||
filepath: "C:\\Users\\foam_user\\foam_notes\\journal\\$FOAM_DATE_YEAR\\$FOAM_DATE_MONTH-$FOAM_DATE_MONTH_NAME_SHORT\\$FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE-daily-note.md"
|
||||
description: Daily Note
|
||||
filepath: '/journal/$FOAM_DATE_YEAR/$FOAM_DATE_MONTH-$FOAM_DATE_MONTH_NAME_SHORT/$FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE-daily-note.md'
|
||||
---
|
||||
|
||||
# $FOAM_DATE_YEAR-$FOAM_DATE_MONTH-$FOAM_DATE_DATE Daily Notes
|
||||
```
|
||||
|
||||
> Note: this method **requires** the use of absolute file paths, and in this example is using Windows path conventions. This method will also override any filename formatting defined in `.vscode/settings.json`
|
||||
|
||||
### `name` and `description` attributes
|
||||
#### `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):
|
||||
|
||||

|
||||
|
||||
### Adding template metadata to an existing YAML Frontmatter block
|
||||
#### Adding template metadata to an existing YAML Frontmatter block
|
||||
|
||||
If your template already has a YAML Frontmatter block, you can add the Foam template metadata to it.
|
||||
|
||||
#### Limitations
|
||||
|
||||
Foam only supports adding the template metadata to *YAML* Frontmatter blocks. If the existing Frontmatter block uses some other format (e.g. JSON), you will have to add the template metadata to its own YAML Frontmatter block.
|
||||
Foam only supports adding the template metadata to _YAML_ Frontmatter blocks. If the existing Frontmatter block uses some other format (e.g. JSON), you will have to add the template metadata to its own YAML Frontmatter block.
|
||||
|
||||
Further, the template metadata must be provided as a [YAML block mapping](https://yaml.org/spec/1.2/spec.html#id2798057), with the attributes placed on the lines immediately following the `foam_template` line:
|
||||
|
||||
@@ -181,11 +331,7 @@ foam_template: # this is a YAML "Block" mapping ("Flow" mappings aren't supporte
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
Due to the technical limitations of parsing the complex YAML format, unless the metadata is provided this specific form, Foam is unable to correctly remove the template metadata before creating the resulting note.
|
||||
|
||||
If this limitation proves inconvenient to you, please let us know. We may be able to extend our parsing capabilities to cover your use case. In the meantime, you can add the template metadata without this limitation by providing it in its own YAML Frontmatter block.
|
||||
|
||||
### Adding template metadata to its own YAML Frontmatter block
|
||||
#### Adding template metadata to its own YAML Frontmatter block
|
||||
|
||||
You can add the template metadata to its own YAML Frontmatter block at the start of the template:
|
||||
|
||||
@@ -210,7 +356,7 @@ foam_template:
|
||||
---
|
||||
|
||||
---
|
||||
existing_frontmatter: "Existing Frontmatter block"
|
||||
existing_frontmatter: 'Existing Frontmatter block'
|
||||
---
|
||||
This is the rest of the template
|
||||
```
|
||||
|
||||
@@ -36,31 +36,31 @@ _Note that first entry in `.order` file defines wiki's home page._
|
||||
|
||||
While you are pushing changes to GitHub, you won't see the wiki updated if you don't add Azure as a remote. You can push to multiple repositories simultaneously.
|
||||
|
||||
1. First open a terminal and check if Azure is added running: `git remote show origin`. If you don't see Azure add it in the output then follow these steps.
|
||||
2. Rename your current remote (most likely named origin) to a different name by running: `git remote rename origin main`
|
||||
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
|
||||
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
|
||||
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
|
||||
1. First open a terminal and check if Azure is added running: `git remote show origin`. If you don't see Azure add it in the output then follow these steps.
|
||||
2. Rename your current remote (most likely named origin) to a different name by running: `git remote rename origin main`
|
||||
3. You can then add the remote for your second remote repository, in this case, Azure. e.g `git remote add azure https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes`. You can get it from: Repos->Files->Clone and copy the URL.
|
||||
4. Now, you need to set up your origin remote to push to both of these. So run: `git config -e` and edit it.
|
||||
5. Add the `remote origin` section to the bottom of the file with the URLs from each remote repository you'd like to push to. You'll see something like that:
|
||||
|
||||
```bash
|
||||
[core]
|
||||
```bash
|
||||
[core]
|
||||
...
|
||||
(ignore this part)
|
||||
...
|
||||
(ignore this part)
|
||||
...
|
||||
[branch "master"]
|
||||
remote = github
|
||||
merge = refs/heads/master
|
||||
[branch "main"]
|
||||
remote = github
|
||||
merge = refs/heads/main
|
||||
[remote "github"]
|
||||
url = git@github.com:username/repo.git
|
||||
fetch = +refs/heads/*:refs/remotes/github/*
|
||||
url = git@github.com:username/repo.git
|
||||
fetch = +refs/heads/*:refs/remotes/github/*
|
||||
[remote "azure"]
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
fetch = +refs/heads/*:refs/remotes/azure/*
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
fetch = +refs/heads/*:refs/remotes/azure/*
|
||||
[remote "origin"]
|
||||
url = git@github.com:username/repo.git
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
```
|
||||
url = git@github.com:username/repo.git
|
||||
url = https://<YOUR_ID>@dev.azure.com/<YOUR_ID>/foam-notes/_git/foam-notes
|
||||
```
|
||||
|
||||
6. You can then push to both repositories by: `git push origin master` or a single one using: `git push github master` or `git push azure master`
|
||||
6. You can then push to both repositories by: `git push origin main` or a single one using: `git push github main` or `git push azure main`
|
||||
|
||||
For more information, read the [Azure DevOps documentation](https://docs.microsoft.com/en-us/azure/devops/project/wiki/publish-repo-to-wiki).
|
||||
|
||||
173
docs/user/recipes/generate-material-for-mkdocs-site.md
Normal file
173
docs/user/recipes/generate-material-for-mkdocs-site.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Generate a site using the Material for MkDocs theme
|
||||
|
||||
Configuring a static-site generator (SSG) to publish your Foam provides access to functionality not available through Foam's default publishing mechanism. For example, compare the [original Foam documentation site](https://foambubble.github.io/foam/) with a [Material for MkDocs version](https://djplaner.github.io/foam-with-material-for-mkdocs/) created using the simple configuration detailed below. Try out the search functionality on the Material for MkDocs version. This [digital garden](https://djon.es/memex) and this [blog](https://djon.es/blog/) provide more advanced examples of Foam content published using Material for MkDocs.
|
||||
|
||||
The following explains how to configure the [Material for MkDocs theme](https://squidfunk.github.io/mkdocs-material/) for the [MkDocs SSG](https://www.mkdocs.org) to publish your Foam.
|
||||
|
||||
Like most SSGs (e.g. [Gatsby](https://www.gatsbyjs.com/) is another SSG that can be [used to publish your Foam](https://foambubble.github.io/foam/user/publishing/generate-gatsby-site)) site content is accepted in the form of Markdown files. Like those produced by Foam. SSGs differ in the languages they are written in (MkDocs is Python, Gatsby is Javascript and React) and the features they provide. MkDocs and Material for MkDocs are designed to support project documentation. Gatsby is more general purpose and provides a nice feature set.
|
||||
|
||||
You choose your poison.
|
||||
|
||||
## Requirements
|
||||
|
||||
To use Material for MkDocs to publish your Foam you need:
|
||||
|
||||
- An existing Foam workspace with content.
|
||||
- [Python installed on your computer](https://realpython.com/installing-python/).
|
||||
- Some familiarity and comfort with using the command line on your computer.
|
||||
|
||||
## Instructions
|
||||
|
||||
Configuring Material for MkDocs to publish your Foam involves the following steps:
|
||||
|
||||
1. [Install Material for MkDocs and other requirements](#install-material-for-mkdocs-and-other-requirements).
|
||||
|
||||
Install the Material for MkDocs theme, MkDocs, and other required Python modules.
|
||||
|
||||
2. [Configure Material for MkDocs for your Foam](#configure-material-for-mkdocs-for-your-foam).
|
||||
|
||||
Create a `mkdocs.yml` file in the root of your Foam workspace directory. This file configures Material for MkDocs to work with your Foam.
|
||||
|
||||
2. [Preview and test your site locally](#preview-and-test-your-site-locally).
|
||||
|
||||
Run MkDocs to preview and test your Material for MkDocs Foam site locally. Good for testing and local use.
|
||||
|
||||
3. [Further customise Material for MkDocs](#further-customise-material-for-mkdocs).
|
||||
|
||||
Explore and leverage the additional configuration settings, possible customisations, and additional themes and plugins to customise your site to your needs.
|
||||
|
||||
4. [Publish your site](#publish-your-site).
|
||||
|
||||
Publish your Material for MkDocs Foam site to the web for others to enjoy. There are many options for publishing your site, including GitHub, GitLab, Netlify, and others.
|
||||
|
||||
### Install Material for MkDocs and other requirements
|
||||
|
||||
Material for MkDocs provides [detailed installation instructions](https://squidfunk.github.io/mkdocs-material/getting-started/) which cover the full range of options for installing and configuring Material for MkDocs. The following is a summary of the recommended process.
|
||||
|
||||
1. Within your Foam workspace directory, create a [Python virtual environment](https://realpython.com/what-is-pip/#using-pip-in-a-python-virtual-environment)
|
||||
|
||||
- `python -m venv .venv`
|
||||
- `source .venv/bin/activate` (Linux/Mac) or `.venv\Scripts\activate` (Windows)
|
||||
|
||||
2. Install Material for MkDocs
|
||||
|
||||
- `pip install mkdocs-material`
|
||||
|
||||
3. Install additional Python modules
|
||||
|
||||
- `pip install mkdocs-roamlinks-plugin`
|
||||
- `pip install mkdocs-exclude`
|
||||
|
||||
### Configure Material for MkDocs for your Foam
|
||||
|
||||
To configure Material for MkDocs for your Foam workspace, create a `mkdocs.yml` file in the root of your Foam workspace directory. Below you will find a sample `mkdocs.yml` file (adapted from the [foam-mkdocs-template repository](https://github.com/Jackiexiao/foam-mkdocs-template/tree/master)). Copy and paste it into your `mkdocs.yml` file, then edit it to suit your needs. In particular, don't forget to change the `site_name` and `site_url` to match your Foam workspace. Though this can be left a little later.
|
||||
|
||||
Material for MkDocs provides documentation on both [minimal](https://squidfunk.github.io/mkdocs-material/creating-your-site/#minimal-configuration) and [advanced](https://squidfunk.github.io/mkdocs-material/creating-your-site/#advanced-configuration) configuration of `mkdocs.yml`. Which are revisited in the [customise section below](#further-customise-your-site)
|
||||
|
||||
```yaml
|
||||
site_name: My site # Change this to your site name
|
||||
site_url: https://mydomain.org/mysite # change this
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.expand
|
||||
- tabs
|
||||
markdown_extensions:
|
||||
- attr_list
|
||||
- pymdownx.tabbed
|
||||
- nl2br
|
||||
- toc:
|
||||
permalink: '#'
|
||||
slugify: !!python/name:pymdownx.slugs.uslugify
|
||||
- admonition
|
||||
- codehilite:
|
||||
guess_lang: false
|
||||
linenums: false
|
||||
- footnotes
|
||||
- meta
|
||||
- def_list
|
||||
- pymdownx.arithmatex
|
||||
- pymdownx.betterem:
|
||||
smart_enable: all
|
||||
- pymdownx.caret
|
||||
- pymdownx.critic
|
||||
- pymdownx.details
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist
|
||||
- pymdownx.tilde
|
||||
plugins:
|
||||
- search
|
||||
- roamlinks
|
||||
- exclude:
|
||||
glob:
|
||||
- "*.tmp"
|
||||
- "*.pdf"
|
||||
- "*.gz"
|
||||
regex:
|
||||
- '.*\.(tmp|bin|tar)$'
|
||||
```
|
||||
|
||||
### Preview and test your site locally
|
||||
|
||||
MkDocs provides a live preview server allowing you to preview and test your Material for MkDocs Foam site. The server will continue to rebuid your site as you write.
|
||||
|
||||
The simplest method to use the preview service is to run the following command whilst in the rood directory of your Foam workspace:
|
||||
|
||||
```bash
|
||||
mkdocs serve
|
||||
```
|
||||
|
||||
See the Material for MkDocs site for more, including [how to run the preview server via docker](https://squidfunk.github.io/mkdocs-material/creating-your-site/#previewing-as-you-write)
|
||||
|
||||
### Further customise your site
|
||||
|
||||
Further customisation is available through expanding the configuration of Material for MkDocs, using additional MkDocs plugins, customising HTML/CSS, using Markdown extensions, writing your own Python scripts, and more.
|
||||
|
||||
For more on the available customisation paths, see the following:
|
||||
|
||||
- Material for MkDocs [Advanced configuration](https://squidfunk.github.io/mkdocs-material/creating-your-site/#advanced-configuration) or the [Set up section](https://squidfunk.github.io/mkdocs-material/setup/)
|
||||
|
||||
For more configuration options to be included in your `mkdocs.yml` file, including customising: colours, fonts, language, icons, navigation, header, footer etc.
|
||||
|
||||
- Material for MkDocs [Customisation](https://squidfunk.github.io/mkdocs-material/customization/)
|
||||
|
||||
For advice on enhancing the visual design of your site by customising and replacing provided HTML, CSS, and Javascript.
|
||||
|
||||
- Material for MkDocs [Reference](https://squidfunk.github.io/mkdocs-material/reference/)
|
||||
|
||||
An overview of customisation methods that can be used directly within your Markdown files, including: admonitions, annotations, buttons, code blocks, content tabs, data tables, diagrams, grids, Mathematics, etc.
|
||||
|
||||
- a [catalog of 300 MkDocs projects and plugins](https://github.com/mkdocs/catalog#readme)
|
||||
|
||||
For functionality and ideas not included in Material for MkDocs, including: additional themes, plugins, and extensions.
|
||||
|
||||
### Building and publishing your site
|
||||
|
||||
As a Static Site Generator (SSG), MkDocs generates a collection of static HTML and other types of files. Publishing your site involves building those HTML files and placing them onto your web server. The method will vary depending on your web server and hosting provider.
|
||||
|
||||
The MkDocs documentation site provides an explanation of the [simplest method to publish your site to any provider](https://www.mkdocs.org/user-guide/deploying-your-docs/#other-providers) using `mkdocs build` and `scp`.
|
||||
|
||||
The Material for MkDocs [publish page](https://squidfunk.github.io/mkdocs-material/publishing-your-site/) lists options for publishing to
|
||||
|
||||
- GitHub using [mkdocs](https://squidfunk.github.io/mkdocs-material/publishing-your-site/#with-mkdocs)
|
||||
|
||||
Perhaps the simplest method, if you are already using GitHub to host your Foam workspace.
|
||||
|
||||
- GitHub using [GitHub actions](https://squidfunk.github.io/mkdocs-material/publishing-your-site/github-actions/)
|
||||
|
||||
A more automated method of publishing your site to GitHub, using GitHub actions.
|
||||
|
||||
- [GitLab](https://squidfunk.github.io/mkdocs-material/publishing-your-site/#with-mkdocs)
|
||||
|
||||
- [Cloudflage pages](https://deborahwrites.com/guides/deploy-host-mkdocs/deploy-mkdocs-material-cloudflare/)
|
||||
|
||||
- [Netlify](https://deborahwrites.com/guides/deploy-host-mkdocs/deploy-mkdocs-material-netlify/)
|
||||
|
||||
- [Fly.io](https://documentation.breadnet.co.uk/cloud/fly/mkdocs-on-fly/#prerequisites)
|
||||
|
||||
- [Scaleway](https://www.scaleway.com/en/docs/tutorials/using-bucket-website-with-mkdocs/)
|
||||
|
||||
@@ -10,7 +10,7 @@ To install search note-macros in vscode or head to [note-macros - Visual Studio
|
||||
|
||||
## Instructions
|
||||
|
||||
### Run macro From command pallette
|
||||
### Run macro From command palette
|
||||
|
||||
Simply use `Ctrl+P` or `Alt+P` depend on your os, and type `Note Macros: Run A Macro` then chose the macro you want to execute.
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
- Publish using community templates
|
||||
- [[publish-to-netlify-with-eleventy]] by [@juanfrank77](https://github.com/juanfrank77)
|
||||
- [[generate-gatsby-site]] by [@mathieudutour](https://github.com/mathieudutour) and [@hikerpig](https://github.com/hikerpig)
|
||||
- [[generate-material-for-mkdocs-site]] by [@djplaner](https://github.com/djplaner)
|
||||
- Make the site your own by [[publish-to-github]].
|
||||
- Render math symbols, by either
|
||||
- adding client-side [[math-support-with-mathjax]] to the default [[publish-to-github-pages]] site
|
||||
@@ -139,6 +140,7 @@ _See [[contribution-guide]] and [[how-to-write-recipes]]._
|
||||
[publish-to-vercel]: ../publishing/publish-to-vercel.md "Publish to Vercel"
|
||||
[publish-to-netlify-with-eleventy]: ../publishing/publish-to-netlify-with-eleventy.md "Publish to Netlify with Eleventy"
|
||||
[generate-gatsby-site]: ../publishing/generate-gatsby-site.md "Generate a site using Gatsby"
|
||||
[generate-material-for-mkdocs-site]: generate-material-for-mkdocs-site.md "Generate a site using the Material for MkDocs theme"
|
||||
[publish-to-github]: ../publishing/publish-to-github.md "Publish to GitHub"
|
||||
[math-support-with-mathjax]: ../publishing/math-support-with-mathjax.md "Math Support"
|
||||
[math-support-with-katex]: ../publishing/math-support-with-katex.md "Katex Math Rendering"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.26.1"
|
||||
"version": "0.27.3"
|
||||
}
|
||||
|
||||
31802
package-lock.json
generated
31802
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,9 +16,9 @@
|
||||
"reset": "yarn && yarn clean && yarn build",
|
||||
"clean": "lerna run clean",
|
||||
"build": "lerna run build",
|
||||
"test": "yarn workspace foam-vscode test --stream",
|
||||
"test": "yarn workspace foam-vscode test",
|
||||
"lint": "lerna run lint",
|
||||
"watch": "lerna run watch --concurrency 20 --stream"
|
||||
"watch": "lerna run watch --concurrency 20"
|
||||
},
|
||||
"devDependencies": {
|
||||
"all-contributors-cli": "^6.16.1",
|
||||
|
||||
@@ -4,6 +4,108 @@ All notable changes to the "foam-vscode" extension will be documented in this fi
|
||||
|
||||
Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file.
|
||||
|
||||
## [0.27.2] - 2025-09-05
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved timezone handling for create-note when passing string date
|
||||
- Added debugging for daily note issue (#1505, #1502, #1494)
|
||||
- Deprecated daily note settings (use daily-note template instead)
|
||||
|
||||
## [0.27.1] - 2025-07-24
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fixed handling of daily note template on Windows machines (#1492)
|
||||
|
||||
## [0.27.0] - 2025-07-23
|
||||
|
||||
Features:
|
||||
|
||||
- Introduced a unified note creation engine supporting both Markdown and JavaScript templates
|
||||
|
||||
Internal:
|
||||
|
||||
- Improved testing framework by creating a mocked VS Code environment
|
||||
|
||||
## [0.26.12] - 2025-06-18
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Fix YAML parsing (#1467)
|
||||
- Improved regex parsing (#1479 - thanks @s-jacob-powell)
|
||||
|
||||
## [0.26.11] - 2025-04-19
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Support for custom fonts in graph view (#1457 - thanks @Tenormis)
|
||||
|
||||
## [0.26.10] - 2025-03-29
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- General improvment of wiki embeds (#1443)
|
||||
|
||||
## [0.26.9] - 2025-03-29
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Defensive get of link object ID in graph (#1438)
|
||||
|
||||
Internal:
|
||||
|
||||
- Updated `force-graph` library
|
||||
|
||||
## [0.26.8] - 2025-03-14
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Tag hierarchy now visible in graph (#1436)
|
||||
- Improved Notes Explorer layout
|
||||
|
||||
## [0.26.7] - 2025-03-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved parsing of tags (fixes #1434)
|
||||
|
||||
## [0.26.6] - 2025-03-08
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved graph based navigation when running in virtual workspace
|
||||
- Improved wikilink embeds and fixed cycle detection issue (#1430)
|
||||
- Added links in tags to navigate to corresponding tag explorer item (#1432)
|
||||
|
||||
Internal:
|
||||
|
||||
- Renamed branch from `master` to `main`
|
||||
|
||||
## [0.26.5] - 2025-02-21
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved handling of virtual FS URIs (#1426)
|
||||
|
||||
## [0.26.4] - 2024-11-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Improved handling of virtual FS URIs (#1409)
|
||||
|
||||
## [0.26.3] - 2024-11-12
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Finetuned use of triemap (#1411 - thanks @pderaaij)
|
||||
|
||||
## [0.26.2] - 2024-11-06
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
- Performance improvements (#1406 - thanks @pderaaij)
|
||||
|
||||
## [0.26.1] - 2024-10-09
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -123,7 +123,7 @@ module.exports = {
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
// setupFiles: [],
|
||||
setupFiles: ['<rootDir>/src/test/support/jest-setup.ts'],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
@@ -153,9 +153,8 @@ module.exports = {
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// This is overridden in every runCLI invocation but it's here as the default
|
||||
// for vscode-jest. We only want unit tests in the test explorer (sidebar),
|
||||
// since spec tests require the entire extension host to be launched before.
|
||||
testRegex: ['\\.test\\.ts$'],
|
||||
// for vscode-jest. Both .test.ts and .spec.ts files use the vscode-mock.
|
||||
testRegex: ['\\.(test|spec)\\.ts$'],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
@@ -8,19 +8,16 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.26.1",
|
||||
"version": "0.27.3",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
"vscode": "^1.70.0"
|
||||
"vscode": "^1.96.0"
|
||||
},
|
||||
"icon": "assets/icon/FOAM_ICON_256.png",
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [
|
||||
"workspaceContains:.vscode/foam.json"
|
||||
],
|
||||
"main": "./out/bundles/extension-node.js",
|
||||
"browser": "./out/bundles/extension-web.js",
|
||||
"capabilities": {
|
||||
@@ -301,19 +298,19 @@
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.update-graph",
|
||||
"title": "Foam: Update graph"
|
||||
"title": "Foam: Update Graph"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.set-log-level",
|
||||
"title": "Foam: Set log level"
|
||||
"title": "Foam: Set Log Level"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.show-graph",
|
||||
"title": "Foam: Show graph"
|
||||
"title": "Foam: Show Graph"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.update-wikilink-definitions",
|
||||
"title": "Foam: Update wikilink definitions"
|
||||
"title": "Foam: Update Wikilink Definitions"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
@@ -349,11 +346,11 @@
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-inplace",
|
||||
"title": "Foam: convert link style in place"
|
||||
"title": "Foam: Convert Link Style in Place"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.convert-link-style-incopy",
|
||||
"title": "Foam: convert link format in copy"
|
||||
"title": "Foam: Convert Link Format in Copy"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.orphans.group-by:folder",
|
||||
@@ -405,6 +402,11 @@
|
||||
"title": "Expand all",
|
||||
"icon": "$(expand-all)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.tags-explorer.focus",
|
||||
"title": "Focus on tag",
|
||||
"icon": "$(symbol-number)"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.views.placeholders.show:for-current-file",
|
||||
"title": "Show placeholders in current file",
|
||||
@@ -457,6 +459,13 @@
|
||||
"configuration": {
|
||||
"title": "Foam",
|
||||
"properties": {
|
||||
"foam.supportedLanguages": {
|
||||
"type": "array",
|
||||
"default": [
|
||||
"markdown"
|
||||
],
|
||||
"description": "List of languages to treat as Markdown-like documents."
|
||||
},
|
||||
"foam.completion.label": {
|
||||
"type": "string",
|
||||
"default": "path",
|
||||
@@ -565,15 +574,18 @@
|
||||
"default": false
|
||||
},
|
||||
"foam.openDailyNote.fileExtension": {
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in future versions. Please use the daily-note template: https://github.com/foambubble/foam/blob/fe0228bdcc647e85682670656b5f09203b1c2518/docs/user/features/daily-notes.md",
|
||||
"type": "string",
|
||||
"default": "md"
|
||||
},
|
||||
"foam.openDailyNote.filenameFormat": {
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in future versions. Please use the daily-note template: https://github.com/foambubble/foam/blob/fe0228bdcc647e85682670656b5f09203b1c2518/docs/user/features/daily-notes.md",
|
||||
"type": "string",
|
||||
"default": "isoDate",
|
||||
"markdownDescription": "Specifies how the daily note filename is formatted. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats"
|
||||
},
|
||||
"foam.openDailyNote.titleFormat": {
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in future versions. Please use the daily-note template: https://github.com/foambubble/foam/blob/fe0228bdcc647e85682670656b5f09203b1c2518/docs/user/features/daily-notes.md",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -582,6 +594,7 @@
|
||||
"markdownDescription": "Specifies how the daily note title is formatted. Will default to the filename format if set to null. See the [dateformat docs](https://www.npmjs.com/package/dateformat) for valid formats"
|
||||
},
|
||||
"foam.openDailyNote.directory": {
|
||||
"deprecationMessage": "This setting is deprecated and will be removed in future versions. Please use the daily-note template: https://github.com/foambubble/foam/blob/fe0228bdcc647e85682670656b5f09203b1c2518/docs/user/features/daily-notes.md",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
@@ -666,13 +679,14 @@
|
||||
"test-reset-workspace": "rm -rf .test-workspace && mkdir .test-workspace && touch .test-workspace/.keep",
|
||||
"test-setup": "yarn compile && yarn build && yarn test-reset-workspace",
|
||||
"test": "yarn test-setup && node ./out/test/run-tests.js",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:unit": "yarn test-setup && node ./out/test/run-tests.js --unit --exclude-specs",
|
||||
"test:unit-with-specs": "yarn test-setup && node ./out/test/run-tests.js --unit",
|
||||
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
|
||||
"lint": "dts lint src",
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"watch": "nodemon --watch 'src/**/*.ts' --exec 'yarn build' --ext ts",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
"package-extension": "npx vsce package --yarn",
|
||||
"package-extension": "npx @vscode/vsce@3.6.0 package --yarn",
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"open-in-browser": "vscode-test-web --quality=stable --browser=chromium --extensionDevelopmentPath=. ",
|
||||
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
|
||||
@@ -685,7 +699,7 @@
|
||||
"@types/lodash": "^4.14.157",
|
||||
"@types/markdown-it": "^12.0.1",
|
||||
"@types/micromatch": "^4.0.1",
|
||||
"@types/node": "^13.11.0",
|
||||
"@types/node": "^18.0.0",
|
||||
"@types/picomatch": "^2.2.1",
|
||||
"@types/remove-markdown": "^0.1.1",
|
||||
"@types/vscode": "^1.70.0",
|
||||
@@ -704,6 +718,7 @@
|
||||
"jest-extended": "^3.2.3",
|
||||
"markdown-it": "^12.0.4",
|
||||
"micromatch": "^4.0.2",
|
||||
"nodemon": "^3.1.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^29.1.1",
|
||||
"tslib": "^2.0.0",
|
||||
@@ -713,6 +728,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"dateformat": "4.5.1",
|
||||
"dayjs": "^1.11.13",
|
||||
"detect-newline": "^3.1.0",
|
||||
"github-slugger": "^1.4.0",
|
||||
"gray-matter": "^4.0.2",
|
||||
@@ -720,6 +736,7 @@
|
||||
"lodash": "^4.17.21",
|
||||
"lru-cache": "^7.14.1",
|
||||
"markdown-it-regex": "^0.2.0",
|
||||
"mnemonist": "^0.39.8",
|
||||
"path-browserify": "^1.0.1",
|
||||
"remark-frontmatter": "^2.0.0",
|
||||
"remark-parse": "^8.0.2",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
import { Emitter, Event } from './event';
|
||||
import { IDisposable } from './lifecycle';
|
||||
@@ -29,7 +29,7 @@ export interface CancellationToken {
|
||||
) => IDisposable;
|
||||
}
|
||||
|
||||
const shortcutEvent: Event<any> = Object.freeze(function(
|
||||
const shortcutEvent: Event<any> = Object.freeze(function (
|
||||
callback,
|
||||
context?
|
||||
): IDisposable {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
// Names from https://blog.codinghorror.com/ascii-pronunciation-rules-for-programmers/
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
export interface ErrorListenerCallback {
|
||||
(error: any): void;
|
||||
@@ -21,7 +21,7 @@ export class ErrorHandler {
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
|
||||
this.unexpectedErrorHandler = function(e: any) {
|
||||
this.unexpectedErrorHandler = function (e: any) {
|
||||
setTimeout(() => {
|
||||
if (e.stack) {
|
||||
throw new Error(e.message + '\n\n' + e.stack);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
import { onUnexpectedError } from './errors';
|
||||
import { once as onceFn } from './functional';
|
||||
@@ -115,7 +115,7 @@ export namespace Event {
|
||||
* Given an event, returns the same event but typed as `Event<void>`.
|
||||
*/
|
||||
export function signal<T>(event: Event<T>): Event<void> {
|
||||
return (event as Event<any>) as Event<void>;
|
||||
return event as Event<any> as Event<void>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -525,9 +525,7 @@ class LeakageMonitor {
|
||||
|
||||
constructor(
|
||||
readonly customThreshold?: number,
|
||||
readonly name: string = Math.random()
|
||||
.toString(18)
|
||||
.slice(2, 5)
|
||||
readonly name: string = Math.random().toString(18).slice(2, 5)
|
||||
) {}
|
||||
|
||||
dispose(): void {
|
||||
@@ -549,10 +547,7 @@ class LeakageMonitor {
|
||||
if (!this._stacks) {
|
||||
this._stacks = new Map();
|
||||
}
|
||||
const stack = new Error()
|
||||
.stack!.split('\n')
|
||||
.slice(3)
|
||||
.join('\n');
|
||||
const stack = new Error().stack!.split('\n').slice(3).join('\n');
|
||||
const count = this._stacks.get(stack) || 0;
|
||||
this._stacks.set(stack, count + 1);
|
||||
this._warnCountdown -= 1;
|
||||
@@ -607,7 +602,7 @@ class LeakageMonitor {
|
||||
}
|
||||
*/
|
||||
export class Emitter<T> {
|
||||
private static readonly _noop = function() {};
|
||||
private static readonly _noop = function () {};
|
||||
|
||||
private readonly _options?: EmitterOptions;
|
||||
private readonly _leakageMon?: LeakageMonitor;
|
||||
|
||||
@@ -3,14 +3,14 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
export function once<T extends Function>(this: unknown, fn: T): T {
|
||||
const _this = this;
|
||||
let didCall = false;
|
||||
let result: unknown;
|
||||
|
||||
return (function() {
|
||||
return function () {
|
||||
if (didCall) {
|
||||
return result;
|
||||
}
|
||||
@@ -19,5 +19,5 @@ export function once<T extends Function>(this: unknown, fn: T): T {
|
||||
result = fn.apply(_this, arguments);
|
||||
|
||||
return result;
|
||||
} as unknown) as T;
|
||||
} as unknown as T;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
export namespace Iterable {
|
||||
export function is<T = any>(thing: any): thing is IterableIterator<T> {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
import { once } from './functional';
|
||||
import { Iterable } from './iterator';
|
||||
@@ -164,7 +164,7 @@ export class DisposableStore implements IDisposable {
|
||||
if (!t) {
|
||||
return t;
|
||||
}
|
||||
if (((t as unknown) as DisposableStore) === this) {
|
||||
if ((t as unknown as DisposableStore) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ export abstract class Disposable implements IDisposable {
|
||||
}
|
||||
|
||||
protected _register<T extends IDisposable>(t: T): T {
|
||||
if (((t as unknown) as Disposable) === this) {
|
||||
if ((t as unknown as Disposable) === this) {
|
||||
throw new Error('Cannot register a disposable on itself!');
|
||||
}
|
||||
return this._store.add(t);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
class Node<E> {
|
||||
static readonly Undefined = new Node<any>(undefined);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// taken from https://github.com/microsoft/vscode/tree/master/src/vs/base/common
|
||||
// taken from https://github.com/microsoft/vscode/tree/main/src/vs/base/common
|
||||
|
||||
const LANGUAGE_DEFAULT = 'en';
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ describe('Foam URI', () => {
|
||||
const base = URI.file('/path/to/file.md');
|
||||
test.each([
|
||||
['https://www.google.com', URI.parse('https://www.google.com')],
|
||||
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
|
||||
['../relative/file.md', URI.file('/path/relative/file.md')],
|
||||
['#section', base.withFragment('section')],
|
||||
['/path/to/a/file.md', URI.parse('file:///path/to/a/file.md')],
|
||||
['../relative/file.md', URI.parse('file:///path/relative/file.md')],
|
||||
['#section', base.with({ fragment: 'section' })],
|
||||
[
|
||||
'../relative/file.md#section',
|
||||
URI.parse('file:/path/relative/file.md#section'),
|
||||
@@ -124,4 +124,135 @@ describe('asAbsoluteUri', () => {
|
||||
asAbsoluteUri(uri, [workspaceFolder1, workspaceFolder2, workspaceFolder3])
|
||||
).toEqual(workspaceFolder2.joinPath('file'));
|
||||
});
|
||||
|
||||
describe('forceSubfolder parameter', () => {
|
||||
it('should return the URI as-is when it is already a subfolder of a base folder', () => {
|
||||
const absolutePath = '/workspace/subfolder/file.md';
|
||||
const baseFolder = URI.file('/workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
expect(result.path).toEqual('/workspace/subfolder/file.md');
|
||||
});
|
||||
|
||||
it('should force URI to be a subfolder when forceSubfolder is true and URI is not a subfolder', () => {
|
||||
const absolutePath = '/other/path/file.md';
|
||||
const baseFolder = URI.file('/workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
expect(result.path).toEqual('/workspace/other/path/file.md');
|
||||
});
|
||||
|
||||
it('should use case-sensitive path comparison when checking if URI is already a subfolder', () => {
|
||||
const absolutePath = '/Workspace/subfolder/file.md'; // Different case
|
||||
const baseFolder = URI.file('/workspace'); // lowercase
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should be forced to subfolder because case-sensitive comparison fails
|
||||
expect(result.path).toEqual('/workspace/Workspace/subfolder/file.md');
|
||||
});
|
||||
|
||||
it('should not force subfolder when URI is exactly a case-sensitive match', () => {
|
||||
const absolutePath = '/workspace/subfolder/file.md';
|
||||
const baseFolder = URI.file('/workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should not be forced because it's already a subfolder (case matches)
|
||||
expect(result.path).toEqual('/workspace/subfolder/file.md');
|
||||
});
|
||||
|
||||
it('should handle multiple base folders when checking subfolder status', () => {
|
||||
const absolutePath = '/project2/subfolder/file.md';
|
||||
const baseFolder1 = URI.file('/project1');
|
||||
const baseFolder2 = URI.file('/project2');
|
||||
const result = asAbsoluteUri(
|
||||
absolutePath,
|
||||
[baseFolder1, baseFolder2],
|
||||
true
|
||||
);
|
||||
|
||||
// Should not be forced because it's already a subfolder of baseFolder2
|
||||
expect(result.path).toEqual('/project2/subfolder/file.md');
|
||||
});
|
||||
|
||||
describe('Windows paths', () => {
|
||||
it('should return the Windows URI as-is when it is already a subfolder of a base folder', () => {
|
||||
const absolutePath = 'C:\\workspace\\subfolder\\file.md';
|
||||
const baseFolder = URI.file('C:\\workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
expect(result.toFsPath()).toEqual('C:\\workspace\\subfolder\\file.md');
|
||||
});
|
||||
|
||||
it('should force Windows URI to be a subfolder when forceSubfolder is true and URI is not a subfolder', () => {
|
||||
const absolutePath = 'D:\\other\\path\\file.md';
|
||||
const baseFolder = URI.file('C:\\workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
expect(result.toFsPath()).toEqual(
|
||||
'C:\\workspace\\D:\\other\\path\\file.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should use case-sensitive path comparison for Windows paths when checking if URI is already a subfolder', () => {
|
||||
const absolutePath = 'C:\\Workspace\\subfolder\\file.md'; // Different case
|
||||
const baseFolder = URI.file('C:\\workspace'); // lowercase
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should be forced to subfolder because case-sensitive comparison fails
|
||||
expect(result.toFsPath()).toEqual(
|
||||
'C:\\workspace\\C:\\Workspace\\subfolder\\file.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should not force Windows subfolder when URI is exactly a case-sensitive match', () => {
|
||||
const absolutePath = 'C:\\workspace\\subfolder\\file.md';
|
||||
const baseFolder = URI.file('C:\\workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should not be forced because it's already a subfolder (case matches)
|
||||
expect(result.toFsPath()).toEqual('C:\\workspace\\subfolder\\file.md');
|
||||
});
|
||||
|
||||
it('should handle different drive letters as non-subfolders', () => {
|
||||
const absolutePath = 'D:\\workspace\\subfolder\\file.md'; // Different drive
|
||||
const baseFolder = URI.file('C:\\workspace'); // Same path, different drive
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should be forced because different drives are not subfolders
|
||||
expect(result.toFsPath()).toEqual(
|
||||
'C:\\workspace\\D:\\workspace\\subfolder\\file.md'
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle Windows backslash paths in case-sensitive comparison', () => {
|
||||
const absolutePath = 'C:\\Workspace\\subfolder\\file.md'; // Different case with backslashes
|
||||
const baseFolder = URI.file('c:\\Workspace'); // lowercase with backslashes
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should be forced to subfolder because case-sensitive comparison fails
|
||||
// Note: Drive letters are normalized to uppercase by URI.file()
|
||||
expect(result.toFsPath()).toEqual('C:\\Workspace\\subfolder\\file.md');
|
||||
});
|
||||
|
||||
it('should handle Windows backslash paths in case-sensitive comparison - reverse', () => {
|
||||
const absolutePath = 'c:\\Workspace\\subfolder\\file.md'; // Different case with backslashes
|
||||
const baseFolder = URI.file('C:\\Workspace'); // lowercase with backslashes
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
// Should be forced to subfolder because case-sensitive comparison fails
|
||||
// Note: Drive letters are normalized to uppercase by URI.file()
|
||||
expect(result.toFsPath()).toEqual('C:\\Workspace\\subfolder\\file.md');
|
||||
});
|
||||
|
||||
it('should handle forward slash absolute path also with windows base folders', () => {
|
||||
// Using this format for the path works on both windows and unix
|
||||
// and allows using absolute paths relative to the workspace root
|
||||
const absolutePath = '/subfolder/file.md';
|
||||
const baseFolder = URI.file('C:\\Workspace');
|
||||
const result = asAbsoluteUri(absolutePath, [baseFolder], true);
|
||||
|
||||
expect(result.toFsPath()).toEqual('C:\\Workspace\\subfolder\\file.md');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,11 @@ export class URI {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Will not work with web extension. Use only for testing.
|
||||
* @param value the path to turn into a URI
|
||||
* @returns the file URI
|
||||
*/
|
||||
static file(value: string): URI {
|
||||
const [path, authority] = pathUtils.fromFsPath(value);
|
||||
return new URI({ scheme: 'file', authority, path });
|
||||
@@ -71,7 +76,7 @@ export class URI {
|
||||
const uri = value instanceof URI ? value : URI.parse(value);
|
||||
if (!uri.isAbsolute()) {
|
||||
if (uri.scheme === 'file' || uri.scheme === 'placeholder') {
|
||||
let newUri = this.withFragment(uri.fragment);
|
||||
let newUri = this.with({ fragment: uri.fragment });
|
||||
if (uri.path) {
|
||||
newUri = (isDirectory ? newUri : newUri.getDirectory())
|
||||
.joinPath(uri.path)
|
||||
@@ -119,8 +124,20 @@ export class URI {
|
||||
return new URI({ ...this, path });
|
||||
}
|
||||
|
||||
withFragment(fragment: string): URI {
|
||||
return new URI({ ...this, fragment });
|
||||
with(change: {
|
||||
scheme?: string;
|
||||
authority?: string;
|
||||
path?: string;
|
||||
query?: string;
|
||||
fragment?: string;
|
||||
}): URI {
|
||||
return new URI({
|
||||
scheme: change.scheme ?? this.scheme,
|
||||
authority: change.authority ?? this.authority,
|
||||
path: change.path ?? this.path,
|
||||
query: change.query ?? this.query,
|
||||
fragment: change.fragment ?? this.fragment,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -376,15 +393,50 @@ function encodeURIComponentMinimal(path: string): string {
|
||||
*
|
||||
* @param uri the uri to evaluate
|
||||
* @param baseFolders the base folders to use
|
||||
* @param forceSubfolder if true, if the URI is not a subfolder of any baseFolder,
|
||||
* it will be forced to be a subfolder of the first base folder
|
||||
* @returns an absolute uri
|
||||
*
|
||||
* TODO this probably needs to be moved to the workspace service
|
||||
*/
|
||||
export function asAbsoluteUri(uri: URI, baseFolders: URI[]): URI {
|
||||
return URI.file(
|
||||
pathUtils.asAbsolutePaths(
|
||||
uri.path,
|
||||
baseFolders.map(f => f.path)
|
||||
)[0]
|
||||
);
|
||||
export function asAbsoluteUri(
|
||||
uriOrPath: URI | string,
|
||||
baseFolders: URI[],
|
||||
forceSubfolder = false
|
||||
): URI {
|
||||
if (baseFolders.length === 0) {
|
||||
throw new Error('At least one base folder needed to compute URI');
|
||||
}
|
||||
const path = uriOrPath instanceof URI ? uriOrPath.path : uriOrPath;
|
||||
|
||||
const isDrivePath = /^[a-zA-Z]:/.test(path);
|
||||
// Check if this is already a POSIX absolute path
|
||||
if (path.startsWith('/') || isDrivePath) {
|
||||
const uri = URI.parse(path); // Validate the path
|
||||
|
||||
if (forceSubfolder) {
|
||||
const isAlreadySubfolder = baseFolders.some(folder =>
|
||||
uri.path.startsWith(folder.path)
|
||||
);
|
||||
if (!isAlreadySubfolder) {
|
||||
return baseFolders[0].joinPath(uri.path);
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
let tokens = path.split('/');
|
||||
while (tokens[0].trim() === '') {
|
||||
tokens.shift();
|
||||
}
|
||||
const firstDir = tokens[0];
|
||||
if (baseFolders.length > 1) {
|
||||
for (const folder of baseFolders) {
|
||||
const lastDir = folder.path.split('/').pop();
|
||||
if (lastDir === firstDir) {
|
||||
tokens = tokens.slice(1);
|
||||
return folder.joinPath(...tokens);
|
||||
}
|
||||
}
|
||||
}
|
||||
return baseFolders[0].joinPath(...tokens);
|
||||
}
|
||||
|
||||
@@ -126,9 +126,9 @@ describe('Identifier computation', () => {
|
||||
});
|
||||
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
|
||||
|
||||
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
|
||||
'to/page-a#section name'
|
||||
);
|
||||
expect(
|
||||
ws.getIdentifier(first.uri.with({ fragment: 'section name' }))
|
||||
).toEqual('to/page-a#section name');
|
||||
});
|
||||
|
||||
const needle = '/project/car/todo';
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Emitter } from '../common/event';
|
||||
import { ResourceProvider } from './provider';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { IDataStore } from '../services/datastore';
|
||||
import TrieMap from 'mnemonist/trie-map';
|
||||
|
||||
export class FoamWorkspace implements IDisposable {
|
||||
private onDidAddEmitter = new Emitter<Resource>();
|
||||
@@ -20,7 +21,7 @@ export class FoamWorkspace implements IDisposable {
|
||||
/**
|
||||
* Resources by path
|
||||
*/
|
||||
private _resources: Map<string, Resource> = new Map();
|
||||
private _resources: TrieMap<string, Resource> = new TrieMap();
|
||||
|
||||
/**
|
||||
* @param defaultExtension: The default extension for notes in this workspace (e.g. `.md`)
|
||||
@@ -33,7 +34,10 @@ export class FoamWorkspace implements IDisposable {
|
||||
|
||||
set(resource: Resource) {
|
||||
const old = this.find(resource.uri);
|
||||
this._resources.set(normalize(resource.uri.path), resource);
|
||||
|
||||
// store resource
|
||||
this._resources.set(this.getTrieIdentifier(resource.uri.path), resource);
|
||||
|
||||
isSome(old)
|
||||
? this.onDidUpdateEmitter.fire({ old: old, new: resource })
|
||||
: this.onDidAddEmitter.fire(resource);
|
||||
@@ -41,13 +45,23 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
delete(uri: URI) {
|
||||
const deleted = this._resources.get(normalize(uri.path));
|
||||
this._resources.delete(normalize(uri.path));
|
||||
const deleted = this._resources.get(this.getTrieIdentifier(uri));
|
||||
this._resources.delete(this.getTrieIdentifier(uri));
|
||||
|
||||
isSome(deleted) && this.onDidDeleteEmitter.fire(deleted);
|
||||
return deleted ?? null;
|
||||
}
|
||||
|
||||
clear() {
|
||||
const resources = Array.from(this._resources.values());
|
||||
this._resources.clear();
|
||||
|
||||
// Fire delete events for all resources
|
||||
resources.forEach(resource => {
|
||||
this.onDidDeleteEmitter.fire(resource);
|
||||
});
|
||||
}
|
||||
|
||||
public exists(uri: URI): boolean {
|
||||
return isSome(this.find(uri));
|
||||
}
|
||||
@@ -57,7 +71,11 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public resources(): IterableIterator<Resource> {
|
||||
return this._resources.values();
|
||||
const resources: Array<Resource> = Array.from(
|
||||
this._resources.values()
|
||||
).sort(Resource.sortByPath);
|
||||
|
||||
return resources.values();
|
||||
}
|
||||
|
||||
public get(uri: URI): Resource {
|
||||
@@ -70,17 +88,21 @@ export class FoamWorkspace implements IDisposable {
|
||||
}
|
||||
|
||||
public listByIdentifier(identifier: string): Resource[] {
|
||||
const needle = normalize('/' + identifier);
|
||||
let needle = this.getTrieIdentifier(identifier);
|
||||
|
||||
const mdNeedle =
|
||||
getExtension(needle) !== this.defaultExtension
|
||||
? needle + this.defaultExtension
|
||||
getExtension(normalize(identifier)) !== this.defaultExtension
|
||||
? this.getTrieIdentifier(identifier + this.defaultExtension)
|
||||
: undefined;
|
||||
|
||||
const resources: Resource[] = [];
|
||||
for (const key of this._resources.keys()) {
|
||||
if (key.endsWith(mdNeedle) || key.endsWith(needle)) {
|
||||
resources.push(this._resources.get(normalize(key)));
|
||||
}
|
||||
|
||||
this._resources.find(needle).forEach(elm => resources.push(elm[1]));
|
||||
|
||||
if (mdNeedle) {
|
||||
this._resources.find(mdNeedle).forEach(elm => resources.push(elm[1]));
|
||||
}
|
||||
|
||||
return resources.sort(Resource.sortByPath);
|
||||
}
|
||||
|
||||
@@ -92,21 +114,19 @@ export class FoamWorkspace implements IDisposable {
|
||||
public getIdentifier(forResource: URI, exclude?: URI[]): string {
|
||||
const amongst = [];
|
||||
const basename = forResource.getBasename();
|
||||
for (const res of this._resources.values()) {
|
||||
// skip elements that cannot possibly match
|
||||
if (!res.uri.path.endsWith(basename)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.listByIdentifier(basename).map(res => {
|
||||
// skip self
|
||||
if (res.uri.isEqual(forResource)) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
|
||||
// skip exclude list
|
||||
if (exclude && exclude.find(ex => ex.isEqual(res.uri))) {
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
amongst.push(res.uri);
|
||||
}
|
||||
});
|
||||
|
||||
let identifier = FoamWorkspace.getShortestIdentifier(
|
||||
forResource.path,
|
||||
@@ -119,9 +139,32 @@ export class FoamWorkspace implements IDisposable {
|
||||
return identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a note identifier in reversed order. Used to optimise the storage of notes in
|
||||
* the workspace to optimise retrieval of notes.
|
||||
*
|
||||
* @param reference the URI path to reverse
|
||||
*/
|
||||
private getTrieIdentifier(reference: URI | string): string {
|
||||
let path: string;
|
||||
if (reference instanceof URI) {
|
||||
path = (reference as URI).path;
|
||||
} else {
|
||||
path = reference as string;
|
||||
}
|
||||
|
||||
let reversedPath = normalize(path).split('/').reverse().join('/');
|
||||
|
||||
if (reversedPath.indexOf('/') < 0) {
|
||||
reversedPath = reversedPath + '/';
|
||||
}
|
||||
|
||||
return reversedPath;
|
||||
}
|
||||
|
||||
public find(reference: URI | string, baseUri?: URI): Resource | null {
|
||||
if (reference instanceof URI) {
|
||||
return this._resources.get(normalize((reference as URI).path)) ?? null;
|
||||
return this._resources.get(this.getTrieIdentifier(reference)) ?? null;
|
||||
}
|
||||
let resource: Resource | null = null;
|
||||
const [path, fragment] = (reference as string).split('#');
|
||||
@@ -135,14 +178,17 @@ export class FoamWorkspace implements IDisposable {
|
||||
: isSome(baseUri)
|
||||
? baseUri.resolve(candidate).path
|
||||
: null;
|
||||
resource = this._resources.get(normalize(searchKey));
|
||||
resource = this._resources.get(this.getTrieIdentifier(searchKey));
|
||||
if (resource) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (resource && fragment) {
|
||||
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
|
||||
resource = {
|
||||
...resource,
|
||||
uri: resource.uri.with({ fragment: fragment }),
|
||||
};
|
||||
}
|
||||
return resource ?? null;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ResourceLink } from '../model/note';
|
||||
import { TextEdit } from './text-edit';
|
||||
|
||||
export abstract class MarkdownLink {
|
||||
private static wikilinkRegex = new RegExp(
|
||||
@@ -45,7 +46,7 @@ export abstract class MarkdownLink {
|
||||
type?: 'wikilink' | 'link';
|
||||
isEmbed?: boolean;
|
||||
}
|
||||
) {
|
||||
): TextEdit {
|
||||
const { target, section, alias } = MarkdownLink.analyzeLink(link);
|
||||
const newTarget = delta.target ?? target;
|
||||
const newSection = delta.section ?? section ?? '';
|
||||
|
||||
@@ -242,6 +242,18 @@ title: - one
|
||||
|
||||
expect(note.properties).toEqual({});
|
||||
});
|
||||
|
||||
it('#1467 - should parse yaml frontmatter with colon in value', () => {
|
||||
const note = createNoteFromMarkdown(`
|
||||
---
|
||||
tags: test
|
||||
source: https://example.com/page:123
|
||||
---
|
||||
|
||||
# Note with colon in meta value\n`);
|
||||
expect(note.properties.source).toBe('https://example.com/page:123');
|
||||
expect(note.tags[0].label).toEqual('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Tags', () => {
|
||||
@@ -320,20 +332,55 @@ this is some #text that includes #tags we #care-about.
|
||||
]);
|
||||
});
|
||||
|
||||
it('provides rough range for tags in yaml', () => {
|
||||
it('provides a specific range for tags in yaml', () => {
|
||||
// For now it's enough to just get the YAML block range
|
||||
// in the future we might want to be more specific
|
||||
|
||||
const noteA = createNoteFromMarkdown(`
|
||||
---
|
||||
prop: hello world
|
||||
tags: [hello, world, this_is_good]
|
||||
another: i love the world
|
||||
---
|
||||
# this is a heading
|
||||
this is some text
|
||||
`);
|
||||
expect(noteA.tags[0]).toEqual({
|
||||
label: 'hello',
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
range: Range.create(3, 7, 3, 12),
|
||||
});
|
||||
expect(noteA.tags[1]).toEqual({
|
||||
label: 'world',
|
||||
range: Range.create(3, 14, 3, 19),
|
||||
});
|
||||
expect(noteA.tags[2]).toEqual({
|
||||
label: 'this_is_good',
|
||||
range: Range.create(3, 21, 3, 33),
|
||||
});
|
||||
|
||||
const noteB = createNoteFromMarkdown(`
|
||||
---
|
||||
prop: hello world
|
||||
tags:
|
||||
- hello
|
||||
- world
|
||||
- this_is_good
|
||||
another: i love the world
|
||||
---
|
||||
# this is a heading
|
||||
this is some text
|
||||
`);
|
||||
expect(noteB.tags[0]).toEqual({
|
||||
label: 'hello',
|
||||
range: Range.create(4, 2, 4, 7),
|
||||
});
|
||||
expect(noteB.tags[1]).toEqual({
|
||||
label: 'world',
|
||||
range: Range.create(5, 2, 5, 7),
|
||||
});
|
||||
expect(noteB.tags[2]).toEqual({
|
||||
label: 'this_is_good',
|
||||
range: Range.create(6, 2, 6, 14),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -173,15 +173,51 @@ const getTextFromChildren = (root: Node): string => {
|
||||
return text;
|
||||
};
|
||||
|
||||
function getPropertiesInfoFromYAML(yamlText: string): {
|
||||
[key: string]: { key: string; value: string; text: string; line: number };
|
||||
} {
|
||||
const yamlProps = `\n${yamlText}`
|
||||
.split(/[\n](\w+:)/g)
|
||||
.filter(item => item.trim() !== '');
|
||||
const lines = yamlText.split('\n');
|
||||
let result: { line: number; key: string; text: string; value: string }[] = [];
|
||||
for (let i = 0; i < yamlProps.length / 2; i++) {
|
||||
const key = yamlProps[i * 2].replace(':', '');
|
||||
const value = yamlProps[i * 2 + 1].trim();
|
||||
const text = yamlProps[i * 2] + yamlProps[i * 2 + 1];
|
||||
result.push({ key, value, text, line: -1 });
|
||||
}
|
||||
result = result.map(p => {
|
||||
const line = lines.findIndex(l => l.startsWith(p.key + ':'));
|
||||
return { ...p, line };
|
||||
});
|
||||
return result.reduce((acc, curr) => {
|
||||
acc[curr.key] = curr;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const tagsPlugin: ParserPlugin = {
|
||||
name: 'tags',
|
||||
onDidFindProperties: (props, note, node) => {
|
||||
if (isSome(props.tags)) {
|
||||
const tagPropertyInfo = getPropertiesInfoFromYAML((node as any).value)[
|
||||
'tags'
|
||||
];
|
||||
const tagPropertyStartLine =
|
||||
node.position!.start.line + tagPropertyInfo.line;
|
||||
const tagPropertyLines = tagPropertyInfo.text.split('\n');
|
||||
const yamlTags = extractTagsFromProp(props.tags);
|
||||
for (const tag of yamlTags) {
|
||||
const tagLine = tagPropertyLines.findIndex(l => l.includes(tag));
|
||||
const line = tagPropertyStartLine + tagLine;
|
||||
const charStart = tagPropertyLines[tagLine].indexOf(tag);
|
||||
note.tags.push({
|
||||
label: tag,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
range: Range.createFromPosition(
|
||||
Position.create(line, charStart),
|
||||
Position.create(line, charStart + tag.length)
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('Link resolution', () => {
|
||||
const ws = createTestWorkspace().set(noteA).set(noteB);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteB.uri.withFragment('section')
|
||||
noteB.uri.with({ fragment: 'section' })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -163,7 +163,7 @@ describe('Link resolution', () => {
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteA.uri.withFragment('section')
|
||||
noteA.uri.with({ fragment: 'section' })
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
URI.placeholder(target);
|
||||
|
||||
if (section) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
targetUri = targetUri.with({ fragment: section });
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -93,7 +93,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
workspace.find(path, resource.uri)?.uri ??
|
||||
URI.placeholder(resource.uri.resolve(path).path);
|
||||
if (section && !targetUri.isPlaceholder()) {
|
||||
targetUri = targetUri.withFragment(section);
|
||||
targetUri = targetUri.with({ fragment: section });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -1,23 +1,85 @@
|
||||
import sha1 from 'js-sha1';
|
||||
|
||||
/**
|
||||
* Checks if a value is not null.
|
||||
*
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is not null, otherwise false.
|
||||
*/
|
||||
export function isNotNull<T>(value: T | null): value is T {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is not null, undefined, or void.
|
||||
*
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is not null, undefined, or void, otherwise false.
|
||||
*/
|
||||
export function isSome<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is NonNullable<T> {
|
||||
return value != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is null, undefined, or void.
|
||||
*
|
||||
* @param value - The value to check.
|
||||
* @returns True if the value is null, undefined, or void, otherwise false.
|
||||
*/
|
||||
export function isNone<T>(
|
||||
value: T | null | undefined | void
|
||||
): value is null | undefined | void {
|
||||
return value == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a string is numeric.
|
||||
*
|
||||
* @param value - The string to check.
|
||||
* @returns True if the string is numeric, otherwise false.
|
||||
*/
|
||||
export function isNumeric(value: string): boolean {
|
||||
return /-?\d+$/.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a SHA-1 hash of the given text.
|
||||
*
|
||||
* @param text - The text to hash.
|
||||
* @returns The SHA-1 hash of the text.
|
||||
*/
|
||||
export const hash = (text: string) => sha1.sha1(text);
|
||||
|
||||
/**
|
||||
* Executes an array of functions and returns the first result that satisfies the predicate.
|
||||
*
|
||||
* @param functions - The array of functions to execute.
|
||||
* @param predicate - The predicate to test the results. Defaults to checking if the result is not null.
|
||||
* @returns The first result that satisfies the predicate, or undefined if no result satisfies the predicate.
|
||||
*/
|
||||
export async function firstFrom<T>(
|
||||
functions: Array<() => T | Promise<T>>,
|
||||
predicate: (result: T) => boolean = result => result != null
|
||||
): Promise<T | undefined> {
|
||||
for (const fn of functions) {
|
||||
const result = await fn();
|
||||
if (predicate(result)) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily executes an array of functions and yields their results.
|
||||
*
|
||||
* @param functions - The array of functions to execute.
|
||||
* @returns A generator yielding the results of the functions.
|
||||
*/
|
||||
function* lazyExecutor<T>(functions: Array<() => T>): Generator<T> {
|
||||
for (const fn of functions) {
|
||||
yield fn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
|
||||
/* @unit-ready */
|
||||
import { workspace, window } from 'vscode';
|
||||
import {
|
||||
CREATE_DAILY_NOTE_WARNING_RESPONSE,
|
||||
createDailyNoteIfNotExists,
|
||||
getDailyNoteUri,
|
||||
} from './dated-notes';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
createFile,
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { fileExists, readFile } from './services/editor';
|
||||
import {
|
||||
getDailyNoteTemplateCandidateUris,
|
||||
getDailyNoteTemplateUri,
|
||||
} from './services/templates';
|
||||
|
||||
describe('getDailyNotePath', () => {
|
||||
describe('getDailyNoteUri', () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
const year = date.getFullYear();
|
||||
const month = date.getMonth() + 1;
|
||||
@@ -21,12 +32,12 @@ describe('getDailyNotePath', () => {
|
||||
test('Adds the root directory to relative directories', async () => {
|
||||
const config = 'journal';
|
||||
|
||||
const expectedPath = fromVsCodeUri(
|
||||
const expectedUri = fromVsCodeUri(
|
||||
workspace.workspaceFolders[0].uri
|
||||
).joinPath(config, `${isoDate}.md`);
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
|
||||
expect(getDailyNoteUri(date)).toEqual(expectedUri)
|
||||
);
|
||||
});
|
||||
|
||||
@@ -39,30 +50,295 @@ describe('getDailyNotePath', () => {
|
||||
: `${config}/${isoDate}.md`;
|
||||
|
||||
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
|
||||
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
|
||||
expect(getDailyNoteUri(date).toFsPath()).toMatch(expectedPath)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Daily note template', () => {
|
||||
it('Uses the daily note variables in the template', async () => {
|
||||
const targetDate = new Date(2021, 8, 12);
|
||||
describe('Daily note creation and template processing', () => {
|
||||
const DAILY_NOTE_TEMPLATE = ['.foam', 'templates', 'daily-note.md'];
|
||||
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
|
||||
['.foam', 'templates', 'daily-note.md']
|
||||
);
|
||||
beforeEach(async () => {
|
||||
// Ensure daily note template are removed before each test
|
||||
for (const template of getDailyNoteTemplateCandidateUris()) {
|
||||
if (await fileExists(template)) {
|
||||
await deleteFile(template);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const uri = getDailyNotePath(targetDate);
|
||||
describe('Basic daily note creation', () => {
|
||||
it('Creates a new daily note when it does not exist', async () => {
|
||||
const targetDate = new Date(2021, 8, 1);
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
|
||||
await createDailyNoteIfNotExists(targetDate);
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toEqual('hello September 12 hello');
|
||||
expect(result.didCreateFile).toBe(true);
|
||||
expect(result.uri).toEqual(uri);
|
||||
|
||||
await deleteFile(template.uri);
|
||||
const doc = await showInEditor(uri);
|
||||
expect(doc.editor.document.getText()).toContain('2021-09-01');
|
||||
});
|
||||
|
||||
it('Opens existing daily note when it already exists', async () => {
|
||||
const targetDate = new Date(2021, 8, 2);
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
|
||||
// Create the file first
|
||||
await createFile('# Existing Note\n\nContent here', [uri.getBasename()]);
|
||||
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
expect(result.didCreateFile).toBe(false);
|
||||
expect(result.uri).toEqual(uri);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
expect(doc.editor.document.getText()).toContain('Existing Note');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template variable resolution', () => {
|
||||
it('Resolves all FOAM_DATE_* variables correctly', async () => {
|
||||
const targetDate = new Date(2021, 8, 12); // September 12, 2021
|
||||
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
`# \${FOAM_DATE_YEAR}-\${FOAM_DATE_MONTH}-\${FOAM_DATE_DATE}
|
||||
|
||||
Year: \${FOAM_DATE_YEAR} (short: \${FOAM_DATE_YEAR_SHORT})
|
||||
Month: \${FOAM_DATE_MONTH} (name: \${FOAM_DATE_MONTH_NAME}, short: \${FOAM_DATE_MONTH_NAME_SHORT})
|
||||
Date: \${FOAM_DATE_DATE}
|
||||
Day: \${FOAM_DATE_DAY_NAME} (short: \${FOAM_DATE_DAY_NAME_SHORT})
|
||||
Week: \${FOAM_DATE_WEEK}
|
||||
Unix: \${FOAM_DATE_SECONDS_UNIX}`,
|
||||
DAILY_NOTE_TEMPLATE
|
||||
);
|
||||
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(result.uri);
|
||||
const content = doc.editor.document.getText();
|
||||
|
||||
expect(content).toContain('# 2021-09-12');
|
||||
expect(content).toContain('Year: 2021 (short: 21)');
|
||||
expect(content).toContain('Month: 09 (name: September, short: Sep)');
|
||||
expect(content).toContain('Date: 12');
|
||||
expect(content).toContain('Day: Sunday (short: Sun)');
|
||||
expect(content).toContain('Week: 36');
|
||||
|
||||
await deleteFile(template.uri);
|
||||
await deleteFile(result.uri);
|
||||
});
|
||||
|
||||
it('Resolves FOAM_TITLE variable for daily notes', async () => {
|
||||
const targetDate = new Date(2021, 8, 13);
|
||||
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
'# Daily Note: ${FOAM_TITLE}\n\nToday is ${FOAM_TITLE}.',
|
||||
DAILY_NOTE_TEMPLATE
|
||||
);
|
||||
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toContain('Daily Note: 2021-09-13');
|
||||
expect(content).toContain('Today is 2021-09-13.');
|
||||
await deleteFile(result.uri);
|
||||
await deleteFile(template.uri);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration settings', () => {
|
||||
it('Respects custom filename format', async () => {
|
||||
const targetDate = new Date(2021, 8, 14);
|
||||
const customFormat = 'yyyy-mm-dd';
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
'openDailyNote.filenameFormat',
|
||||
customFormat,
|
||||
async () => {
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
expect(uri.getBasename()).toBe('2021-09-14.md');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Respects custom file extension', async () => {
|
||||
const targetDate = new Date(2021, 8, 15);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
'openDailyNote.fileExtension',
|
||||
'txt',
|
||||
async () => {
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
expect(uri.getBasename()).toBe('2021-09-15.txt');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Respects custom directory setting', async () => {
|
||||
const targetDate = new Date(2021, 8, 16);
|
||||
const customDir = 'journal/daily';
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
'openDailyNote.directory',
|
||||
customDir,
|
||||
async () => {
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
expect(uri.path).toContain('/journal/daily/');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('Uses custom title format when specified', async () => {
|
||||
const targetDate = new Date(2021, 8, 17);
|
||||
|
||||
await withModifiedFoamConfiguration(
|
||||
'openDailyNote.titleFormat',
|
||||
'fullDate',
|
||||
async () => {
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toContain('# Friday, September 17, 2021');
|
||||
await deleteFile(result.uri);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Template types and processing', () => {
|
||||
it('Processes Markdown templates correctly', async () => {
|
||||
const targetDate = new Date(2021, 8, 19);
|
||||
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
'hello ${FOAM_DATE_MONTH_NAME} ${FOAM_DATE_DATE} hello',
|
||||
DAILY_NOTE_TEMPLATE
|
||||
);
|
||||
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toEqual('hello September 19 hello');
|
||||
await deleteFile(result.uri);
|
||||
await deleteFile(template.uri);
|
||||
});
|
||||
|
||||
it('Processes JavaScript templates correctly', async () => {
|
||||
const targetDate = new Date(2021, 8, 20);
|
||||
|
||||
const jsTemplate = await createFile(
|
||||
`async function createNote ({ foamDate }) {
|
||||
const monthName = foamDate.toLocaleString('default', { month: 'long' });
|
||||
const day = foamDate.getDate();
|
||||
return {
|
||||
filepath: \`\${foamDate.getFullYear()}-\${String(foamDate.getMonth() + 1).padStart(2, '0')}-\${String(day).padStart(2, '0')}.md\`,
|
||||
content: \`# JS Template: \${monthName} \${day}\n\nGenerated by JavaScript template.\`
|
||||
};
|
||||
};`,
|
||||
['.foam', 'templates', 'daily-note.js']
|
||||
);
|
||||
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toContain('# JS Template: September 20');
|
||||
expect(content).toContain('Generated by JavaScript template.');
|
||||
|
||||
await deleteFile(jsTemplate.uri);
|
||||
await deleteFile(result.uri);
|
||||
});
|
||||
|
||||
it('Falls back to default text when no template exists', async () => {
|
||||
const targetDate = new Date(2021, 8, 21);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(result.uri);
|
||||
const content = doc.editor.document.getText();
|
||||
expect(content).toContain('# 2021-09-21'); // Should use fallback text with formatted date
|
||||
});
|
||||
|
||||
it('prompts to create a daily note template if one does not exist', async () => {
|
||||
const targetDate = new Date(2021, 8, 23);
|
||||
const foam = {} as any;
|
||||
|
||||
expect(await getDailyNoteTemplateUri()).not.toBeDefined();
|
||||
|
||||
// Intercept the showWarningMessage call
|
||||
const showWarningMessageSpy = jest
|
||||
.spyOn(window, 'showWarningMessage')
|
||||
.mockResolvedValue(CREATE_DAILY_NOTE_WARNING_RESPONSE as any); // simulate user action
|
||||
|
||||
await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
expect(showWarningMessageSpy.mock.calls[0][0]).toMatch(
|
||||
/No daily note template found/
|
||||
);
|
||||
|
||||
const templateUri = await getDailyNoteTemplateUri();
|
||||
|
||||
expect(templateUri).toBeDefined();
|
||||
expect(await fileExists(templateUri)).toBe(true);
|
||||
|
||||
const templateContent = await readFile(templateUri);
|
||||
expect(templateContent).toContain('foam_template:');
|
||||
|
||||
// Clean up the created template
|
||||
await deleteFile(templateUri);
|
||||
showWarningMessageSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('Processes template frontmatter metadata correctly', async () => {
|
||||
const targetDate = new Date(2021, 8, 22);
|
||||
|
||||
const template = await createFile(
|
||||
`---
|
||||
tags: [daily, journal]
|
||||
author: foam
|
||||
---
|
||||
# Daily Note
|
||||
|
||||
Content here with \${FOAM_DATE_MONTH_NAME} \${FOAM_DATE_DATE}`,
|
||||
DAILY_NOTE_TEMPLATE
|
||||
);
|
||||
|
||||
const uri = getDailyNoteUri(targetDate);
|
||||
const foam = {} as any; // Mock Foam instance
|
||||
const result = await createDailyNoteIfNotExists(targetDate, foam);
|
||||
|
||||
const doc = await showInEditor(uri);
|
||||
const content = doc.editor.document.getText();
|
||||
|
||||
// Should not contain the frontmatter separator in final content
|
||||
expect(content).toContain(`---
|
||||
tags: [daily, journal]
|
||||
author: foam
|
||||
---`);
|
||||
expect(content).toContain('# Daily Note');
|
||||
expect(content).toContain('Content here with September 22');
|
||||
|
||||
await deleteFile(template.uri);
|
||||
await deleteFile(result.uri);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { Uri, window, workspace } from 'vscode';
|
||||
import { joinPath } from './core/utils/path';
|
||||
import dateFormat from 'dateformat';
|
||||
import { URI } from './core/model/uri';
|
||||
import { NoteFactory } from './services/templates';
|
||||
import { getDailyNoteTemplateUri } from './services/templates';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
import { asAbsoluteWorkspaceUri, focusNote } from './services/editor';
|
||||
import { Foam } from './core/model/foam';
|
||||
import { createNote } from './features/commands/create-note';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { showInEditor } from './test/test-utils-vscode';
|
||||
|
||||
/**
|
||||
* Open the daily note file.
|
||||
@@ -11,13 +17,14 @@ import { asAbsoluteWorkspaceUri, focusNote } from './services/editor';
|
||||
* it gets created along with any folders in its path.
|
||||
*
|
||||
* @param date The target date. If not provided, the function returns immediately.
|
||||
* @param foam The Foam instance, used to create the note.
|
||||
*/
|
||||
export async function openDailyNoteFor(date?: Date) {
|
||||
export async function openDailyNoteFor(date?: Date, foam?: Foam) {
|
||||
if (date == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { didCreateFile, uri } = await createDailyNoteIfNotExists(date);
|
||||
const { didCreateFile, uri } = await createDailyNoteIfNotExists(date, foam);
|
||||
// if a new file is created, the editor is automatically created
|
||||
// but forcing the focus will block the template placeholders from working
|
||||
// so we only explicitly focus on the note if the file already exists
|
||||
@@ -32,17 +39,13 @@ export async function openDailyNoteFor(date?: Date) {
|
||||
* This function first checks the `foam.openDailyNote.directory` configuration string,
|
||||
* defaulting to the current directory.
|
||||
*
|
||||
* In the case that the directory path is not absolute,
|
||||
* the resulting path will start on the current workspace top-level.
|
||||
*
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @returns The path to the daily note file.
|
||||
* @returns The URI to the daily note file.
|
||||
*/
|
||||
export function getDailyNotePath(date: Date): URI {
|
||||
export function getDailyNoteUri(date: Date): URI {
|
||||
const folder = getFoamVsCodeConfig<string>('openDailyNote.directory') ?? '.';
|
||||
const dailyNoteDirectory = asAbsoluteWorkspaceUri(URI.file(folder));
|
||||
const dailyNoteFilename = getDailyNoteFileName(date);
|
||||
return dailyNoteDirectory.joinPath(dailyNoteFilename);
|
||||
return asAbsoluteWorkspaceUri(joinPath(folder, dailyNoteFilename));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -57,40 +60,103 @@ export function getDailyNotePath(date: Date): URI {
|
||||
*/
|
||||
export function getDailyNoteFileName(date: Date): string {
|
||||
const filenameFormat: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.filenameFormat'
|
||||
'openDailyNote.filenameFormat',
|
||||
'yyyy-mm-dd'
|
||||
);
|
||||
const fileExtension: string = getFoamVsCodeConfig(
|
||||
'openDailyNote.fileExtension'
|
||||
'openDailyNote.fileExtension',
|
||||
'md'
|
||||
);
|
||||
|
||||
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a daily note if it does not exist.
|
||||
*
|
||||
* In the case that the folders referenced in the file path also do not exist,
|
||||
* this function will create all folders in the path.
|
||||
*
|
||||
* @param currentDate The current date, to be used as a title.
|
||||
* @returns Whether the file was created.
|
||||
*/
|
||||
export async function createDailyNoteIfNotExists(targetDate: Date) {
|
||||
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
|
||||
const titleFormat: string =
|
||||
getFoamVsCodeConfig('openDailyNote.titleFormat') ??
|
||||
getFoamVsCodeConfig('openDailyNote.filenameFormat');
|
||||
|
||||
const templateFallbackText = `---
|
||||
const DEFAULT_DAILY_NOTE_TEMPLATE = `---
|
||||
foam_template:
|
||||
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
|
||||
filepath: "/journal/\${FOAM_DATE_YEAR}-\${FOAM_DATE_MONTH}-\${FOAM_DATE_DATE}.md"
|
||||
description: "Daily note template"
|
||||
---
|
||||
# ${dateFormat(targetDate, titleFormat, false)}
|
||||
# \${FOAM_DATE_YEAR}-\${FOAM_DATE_MONTH}-\${FOAM_DATE_DATE}
|
||||
|
||||
> you probably want to delete these instructions as you customize your template
|
||||
|
||||
Welcome to your new daily note template.
|
||||
The file is located in \`.foam/templates/daily-note.md\`.
|
||||
The text in this file will be used as the content of your daily note.
|
||||
You can customize it as you like, and you can use the following variables in the template:
|
||||
- \`\${FOAM_DATE_YEAR}\`: The year of the date
|
||||
- \`\${FOAM_DATE_MONTH}\`: The month of the date
|
||||
- \`\${FOAM_DATE_DATE}\`: The day of the date
|
||||
- \`\${FOAM_TITLE}\`: The title of the note
|
||||
|
||||
Go to https://github.com/foambubble/foam/blob/main/docs/user/features/daily-notes.md for more details.
|
||||
For more complex templates, including Javascript dynamic templates, see https://github.com/foambubble/foam/blob/main/docs/user/features/note-templates.md.
|
||||
`;
|
||||
|
||||
return await NoteFactory.createFromDailyNoteTemplate(
|
||||
pathFromLegacyConfiguration,
|
||||
templateFallbackText,
|
||||
targetDate
|
||||
export const CREATE_DAILY_NOTE_WARNING_RESPONSE = 'Create daily note template';
|
||||
|
||||
/**
|
||||
* Create a daily note using the unified creation engine (supports JS templates)
|
||||
*
|
||||
* @param targetDate The target date
|
||||
* @param foam The Foam instance
|
||||
* @returns Whether the file was created and the URI
|
||||
*/
|
||||
export async function createDailyNoteIfNotExists(targetDate: Date, foam: Foam) {
|
||||
const templatePath = await getDailyNoteTemplateUri();
|
||||
|
||||
if (!templatePath) {
|
||||
window
|
||||
.showWarningMessage(
|
||||
'No daily note template found. Using legacy configuration (deprecated). Create a daily note template to avoid this warning and customize your daily note.',
|
||||
CREATE_DAILY_NOTE_WARNING_RESPONSE
|
||||
)
|
||||
.then(async action => {
|
||||
if (action === CREATE_DAILY_NOTE_WARNING_RESPONSE) {
|
||||
const newTemplateUri = Uri.joinPath(
|
||||
workspace.workspaceFolders[0].uri,
|
||||
'.foam',
|
||||
'templates',
|
||||
'daily-note.md'
|
||||
);
|
||||
await workspace.fs.writeFile(
|
||||
newTemplateUri,
|
||||
new TextEncoder().encode(DEFAULT_DAILY_NOTE_TEMPLATE)
|
||||
);
|
||||
await showInEditor(fromVsCodeUri(newTemplateUri));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Set up variables for template processing
|
||||
const formattedDate = dateFormat(targetDate, 'yyyy-mm-dd', false);
|
||||
const variables = {
|
||||
FOAM_TITLE: formattedDate,
|
||||
title: formattedDate,
|
||||
};
|
||||
|
||||
const dailyNoteUri = getDailyNoteUri(targetDate);
|
||||
const titleFormat: string =
|
||||
getFoamVsCodeConfig('openDailyNote.titleFormat') ??
|
||||
getFoamVsCodeConfig('openDailyNote.filenameFormat') ??
|
||||
'isoDate';
|
||||
|
||||
const templateFallbackText = `# ${dateFormat(
|
||||
targetDate,
|
||||
titleFormat,
|
||||
false
|
||||
)}\n`;
|
||||
|
||||
return await createNote(
|
||||
{
|
||||
notePath: dailyNoteUri.toFsPath(),
|
||||
templatePath: templatePath,
|
||||
text: templateFallbackText,
|
||||
date: targetDate,
|
||||
variables: variables,
|
||||
onFileExists: 'open',
|
||||
onRelativeNotePath: 'resolve-from-root',
|
||||
},
|
||||
foam
|
||||
);
|
||||
}
|
||||
|
||||
@@ -175,11 +175,11 @@ async function convertLinkInCopy(
|
||||
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
|
||||
const basePath = doc.uri.path.split('/').slice(0, -1).join('/');
|
||||
|
||||
const fileUri = Uri.file(
|
||||
`${
|
||||
const fileUri = doc.uri.with({
|
||||
path: `${
|
||||
basePath ? basePath + '/' : ''
|
||||
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`
|
||||
);
|
||||
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`,
|
||||
});
|
||||
const encoder = new TextEncoder();
|
||||
await workspace.fs.writeFile(fileUri, encoder.encode(text));
|
||||
await window.showTextDocument(fileUri);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
/* @unit-ready */
|
||||
import { env, Position, Selection, commands } from 'vscode';
|
||||
import { createFile, showInEditor } from '../../test/test-utils-vscode';
|
||||
import { removeBrackets, toTitleCase } from './copy-without-brackets';
|
||||
|
||||
describe('copy-without-brackets command', () => {
|
||||
it('should get the input from the active editor selection', async () => {
|
||||
const { uri } = await createFile('This is my [[test-content]].');
|
||||
const { uri } = await createFile('This is my [[test-content]].', [
|
||||
'copy-without-brackets',
|
||||
'file.md',
|
||||
]);
|
||||
const { editor } = await showInEditor(uri);
|
||||
editor.selection = new Selection(new Position(0, 0), new Position(1, 0));
|
||||
await commands.executeCommand('foam-vscode.copy-without-brackets');
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { createFile } from '../../test/test-utils-vscode';
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { commands, ExtensionContext } from 'vscode';
|
||||
import { askUserForTemplate, NoteFactory } from '../../services/templates';
|
||||
import { Resolver } from '../../services/variable-resolver';
|
||||
import { askUserForTemplate } from '../../services/templates';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.create-note-from-template',
|
||||
async () => {
|
||||
const templateUri = await askUserForTemplate();
|
||||
|
||||
if (templateUri) {
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
|
||||
await NoteFactory.createFromTemplate(templateUri, resolver);
|
||||
}
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
askForTemplate: true,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { commands, window } from 'vscode';
|
||||
/* @unit-ready */
|
||||
import { commands, window, workspace } from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { asAbsoluteWorkspaceUri, readFile } from '../../services/editor';
|
||||
import {
|
||||
@@ -41,7 +42,7 @@ describe('create-note command', () => {
|
||||
]);
|
||||
const target = getUriInWorkspace();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.path,
|
||||
notePath: target,
|
||||
templatePath: templateA.uri.path,
|
||||
text: 'hello',
|
||||
});
|
||||
@@ -54,7 +55,7 @@ describe('create-note command', () => {
|
||||
it('focuses on the newly created note', async () => {
|
||||
const target = getUriInWorkspace();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.path,
|
||||
notePath: target,
|
||||
text: 'hello',
|
||||
});
|
||||
expect(window.activeTextEditor.document.getText()).toEqual('hello');
|
||||
@@ -65,7 +66,7 @@ describe('create-note command', () => {
|
||||
it('supports variables', async () => {
|
||||
const target = getUriInWorkspace();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.path,
|
||||
notePath: target,
|
||||
text: 'hello ${FOAM_TITLE}', // eslint-disable-line no-template-curly-in-string
|
||||
variables: { FOAM_TITLE: 'world' },
|
||||
});
|
||||
@@ -77,7 +78,7 @@ describe('create-note command', () => {
|
||||
it('supports date variables', async () => {
|
||||
const target = getUriInWorkspace();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.path,
|
||||
notePath: target,
|
||||
text: 'hello ${FOAM_DATE_YEAR}', // eslint-disable-line no-template-curly-in-string
|
||||
date: '2021-10-01',
|
||||
});
|
||||
@@ -92,7 +93,7 @@ describe('create-note command', () => {
|
||||
expect(content).toEqual('hello');
|
||||
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.uri.path,
|
||||
notePath: target.uri,
|
||||
text: 'test overwrite',
|
||||
onFileExists: 'overwrite',
|
||||
});
|
||||
@@ -103,7 +104,7 @@ describe('create-note command', () => {
|
||||
|
||||
await closeEditors();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.uri.path,
|
||||
notePath: target.uri,
|
||||
text: 'test open',
|
||||
onFileExists: 'open',
|
||||
});
|
||||
@@ -114,7 +115,7 @@ describe('create-note command', () => {
|
||||
|
||||
await closeEditors();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.uri.path,
|
||||
notePath: target.uri,
|
||||
text: 'test cancel',
|
||||
onFileExists: 'cancel',
|
||||
});
|
||||
@@ -125,7 +126,7 @@ describe('create-note command', () => {
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
await closeEditors();
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.uri.path,
|
||||
notePath: target.uri,
|
||||
text: 'test ask',
|
||||
onFileExists: 'ask',
|
||||
});
|
||||
@@ -135,44 +136,53 @@ describe('create-note command', () => {
|
||||
});
|
||||
|
||||
it('supports various options to deal with relative paths', async () => {
|
||||
const TEST_FOLDER = 'create-note-tests';
|
||||
const base = await createFile('relative path tests base file', [
|
||||
'create-note-tests',
|
||||
TEST_FOLDER,
|
||||
'base-file.md',
|
||||
]);
|
||||
|
||||
await closeEditors();
|
||||
await showInEditor(base.uri);
|
||||
|
||||
const target = getUriInWorkspace();
|
||||
expectSameUri(window.activeTextEditor.document.uri, base.uri);
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.getBasename(),
|
||||
notePath: 'note-resolved-from-root.md',
|
||||
text: 'test resolving from root',
|
||||
onRelativeNotePath: 'resolve-from-root',
|
||||
});
|
||||
expectSameUri(
|
||||
window.activeTextEditor.document.uri,
|
||||
fromVsCodeUri(workspace.workspaceFolders?.[0].uri).joinPath(
|
||||
'note-resolved-from-root.md'
|
||||
)
|
||||
);
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
'test resolving from root'
|
||||
);
|
||||
expectSameUri(window.activeTextEditor.document.uri, target);
|
||||
|
||||
await closeEditors();
|
||||
await showInEditor(base.uri);
|
||||
expectSameUri(window.activeTextEditor.document.uri, base.uri);
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.getBasename(),
|
||||
notePath: 'note-resolved-from-current-dir.md',
|
||||
text: 'test resolving from current dir',
|
||||
onRelativeNotePath: 'resolve-from-current-dir',
|
||||
});
|
||||
expectSameUri(
|
||||
window.activeTextEditor.document.uri,
|
||||
fromVsCodeUri(workspace.workspaceFolders?.[0].uri).joinPath(
|
||||
TEST_FOLDER,
|
||||
'note-resolved-from-current-dir.md'
|
||||
)
|
||||
);
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
'test resolving from current dir'
|
||||
);
|
||||
expect(fromVsCodeUri(window.activeTextEditor.document.uri).path).toEqual(
|
||||
fromVsCodeUri(window.activeTextEditor.document.uri)
|
||||
.getDirectory()
|
||||
.joinPath(target.getBasename()).path
|
||||
);
|
||||
|
||||
await closeEditors();
|
||||
await showInEditor(base.uri);
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.getBasename(),
|
||||
notePath: 'note-that-should-not-be-created.md',
|
||||
text: 'test cancelling',
|
||||
onRelativeNotePath: 'cancel',
|
||||
});
|
||||
@@ -184,13 +194,61 @@ describe('create-note command', () => {
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: target.getBasename(),
|
||||
notePath: 'ask-me-about-it.md',
|
||||
text: 'test asking',
|
||||
onRelativeNotePath: 'ask',
|
||||
});
|
||||
expect(spy).toHaveBeenCalled();
|
||||
|
||||
await deleteFile(base);
|
||||
// await deleteFile(base);
|
||||
});
|
||||
|
||||
it('throws an error if the template file does not exist', async () => {
|
||||
const nonExistentTemplatePath = '/non/existent/template/path.md';
|
||||
await expect(
|
||||
commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: 'note-with-missing-template.md',
|
||||
templatePath: nonExistentTemplatePath,
|
||||
text: 'should not matter',
|
||||
})
|
||||
).rejects.toThrow(
|
||||
`Failed to load template (file://${nonExistentTemplatePath}): Template file not found: file://${nonExistentTemplatePath}`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error if the template file does not exist (relative path)', async () => {
|
||||
try {
|
||||
const nonExistentTemplatePath = 'relative/non-existent-template.md';
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: 'note-with-missing-template-relative.md',
|
||||
templatePath: nonExistentTemplatePath,
|
||||
text: 'should not matter',
|
||||
});
|
||||
throw new Error('Expected an error to be thrown');
|
||||
} catch (error) {
|
||||
expect(error.message).toContain(`Failed to load template`); // eslint-disable-line jest/no-conditional-expect
|
||||
}
|
||||
});
|
||||
|
||||
it('creates a note with absolute path within the workspace', async () => {
|
||||
await commands.executeCommand('foam-vscode.create-note', {
|
||||
notePath: '/note-in-workspace.md',
|
||||
text: 'hello workspace',
|
||||
});
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
'hello workspace'
|
||||
);
|
||||
expectSameUri(
|
||||
window.activeTextEditor.document.uri,
|
||||
fromVsCodeUri(workspace.workspaceFolders?.[0].uri).joinPath(
|
||||
'note-in-workspace.md'
|
||||
)
|
||||
);
|
||||
await deleteFile(
|
||||
fromVsCodeUri(workspace.workspaceFolders?.[0].uri).joinPath(
|
||||
'note-in-workspace.md'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -242,7 +300,7 @@ foam_template:
|
||||
const results: Awaited<ReturnType<typeof createNote>> =
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
expect(results.didCreateFile).toBeTruthy();
|
||||
expect(results.uri.path.endsWith('hello-world.md')).toBeTruthy();
|
||||
expect(results.uri.path).toMatch(/hello-world.md$/);
|
||||
|
||||
const newNoteDoc = window.activeTextEditor.document;
|
||||
expect(newNoteDoc.uri.path).toMatch(/hello-world.md$/);
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { workspace, commands, WorkspaceEdit, ExtensionContext } from 'vscode';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import {
|
||||
askUserForTemplate,
|
||||
getDefaultTemplateUri,
|
||||
getPathFromTitle,
|
||||
NoteFactory,
|
||||
} from '../../services/templates';
|
||||
import { NoteCreationEngine } from '../../services/note-creation-engine';
|
||||
import { TriggerFactory } from '../../services/note-creation-triggers';
|
||||
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';
|
||||
@@ -14,15 +17,20 @@ import { Foam } from '../../core/model/foam';
|
||||
import { Location } from '../../core/model/location';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { ResourceLink } from '../../core/model/note';
|
||||
import { toVsCodeRange, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import {
|
||||
fromVsCodeUri,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(CREATE_NOTE_COMMAND.command, args =>
|
||||
commands.registerCommand(CREATE_NOTE_COMMAND.command, args =>
|
||||
createNote(args, foam)
|
||||
)
|
||||
);
|
||||
@@ -33,11 +41,11 @@ interface CreateNoteArgs {
|
||||
* The path of the note to create.
|
||||
* If relative it will be resolved against the workspace root.
|
||||
*/
|
||||
notePath?: string;
|
||||
notePath?: string | URI;
|
||||
/**
|
||||
* The path of the template to use.
|
||||
*/
|
||||
templatePath?: string;
|
||||
templatePath?: string | URI;
|
||||
/**
|
||||
* Whether to ask the user to select a template for the new note. If so, overwrites templatePath.
|
||||
*/
|
||||
@@ -54,7 +62,7 @@ interface CreateNoteArgs {
|
||||
/**
|
||||
* The date used to resolve the FOAM_DATE_* variables. in YYYY-MM-DD format
|
||||
*/
|
||||
date?: string;
|
||||
date?: string | Date;
|
||||
/**
|
||||
* The title of the note (translates into the FOAM_TITLE variable)
|
||||
*/
|
||||
@@ -82,18 +90,39 @@ const DEFAULT_NEW_NOTE_TEXT = `# \${FOAM_TITLE}
|
||||
|
||||
\${FOAM_SELECTED_TEXT}`;
|
||||
|
||||
/**
|
||||
* Related to #1505.
|
||||
* This function forces the date to be local by removing any time information and
|
||||
* adding a local time (noon) to it.
|
||||
* @param dateString The date string, either in YYYY-MM-DD format or any format parsable by Date()
|
||||
* @returns The parsed Date object
|
||||
*/
|
||||
function forceLocalDate(dateString: string): Date {
|
||||
// Remove the time part if present
|
||||
const dateOnly = dateString.split('T')[0];
|
||||
// Otherwise, treat as local date by adding local noon time
|
||||
return new Date(dateOnly + 'T12:00:00');
|
||||
}
|
||||
|
||||
export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
args = args ?? {};
|
||||
const date = isSome(args.date) ? new Date(Date.parse(args.date)) : new Date();
|
||||
const resolver = new Resolver(
|
||||
new Map(Object.entries(args.variables ?? {})),
|
||||
date
|
||||
);
|
||||
if (args.title) {
|
||||
resolver.define('FOAM_TITLE', args.title);
|
||||
}
|
||||
const text = args.text ?? DEFAULT_NEW_NOTE_TEXT;
|
||||
const noteUri = args.notePath && URI.file(args.notePath);
|
||||
const foamDate =
|
||||
typeof args.date === 'string'
|
||||
? forceLocalDate(args.date)
|
||||
: args.date instanceof Date
|
||||
? args.date
|
||||
: new Date();
|
||||
|
||||
// Create appropriate trigger based on context
|
||||
const trigger = args.sourceLink
|
||||
? TriggerFactory.createPlaceholderTrigger(
|
||||
args.sourceLink.uri,
|
||||
foam.workspace.find(new URI(args.sourceLink.uri))?.title || 'Unknown',
|
||||
args.sourceLink
|
||||
)
|
||||
: TriggerFactory.createCommandTrigger('foam-vscode.create-note');
|
||||
|
||||
// Determine template path
|
||||
let templateUri: URI;
|
||||
if (args.askForTemplate) {
|
||||
const selectedTemplate = await askUserForTemplate();
|
||||
@@ -104,42 +133,87 @@ export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
}
|
||||
} else {
|
||||
templateUri = args.templatePath
|
||||
? asAbsoluteWorkspaceUri(URI.file(args.templatePath))
|
||||
: getDefaultTemplateUri();
|
||||
? asAbsoluteWorkspaceUri(args.templatePath)
|
||||
: await getDefaultTemplateUri();
|
||||
}
|
||||
|
||||
const createdNote = (await fileExists(templateUri))
|
||||
? await NoteFactory.createFromTemplate(
|
||||
templateUri,
|
||||
resolver,
|
||||
noteUri,
|
||||
text,
|
||||
args.onFileExists
|
||||
)
|
||||
: await NoteFactory.createNote(
|
||||
noteUri ?? (await getPathFromTitle(resolver)),
|
||||
text,
|
||||
resolver,
|
||||
args.onFileExists,
|
||||
args.onRelativeNotePath
|
||||
);
|
||||
// Load template using the new system
|
||||
const templateLoader = new TemplateLoader();
|
||||
let template: Template;
|
||||
|
||||
if (args.sourceLink) {
|
||||
try {
|
||||
if (!templateUri) {
|
||||
template = {
|
||||
type: 'markdown',
|
||||
content: args.text || DEFAULT_NEW_NOTE_TEXT,
|
||||
};
|
||||
} else if (await fileExists(templateUri)) {
|
||||
template = await templateLoader.loadTemplate(templateUri);
|
||||
} else {
|
||||
throw new Error(`Template file not found: ${templateUri}`);
|
||||
}
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Failed to load template (${templateUri}): ${error.message}`
|
||||
);
|
||||
}
|
||||
|
||||
// If notePath is provided, add it to template metadata to avoid unnecessary title resolution
|
||||
if (args.notePath && template.type === 'markdown') {
|
||||
template.metadata = template.metadata || new Map();
|
||||
template.metadata.set(
|
||||
'filepath',
|
||||
args.notePath instanceof URI ? args.notePath.toFsPath() : args.notePath
|
||||
);
|
||||
}
|
||||
|
||||
// Create resolver with all variables upfront
|
||||
const resolver = new Resolver(
|
||||
new Map(Object.entries(args.variables ?? {})),
|
||||
foamDate,
|
||||
args.title
|
||||
);
|
||||
|
||||
if (Logger.getLevel() === 'debug') {
|
||||
Logger.debug(`[createNote] args: ${JSON.stringify(args, null, 2)}`);
|
||||
Logger.debug(`[createNote] template: ${JSON.stringify(template, null, 2)}`);
|
||||
Logger.debug(`[createNote] resolver: ${JSON.stringify(resolver, null, 2)}`);
|
||||
}
|
||||
|
||||
// Process template using the new engine with unified resolver
|
||||
const engine = new NoteCreationEngine(
|
||||
foam,
|
||||
workspace.workspaceFolders.map(folder => fromVsCodeUri(folder.uri))
|
||||
);
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// Create the note using NoteFactory with the same resolver
|
||||
const createdNote = await NoteFactory.createNote(
|
||||
result.filepath,
|
||||
result.content,
|
||||
resolver,
|
||||
args.onFileExists,
|
||||
args.onRelativeNotePath
|
||||
);
|
||||
|
||||
// Handle source link updates for placeholders
|
||||
if (args.sourceLink && createdNote.uri) {
|
||||
const identifier = foam.workspace.getIdentifier(createdNote.uri);
|
||||
const edit = MarkdownLink.createUpdateLinkEdit(args.sourceLink.data, {
|
||||
target: identifier,
|
||||
});
|
||||
if (edit.newText !== args.sourceLink.data.rawText) {
|
||||
const updateLink = new vscode.WorkspaceEdit();
|
||||
const updateLink = new WorkspaceEdit();
|
||||
const uri = toVsCodeUri(args.sourceLink.uri);
|
||||
updateLink.replace(
|
||||
uri,
|
||||
toVsCodeRange(args.sourceLink.range),
|
||||
edit.newText
|
||||
);
|
||||
await vscode.workspace.applyEdit(updateLink);
|
||||
await workspace.applyEdit(updateLink);
|
||||
}
|
||||
}
|
||||
|
||||
return createdNote;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
/* @unit-ready */
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
|
||||
describe('open-daily-note-for-date command', () => {
|
||||
it('offers to pick which template to use', async () => {
|
||||
it('offers to pick which date to use', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
@@ -23,7 +23,7 @@ export default async function activate(
|
||||
.then(item => {
|
||||
return item?.date;
|
||||
});
|
||||
return openDailyNoteFor(date);
|
||||
return openDailyNoteFor(date, await foamPromise);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import { openDailyNoteFor } from '../../dated-notes';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', () =>
|
||||
openDailyNoteFor(new Date())
|
||||
commands.registerCommand('foam-vscode.open-daily-note', async () =>
|
||||
openDailyNoteFor(new Date(), await foamPromise)
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import {
|
||||
createDailyNoteIfNotExists,
|
||||
openDailyNoteFor,
|
||||
} from '../../dated-notes';
|
||||
|
||||
export default async function activate(context: ExtensionContext) {
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-dated-note', date => {
|
||||
commands.registerCommand('foam-vscode.open-dated-note', async date => {
|
||||
const foam = await foamPromise;
|
||||
switch (getFoamVsCodeConfig('dateSnippets.afterCompletion')) {
|
||||
case 'navigateToNote':
|
||||
return openDailyNoteFor(date);
|
||||
return openDailyNoteFor(date, foam);
|
||||
case 'createNote':
|
||||
return createDailyNoteIfNotExists(date);
|
||||
return createDailyNoteIfNotExists(date, foam);
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
@@ -3,19 +3,28 @@ import { CommandDescriptor } from '../../utils/commands';
|
||||
import { OpenResourceArgs, OPEN_COMMAND } from './open-resource';
|
||||
import * as filter from '../../core/services/resource-filter';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { closeEditors, createFile } from '../../test/test-utils-vscode';
|
||||
import {
|
||||
closeEditors,
|
||||
createFile,
|
||||
waitForNoteInFoamWorkspace,
|
||||
} from '../../test/test-utils-vscode';
|
||||
import { deleteFile } from '../../services/editor';
|
||||
import waitForExpect from 'wait-for-expect';
|
||||
|
||||
describe('open-resource command', () => {
|
||||
beforeEach(async () => {
|
||||
await jest.resetAllMocks();
|
||||
jest.resetAllMocks();
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('URI param has precedence over filter', async () => {
|
||||
const spy = jest.spyOn(filter, 'createFilter');
|
||||
const noteA = await createFile('Note A for open command');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
@@ -26,7 +35,8 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
expect(spy).not.toHaveBeenCalled();
|
||||
@@ -36,15 +46,17 @@ describe('open-resource command', () => {
|
||||
|
||||
it('URI param accept URI object, or path', async () => {
|
||||
const noteA = await createFile('Note A for open command');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
|
||||
const uriCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
uri: noteA.uri,
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(uriCommand.name, uriCommand.params);
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
|
||||
@@ -53,17 +65,18 @@ describe('open-resource command', () => {
|
||||
const pathCommand: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/file.md'),
|
||||
uri: noteA.uri.path,
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(pathCommand.name, pathCommand.params);
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(window.activeTextEditor).toBeTruthy();
|
||||
expect(window.activeTextEditor.document.uri.path).toEqual(noteA.uri.path);
|
||||
});
|
||||
await deleteFile(noteA.uri);
|
||||
});
|
||||
|
||||
it('User is notified if no resource is found', async () => {
|
||||
it('User is notified if no resource is found with filter', async () => {
|
||||
const spy = jest.spyOn(window, 'showInformationMessage');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
@@ -74,12 +87,33 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('User is notified if no resource is found with URI', async () => {
|
||||
const spy = jest.spyOn(window, 'showInformationMessage');
|
||||
|
||||
const command: CommandDescriptor<OpenResourceArgs> = {
|
||||
name: OPEN_COMMAND.command,
|
||||
params: {
|
||||
uri: URI.file('path/to/nonexistent.md'),
|
||||
},
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('filter with multiple results will show a quick pick', async () => {
|
||||
const noteA = await createFile('Note A for filter test');
|
||||
const noteB = await createFile('Note B for filter test');
|
||||
await waitForNoteInFoamWorkspace(noteA.uri);
|
||||
await waitForNoteInFoamWorkspace(noteB.uri);
|
||||
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
@@ -92,8 +126,11 @@ describe('open-resource command', () => {
|
||||
};
|
||||
await commands.executeCommand(command.name, command.params);
|
||||
|
||||
waitForExpect(() => {
|
||||
await waitForExpect(() => {
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
await deleteFile(noteA.uri);
|
||||
await deleteFile(noteB.uri);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -82,13 +82,18 @@ async function openResource(workspace: FoamWorkspace, args?: OpenResourceArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
if (isSome(item)) {
|
||||
const targetUri =
|
||||
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
if (isNone(item)) {
|
||||
vscode.window.showInformationMessage(
|
||||
'Foam: No note matches given filters or URI.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetUri =
|
||||
item.uri.path === vscode.window.activeTextEditor?.document.uri.path
|
||||
? vscode.window.activeTextEditor?.document.uri
|
||||
: toVsCodeUri(item.uri.asPlain());
|
||||
return vscode.commands.executeCommand('vscode.open', targetUri);
|
||||
}
|
||||
|
||||
interface ResourceItem extends vscode.QuickPickItem {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
workspace,
|
||||
Position,
|
||||
} from 'vscode';
|
||||
import { isMdEditor, mdDocSelector } from '../../services/editor';
|
||||
import { isMdEditor, getFoamDocSelectors } from '../../services/editor';
|
||||
import { Foam } from '../../core/model/foam';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import {
|
||||
@@ -48,7 +48,7 @@ export default async function activate(
|
||||
);
|
||||
}),
|
||||
languages.registerCodeLensProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
new WikilinkReferenceCodeLensProvider(
|
||||
foam.workspace,
|
||||
foam.services.parser
|
||||
|
||||
@@ -14,7 +14,7 @@ import { OPEN_COMMAND } from './commands/open-resource';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
import { getNoteTooltip, getFoamDocSelectors } from '../services/editor';
|
||||
import { isSome } from '../core/utils';
|
||||
|
||||
export const CONFIG_KEY = 'links.hover.enable';
|
||||
@@ -31,7 +31,7 @@ export default async function activate(
|
||||
context.subscriptions.push(
|
||||
isHoverEnabled,
|
||||
vscode.languages.registerHoverProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
new HoverProvider(
|
||||
isHoverEnabled,
|
||||
foam.workspace,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { URI } from '../core/model/uri';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { getNoteTooltip, mdDocSelector } from '../services/editor';
|
||||
import { getNoteTooltip, getFoamDocSelectors } from '../services/editor';
|
||||
|
||||
export const aliasCommitCharacters = ['#'];
|
||||
export const linkCommitCharacters = ['#', '|'];
|
||||
@@ -27,12 +27,12 @@ export default async function activate(
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
new WikilinkCompletionProvider(foam.workspace, foam.graph),
|
||||
'['
|
||||
),
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
new SectionCompletionProvider(foam.workspace),
|
||||
'#'
|
||||
),
|
||||
@@ -123,7 +123,7 @@ export class SectionCompletionProvider
|
||||
const item = new ResourceCompletionItem(
|
||||
b.label,
|
||||
vscode.CompletionItemKind.Text,
|
||||
resource.uri.withFragment(b.label)
|
||||
resource.uri.with({ fragment: b.label })
|
||||
);
|
||||
item.sortText = String(b.range.start.line).padStart(5, '0');
|
||||
item.range = replacementRange;
|
||||
|
||||
@@ -231,6 +231,10 @@ describe('Document navigation', () => {
|
||||
doc,
|
||||
new vscode.Position(0, 26)
|
||||
);
|
||||
|
||||
// Make sure the references are sorted by position, so we match the right expectation
|
||||
refs.sort((a, b) => a.range.start.character - b.range.start.character);
|
||||
|
||||
expect(refs.length).toEqual(2);
|
||||
expect(refs[0]).toEqual({
|
||||
uri: toVsCodeUri(fileB.uri),
|
||||
|
||||
@@ -10,7 +10,7 @@ import { Position } from '../core/model/position';
|
||||
import { CREATE_NOTE_COMMAND } from './commands/create-note';
|
||||
import { commandAsURI } from '../utils/commands';
|
||||
import { Location } from '../core/model/location';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
import { getFoamDocSelectors } from '../services/editor';
|
||||
|
||||
export default async function activate(
|
||||
context: vscode.ExtensionContext,
|
||||
@@ -26,15 +26,15 @@ export default async function activate(
|
||||
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerDefinitionProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerDocumentLinkProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
navigationProvider
|
||||
),
|
||||
vscode.languages.registerReferenceProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
navigationProvider
|
||||
)
|
||||
);
|
||||
@@ -157,7 +157,7 @@ export class NavigationProvider
|
||||
})
|
||||
);
|
||||
|
||||
return targets
|
||||
const links: vscode.DocumentLink[] = targets
|
||||
.filter(o => o.target.isPlaceholder()) // links to resources are managed by the definition provider
|
||||
.map(o => {
|
||||
const command = CREATE_NOTE_COMMAND.forPlaceholder(
|
||||
@@ -180,5 +180,26 @@ export class NavigationProvider
|
||||
documentLink.tooltip = `Create note for '${o.target.path}'`;
|
||||
return documentLink;
|
||||
});
|
||||
|
||||
const tags: vscode.DocumentLink[] = resource.tags.map(tag => {
|
||||
const command = {
|
||||
name: 'foam-vscode.views.tags-explorer.focus',
|
||||
params: [tag.label, documentUri],
|
||||
};
|
||||
|
||||
const documentLink = new vscode.DocumentLink(
|
||||
new vscode.Range(
|
||||
tag.range.start.line,
|
||||
tag.range.start.character,
|
||||
tag.range.end.line,
|
||||
tag.range.end.character
|
||||
),
|
||||
commandAsURI(command)
|
||||
);
|
||||
documentLink.tooltip = `Explore tag '${tag.label}'`;
|
||||
return documentLink;
|
||||
});
|
||||
|
||||
return links.concat(tags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { workspace, window } from 'vscode';
|
||||
import { createTestNote, createTestWorkspace } from '../../test/test-utils';
|
||||
import {
|
||||
|
||||
@@ -39,7 +39,7 @@ export default async function activate(
|
||||
});
|
||||
|
||||
vscode.window.onDidChangeActiveTextEditor(e => {
|
||||
if (e?.document?.uri?.scheme === 'file') {
|
||||
if (e?.document?.uri?.scheme !== 'untitled') {
|
||||
const note = foam.workspace.get(fromVsCodeUri(e.document.uri));
|
||||
if (isSome(note)) {
|
||||
panel.webview.postMessage({
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../../test/test-utils-vscode';
|
||||
import { TagItem, TagsProvider } from './tags-explorer';
|
||||
|
||||
@@ -54,6 +54,51 @@ export default async function activate(
|
||||
provider,
|
||||
node => node.contextValue === 'tag' || node.contextValue === 'folder'
|
||||
)
|
||||
),
|
||||
vscode.commands.registerCommand(
|
||||
`foam-vscode.views.${provider.providerId}.focus`,
|
||||
async (tag?: string, source?: object) => {
|
||||
if (tag == null) {
|
||||
tag = await vscode.window.showQuickPick(
|
||||
Array.from(foam.tags.tags.keys()),
|
||||
{
|
||||
title: 'Select a tag to focus',
|
||||
}
|
||||
);
|
||||
}
|
||||
if (tag == null) {
|
||||
return;
|
||||
}
|
||||
const tagItem = (await provider.findTreeItemByPath(
|
||||
provider.valueToPath(tag)
|
||||
)) as TagItem;
|
||||
if (tagItem == null) {
|
||||
return;
|
||||
}
|
||||
await treeView.reveal(tagItem, {
|
||||
select: true,
|
||||
focus: true,
|
||||
expand: true,
|
||||
});
|
||||
const children = await provider.getChildren(tagItem);
|
||||
const sourceUri = source ? new URI(source) : undefined;
|
||||
const resourceItem = sourceUri
|
||||
? children.find(
|
||||
t =>
|
||||
t instanceof ResourceTreeItem &&
|
||||
sourceUri.isEqual(t.resource?.uri)
|
||||
)
|
||||
: undefined;
|
||||
// doing it as a two reveal process as revealing just the resource
|
||||
// was only working when the tag item was already expanded
|
||||
if (resourceItem) {
|
||||
treeView.reveal(resourceItem, {
|
||||
select: true,
|
||||
focus: true,
|
||||
expand: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ export interface Folder<T> {
|
||||
export class FolderTreeItem<T> extends vscode.TreeItem {
|
||||
collapsibleState = vscode.TreeItemCollapsibleState.Collapsed;
|
||||
contextValue = 'folder';
|
||||
iconPath = new vscode.ThemeIcon('folder');
|
||||
|
||||
constructor(
|
||||
public node: Folder<T>,
|
||||
|
||||
@@ -43,6 +43,9 @@ export class UriTreeItem extends BaseTreeItem {
|
||||
}
|
||||
|
||||
export class ResourceTreeItem extends UriTreeItem {
|
||||
iconPath = vscode.ThemeIcon.File;
|
||||
contextValue = 'foam.resource';
|
||||
|
||||
constructor(
|
||||
public readonly resource: Resource,
|
||||
private readonly workspace: FoamWorkspace,
|
||||
@@ -62,8 +65,6 @@ export class ResourceTreeItem extends UriTreeItem {
|
||||
title: 'Go to location',
|
||||
};
|
||||
this.resourceUri = toVsCodeUri(resource.uri);
|
||||
this.iconPath = vscode.ThemeIcon.File;
|
||||
this.contextValue = 'foam.resource';
|
||||
}
|
||||
|
||||
async resolveTreeItem(): Promise<ResourceTreeItem> {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { default as markdownItFoamTags } from './tag-highlight';
|
||||
|
||||
@@ -4,6 +4,7 @@ import markdownItRegex from 'markdown-it-regex';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
import { isNone } from '../../core/utils';
|
||||
import { commandAsURI } from '../../utils/commands';
|
||||
|
||||
export const markdownItFoamTags = (
|
||||
md: markdownit,
|
||||
@@ -14,10 +15,7 @@ export const markdownItFoamTags = (
|
||||
regex: /(?<=^|\s)(#[0-9]*[\p{L}/_-][\p{L}\p{N}/_-]*)/u,
|
||||
replace: (tag: string) => {
|
||||
try {
|
||||
const resource = workspace.find(tag);
|
||||
if (isNone(resource)) {
|
||||
return getFoamTag(tag);
|
||||
}
|
||||
return getFoamTag(tag);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for ${tag} in Preview panel`,
|
||||
@@ -29,6 +27,8 @@ export const markdownItFoamTags = (
|
||||
});
|
||||
};
|
||||
|
||||
// Commands can't be run in the preview (see https://github.com/microsoft/vscode/issues/102532)
|
||||
// for we just return the tag as a span
|
||||
const getFoamTag = (content: string) =>
|
||||
`<span class='foam-tag'>${content}</span>`;
|
||||
|
||||
|
||||
@@ -17,9 +17,10 @@ export const markdownItWikilinkEmbed = (
|
||||
regex: WIKILINK_EMBED_REGEX,
|
||||
replace: (wikilinkItem: string) => {
|
||||
return `
|
||||
<div style="padding: 0.25em; margin: 1.5em 0; text-align: center; border: 1px solid var(--vscode-editorLineNumber-foreground);">
|
||||
Embeds are not supported in web extension: <br/> ${wikilinkItem}
|
||||
</div>`;
|
||||
<div class="foam-embed-not-supported-warning">
|
||||
Embed not supported in web mode: ${wikilinkItem}
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { createMarkdownParser } from '../../core/services/markdown-parser';
|
||||
@@ -385,7 +386,7 @@ content-card![[note-e#Section 2]]`);
|
||||
);
|
||||
});
|
||||
|
||||
it('should display a warning in case of cyclical inclusions', async () => {
|
||||
it.skip('should display a warning in case of cyclical inclusions', async () => {
|
||||
const noteA = await createFile(
|
||||
'This is the text of note A which includes ![[note-b]]',
|
||||
['preview', 'note-a.md']
|
||||
|
||||
@@ -10,9 +10,14 @@ import { Resource, ResourceParser } from '../../core/model/note';
|
||||
import { getFoamVsCodeConfig } from '../../services/config';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../../utils/vsc-utils';
|
||||
import { MarkdownLink } from '../../core/services/markdown-link';
|
||||
import { URI } from '../../core/model/uri';
|
||||
import { Position } from '../../core/model/position';
|
||||
import { TextEdit } from '../../core/services/text-edit';
|
||||
import { isNone, isSome } from '../../core/utils';
|
||||
import {
|
||||
asAbsoluteWorkspaceUri,
|
||||
isVirtualWorkspace,
|
||||
} from '../../services/editor';
|
||||
|
||||
export const WIKILINK_EMBED_REGEX =
|
||||
/((?:(?:full|content)-(?:inline|card)|full|content|inline|card)?!\[\[[^[\]]+?\]\])/;
|
||||
@@ -22,7 +27,7 @@ export const WIKILINK_EMBED_REGEX =
|
||||
export const WIKILINK_EMBED_REGEX_GROUPS =
|
||||
/((?:\w+)|(?:(?:\w+)-(?:\w+)))?!\[\[([^[\]]+?)\]\]/;
|
||||
export const CONFIG_EMBED_NOTE_TYPE = 'preview.embedNoteType';
|
||||
const refsStack: string[] = [];
|
||||
let refsStack: string[] = [];
|
||||
|
||||
export const markdownItWikilinkEmbed = (
|
||||
md: markdownit,
|
||||
@@ -38,6 +43,14 @@ export const markdownItWikilinkEmbed = (
|
||||
WIKILINK_EMBED_REGEX_GROUPS
|
||||
);
|
||||
|
||||
if (isVirtualWorkspace()) {
|
||||
return `
|
||||
<div class="foam-embed-not-supported-warning">
|
||||
Embed not supported in virtual workspace: ![[${wikilink}]]
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const includedNote = workspace.find(wikilink);
|
||||
|
||||
if (!includedNote) {
|
||||
@@ -48,56 +61,31 @@ export const markdownItWikilinkEmbed = (
|
||||
includedNote.uri.path.toLocaleLowerCase()
|
||||
);
|
||||
|
||||
if (!cyclicLinkDetected) {
|
||||
refsStack.push(includedNote.uri.path.toLocaleLowerCase());
|
||||
}
|
||||
|
||||
if (cyclicLinkDetected) {
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
return `
|
||||
<div class="foam-cyclic-link-warning">
|
||||
Cyclic link detected for wikilink: ${wikilink}
|
||||
<div class="foam-cyclic-link-warning__stack">
|
||||
Link sequence:
|
||||
<ul>
|
||||
${refsStack.map(ref => `<li>${ref}</li>`).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
let html: string;
|
||||
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
const { noteScope, noteStyle } =
|
||||
retrieveNoteConfig(noteEmbedModifier);
|
||||
refsStack.push(includedNote.uri.path.toLocaleLowerCase());
|
||||
|
||||
const extractor: EmbedNoteExtractor =
|
||||
noteScope === 'full'
|
||||
? fullExtractor
|
||||
: noteScope === 'content'
|
||||
? contentExtractor
|
||||
: fullExtractor;
|
||||
|
||||
const formatter: EmbedNoteFormatter =
|
||||
noteStyle === 'card'
|
||||
? cardFormatter
|
||||
: noteStyle === 'inline'
|
||||
? inlineFormatter
|
||||
: cardFormatter;
|
||||
|
||||
content = extractor(includedNote, parser, workspace);
|
||||
html = formatter(content, md);
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
content = `
|
||||
<div class="embed-container-attachment">
|
||||
${md.renderInline('[[' + wikilink + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`})`
|
||||
)}</div>`;
|
||||
html = md.render(content);
|
||||
break;
|
||||
}
|
||||
const content = getNoteContent(
|
||||
includedNote,
|
||||
noteEmbedModifier,
|
||||
parser,
|
||||
workspace,
|
||||
md
|
||||
);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
return refsStack.length === 0 ? md.render(content) : content;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including ${wikilinkItem} into the current document of the Preview panel`,
|
||||
@@ -109,11 +97,65 @@ Embed for attachments is not supported
|
||||
});
|
||||
};
|
||||
|
||||
function getNoteContent(
|
||||
includedNote: Resource,
|
||||
noteEmbedModifier: string | undefined,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace,
|
||||
md: markdownit
|
||||
): string {
|
||||
let content = `Embed for [[${includedNote.uri.path}]]`;
|
||||
let toRender: string;
|
||||
|
||||
switch (includedNote.type) {
|
||||
case 'note': {
|
||||
const { noteScope, noteStyle } = retrieveNoteConfig(noteEmbedModifier);
|
||||
|
||||
const extractor: EmbedNoteExtractor =
|
||||
noteScope === 'full'
|
||||
? fullExtractor
|
||||
: noteScope === 'content'
|
||||
? contentExtractor
|
||||
: fullExtractor;
|
||||
|
||||
const formatter: EmbedNoteFormatter =
|
||||
noteStyle === 'card'
|
||||
? cardFormatter
|
||||
: noteStyle === 'inline'
|
||||
? inlineFormatter
|
||||
: cardFormatter;
|
||||
|
||||
content = extractor(includedNote, parser, workspace);
|
||||
toRender = formatter(content, md);
|
||||
break;
|
||||
}
|
||||
case 'attachment':
|
||||
content = `
|
||||
<div class="embed-container-attachment">
|
||||
${md.renderInline('[[' + includedNote.uri.path + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
toRender = md.render(content);
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`})`
|
||||
)}</div>`;
|
||||
toRender = md.render(content);
|
||||
break;
|
||||
default:
|
||||
toRender = content;
|
||||
}
|
||||
|
||||
return toRender;
|
||||
}
|
||||
|
||||
function withLinksRelativeToWorkspaceRoot(
|
||||
noteUri: URI,
|
||||
noteText: string,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) {
|
||||
): string {
|
||||
const note = parser.parse(
|
||||
fromVsCodeUri(vsWorkspace.workspaceFolders[0].uri),
|
||||
noteText
|
||||
@@ -121,15 +163,13 @@ function withLinksRelativeToWorkspaceRoot(
|
||||
const edits = note.links
|
||||
.map(link => {
|
||||
const info = MarkdownLink.analyzeLink(link);
|
||||
const resource = workspace.find(info.target);
|
||||
const resource = workspace.find(info.target, noteUri);
|
||||
// embedded notes that aren't created are still collected
|
||||
// return null so it can be filtered in the next step
|
||||
if (isNone(resource)) {
|
||||
return null;
|
||||
}
|
||||
const pathFromRoot = vsWorkspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
);
|
||||
const pathFromRoot = asAbsoluteWorkspaceUri(resource.uri).path;
|
||||
return MarkdownLink.createUpdateLinkEdit(link, {
|
||||
target: pathFromRoot,
|
||||
});
|
||||
@@ -185,7 +225,12 @@ function fullExtractor(
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
noteText = withLinksRelativeToWorkspaceRoot(
|
||||
note.uri,
|
||||
noteText,
|
||||
parser,
|
||||
workspace
|
||||
);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
@@ -211,7 +256,12 @@ function contentExtractor(
|
||||
}
|
||||
rows.shift();
|
||||
noteText = rows.join('\n');
|
||||
noteText = withLinksRelativeToWorkspaceRoot(noteText, parser, workspace);
|
||||
noteText = withLinksRelativeToWorkspaceRoot(
|
||||
note.uri,
|
||||
noteText,
|
||||
parser,
|
||||
workspace
|
||||
);
|
||||
return noteText;
|
||||
}
|
||||
|
||||
@@ -221,13 +271,11 @@ function contentExtractor(
|
||||
export type EmbedNoteFormatter = (content: string, md: markdownit) => string;
|
||||
|
||||
function cardFormatter(content: string, md: markdownit): string {
|
||||
return md.render(
|
||||
`<div class="embed-container-note">${md.render(content)}</div>`
|
||||
);
|
||||
return `<div class="embed-container-note">\n\n${content}\n\n</div>`;
|
||||
}
|
||||
|
||||
function inlineFormatter(content: string, md: markdownit): string {
|
||||
return md.render(content);
|
||||
return content;
|
||||
}
|
||||
|
||||
export default markdownItWikilinkEmbed;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import MarkdownIt from 'markdown-it';
|
||||
import { FoamWorkspace } from '../../core/model/workspace';
|
||||
import { createTestNote } from '../../test/test-utils';
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as vscode from 'vscode';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { FoamTags } from '../core/model/tags';
|
||||
import { isInFrontMatter, isOnYAMLKeywordLine } from '../core/utils/md';
|
||||
import { mdDocSelector } from '../services/editor';
|
||||
import { getFoamDocSelectors } from '../services/editor';
|
||||
|
||||
// this regex is different from HASHTAG_REGEX in that it does not look for a
|
||||
// #+character. It uses a negative look-ahead for `# `
|
||||
@@ -17,7 +17,7 @@ export default async function activate(
|
||||
const foam = await foamPromise;
|
||||
context.subscriptions.push(
|
||||
vscode.languages.registerCompletionItemProvider(
|
||||
mdDocSelector,
|
||||
getFoamDocSelectors(),
|
||||
new TagCompletionProvider(foam.tags),
|
||||
'#'
|
||||
)
|
||||
|
||||
@@ -18,23 +18,27 @@ describe('Editor utils', () => {
|
||||
|
||||
describe('getCurrentEditorDirectory', () => {
|
||||
it('should return the directory of the active text editor', async () => {
|
||||
const file = await createFile('this is the file content.');
|
||||
const file = await createFile('this is the file content.', [
|
||||
'editor-utils',
|
||||
'file.md',
|
||||
]);
|
||||
await showInEditor(file.uri);
|
||||
|
||||
expect(getCurrentEditorDirectory()).toEqual(file.uri.getDirectory());
|
||||
});
|
||||
|
||||
it('should return the directory of the workspace folder if no editor is open', async () => {
|
||||
it('should throw if no editor is open', async () => {
|
||||
await closeEditors();
|
||||
expect(getCurrentEditorDirectory()).toEqual(
|
||||
fromVsCodeUri(workspace.workspaceFolders[0].uri)
|
||||
);
|
||||
expect(() => getCurrentEditorDirectory()).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('replaceSelection', () => {
|
||||
it('should replace the selection in the active editor', async () => {
|
||||
const fileA = await createFile('This is the file A');
|
||||
const fileA = await createFile('This is the file A', [
|
||||
'replace-selection',
|
||||
'file.md',
|
||||
]);
|
||||
const doc = await showInEditor(fileA.uri);
|
||||
const selection = new Selection(0, 5, 0, 7); // 'is'
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getExcerpt, stripFrontMatter, stripImages } from '../core/utils/md';
|
||||
import { isSome } from '../core/utils/core';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { getFoamVsCodeConfig } from './config';
|
||||
import {
|
||||
AlwaysIncludeMatcher,
|
||||
FileListBasedMatcher,
|
||||
@@ -52,13 +53,38 @@ export function formatMarkdownTooltip(content: string): MarkdownString {
|
||||
return md;
|
||||
}
|
||||
|
||||
export const mdDocSelector = [
|
||||
{ language: 'markdown', scheme: 'file' },
|
||||
{ language: 'markdown', scheme: 'untitled' },
|
||||
];
|
||||
// Generate the document selector dynamically
|
||||
export const getFoamDocSelectors = () =>
|
||||
getFoamVsCodeConfig<string[]>('supportedLanguages', ['markdown']).flatMap(
|
||||
lang => [
|
||||
{ language: lang, scheme: 'file' }, // Local files
|
||||
{ language: lang, scheme: 'vscode-vfs' }, // Remote files
|
||||
{ language: lang, scheme: 'untitled' }, // Untitled files
|
||||
]
|
||||
);
|
||||
|
||||
export function isMdEditor(editor: TextEditor) {
|
||||
return editor && editor.document && editor.document.languageId === 'markdown';
|
||||
// Check if the editor's document is a supported language
|
||||
export function isMdEditor(editor: TextEditor): boolean {
|
||||
const supportedLanguages = getFoamVsCodeConfig<string[]>(
|
||||
'supportedLanguages',
|
||||
['markdown']
|
||||
);
|
||||
return (
|
||||
editor &&
|
||||
editor.document &&
|
||||
supportedLanguages.includes(editor.document.languageId)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the workspace contains remote or virtual file system folders.
|
||||
* @returns True if the workspace contains remote or virtual file system folders, false otherwise.
|
||||
*/
|
||||
export function isVirtualWorkspace(): boolean {
|
||||
return workspace.workspaceFolders.some(folder => {
|
||||
const scheme = folder.uri.scheme;
|
||||
return scheme === 'vscode-remote' || scheme === 'vscode-vfs';
|
||||
});
|
||||
}
|
||||
|
||||
export function findSelectionContent(): SelectionInfo | undefined {
|
||||
@@ -134,12 +160,10 @@ export function getEditorEOL(): string {
|
||||
|
||||
/**
|
||||
* Returns the directory of the file currently open in the editor.
|
||||
* If no file is open in the editor it will return the first folder
|
||||
* in the workspace.
|
||||
* If both aren't available it will throw.
|
||||
* If no file is open in the editor it will throw.
|
||||
*
|
||||
* @returns URI
|
||||
* @throws Error if no file is open in editor AND no workspace folder defined
|
||||
* @throws Error if no file is open in editor
|
||||
*/
|
||||
export function getCurrentEditorDirectory(): URI {
|
||||
const uri = window.activeTextEditor?.document?.uri;
|
||||
@@ -148,11 +172,7 @@ export function getCurrentEditorDirectory(): URI {
|
||||
return fromVsCodeUri(uri).getDirectory();
|
||||
}
|
||||
|
||||
if (workspace.workspaceFolders.length > 0) {
|
||||
return fromVsCodeUri(workspace.workspaceFolders[0].uri);
|
||||
}
|
||||
|
||||
throw new Error('A file must be open in editor, or workspace folder needed');
|
||||
throw new Error('No editor open');
|
||||
}
|
||||
|
||||
export async function fileExists(uri: URI): Promise<boolean> {
|
||||
@@ -168,7 +188,7 @@ export async function readFile(uri: URI): Promise<string | undefined> {
|
||||
if (await fileExists(uri)) {
|
||||
return workspace.fs
|
||||
.readFile(toVsCodeUri(uri))
|
||||
.then(bytes => bytes.toString());
|
||||
.then(bytes => new TextDecoder('utf-8').decode(bytes));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -179,17 +199,22 @@ export function deleteFile(uri: URI) {
|
||||
|
||||
/**
|
||||
* Turns a relative URI into an absolute URI for the given workspace.
|
||||
* @param uri the uri to evaluate
|
||||
* @param uriOrPath the uri or path to evaluate
|
||||
* @param forceSubfolder if true, if the URI is absolute and not a subfolder in the workspace,
|
||||
* it will be forced to be a subfolder of the first workspace folder
|
||||
* @returns an absolute uri
|
||||
*/
|
||||
export function asAbsoluteWorkspaceUri(uri: URI): URI {
|
||||
export function asAbsoluteWorkspaceUri(
|
||||
uriOrPath: URI | string,
|
||||
forceSubfolder = false
|
||||
): URI {
|
||||
if (workspace.workspaceFolders === undefined) {
|
||||
throw new Error('An open folder or workspace is required');
|
||||
}
|
||||
const folders = workspace.workspaceFolders.map(folder =>
|
||||
fromVsCodeUri(folder.uri)
|
||||
);
|
||||
const res = asAbsoluteUri(uri, folders);
|
||||
const res = asAbsoluteUri(uriOrPath, folders, forceSubfolder);
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -219,9 +244,9 @@ export async function createMatcherAndDataStore(excludes: string[]): Promise<{
|
||||
let files: Uri[] = [];
|
||||
for (const folder of workspace.workspaceFolders) {
|
||||
const uris = await workspace.findFiles(
|
||||
new RelativePattern(folder.uri.path, '**/*'),
|
||||
new RelativePattern(folder.uri, '**/*'),
|
||||
new RelativePattern(
|
||||
folder.uri.path,
|
||||
folder.uri,
|
||||
`{${excludePatterns.get(folder.name).join(',')}}`
|
||||
)
|
||||
);
|
||||
|
||||
206
packages/foam-vscode/src/services/js-template-loader.ts
Normal file
206
packages/foam-vscode/src/services/js-template-loader.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import * as vm from 'vm';
|
||||
import { readFile } from './editor';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { CreateNoteFunction, TemplateContext } from './note-creation-types';
|
||||
import { createTemplateSandbox, BLOCKED_GLOBALS } from './js-template-sandbox';
|
||||
import { Logger } from '../core/utils/log';
|
||||
|
||||
/**
|
||||
* Error thrown when there are issues loading or executing JavaScript templates
|
||||
*/
|
||||
export class JSTemplateError extends Error {
|
||||
constructor(message: string, public readonly templatePath: string) {
|
||||
super(`JavaScript template error in ${templatePath}: ${message}`);
|
||||
this.name = 'JSTemplateError';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loader for JavaScript template functions with secure VM execution
|
||||
*/
|
||||
export class JSTemplateLoader {
|
||||
private static readonly EXECUTION_TIMEOUT = 10000; // 10 seconds
|
||||
private static readonly VM_OPTIONS: vm.RunningScriptOptions = {
|
||||
timeout: JSTemplateLoader.EXECUTION_TIMEOUT,
|
||||
displayErrors: true,
|
||||
};
|
||||
|
||||
/**
|
||||
* Loads and returns a note creation function from a JavaScript template file
|
||||
*
|
||||
* @param template Path to the JavaScript template file
|
||||
* @returns The createNote function from the template
|
||||
*/
|
||||
async loadFunction(template: URI): Promise<CreateNoteFunction> {
|
||||
try {
|
||||
Logger.info(`Loading JavaScript template: ${template.path}`);
|
||||
|
||||
const templateCode = await readFile(template);
|
||||
|
||||
if (!templateCode) {
|
||||
throw new JSTemplateError(
|
||||
`Template file not found or empty`,
|
||||
template.path
|
||||
);
|
||||
}
|
||||
|
||||
return this.createFunctionFromCode(templateCode, template);
|
||||
} catch (error) {
|
||||
if (error instanceof JSTemplateError) {
|
||||
throw error;
|
||||
}
|
||||
throw new JSTemplateError(
|
||||
`Failed to load template: ${error.message}`,
|
||||
template.path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a note creation function from JavaScript code
|
||||
*
|
||||
* @param code The JavaScript code containing the createNote function
|
||||
* @param template Path for error reporting
|
||||
* @returns The createNote function
|
||||
*/
|
||||
private createFunctionFromCode(
|
||||
code: string,
|
||||
template: URI
|
||||
): CreateNoteFunction {
|
||||
try {
|
||||
// Validate the code structure
|
||||
this.validateTemplateCode(code, template);
|
||||
|
||||
// Create the VM context with sandbox
|
||||
const sandbox = this.createVMSandbox();
|
||||
const context = vm.createContext(sandbox);
|
||||
|
||||
// Execute the template code in the sandbox
|
||||
const script = new vm.Script(code, {
|
||||
filename: template.toFsPath(),
|
||||
lineOffset: 0,
|
||||
columnOffset: 0,
|
||||
});
|
||||
|
||||
script.runInContext(context, JSTemplateLoader.VM_OPTIONS);
|
||||
|
||||
// Extract the createNote function
|
||||
const createNote = context.createNote;
|
||||
if (typeof createNote !== 'function') {
|
||||
throw new JSTemplateError(
|
||||
'Template must declare a createNote function',
|
||||
template.path
|
||||
);
|
||||
}
|
||||
|
||||
// Wrap the function to inject the sandbox context
|
||||
return async (noteContext: TemplateContext) => {
|
||||
try {
|
||||
// Update the sandbox with the current context
|
||||
const contextSandbox = createTemplateSandbox(noteContext);
|
||||
Object.assign(context, contextSandbox);
|
||||
|
||||
// Execute the template function
|
||||
const result = await createNote(noteContext);
|
||||
|
||||
// Validate the result
|
||||
this.validateResult(result, template);
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
if (error instanceof JSTemplateError) {
|
||||
throw error;
|
||||
}
|
||||
throw new JSTemplateError(
|
||||
`Template execution failed: ${error.message}`,
|
||||
template.path
|
||||
);
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof JSTemplateError) {
|
||||
throw error;
|
||||
}
|
||||
throw new JSTemplateError(
|
||||
`Failed to create function: ${error.message}`,
|
||||
template.path
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a secure VM sandbox with limited globals
|
||||
*/
|
||||
private createVMSandbox() {
|
||||
const sandbox: Record<string, any> = {};
|
||||
|
||||
// Block dangerous globals
|
||||
BLOCKED_GLOBALS.forEach(globalName => {
|
||||
sandbox[globalName] = undefined;
|
||||
});
|
||||
|
||||
return sandbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that the template code has the expected structure
|
||||
*/
|
||||
private validateTemplateCode(code: string, template: URI): void {
|
||||
// Check for createNote function
|
||||
if (
|
||||
!code.includes('function createNote') &&
|
||||
!code.includes('createNote =')
|
||||
) {
|
||||
throw new JSTemplateError(
|
||||
'Template must define a createNote function',
|
||||
template.path
|
||||
);
|
||||
}
|
||||
|
||||
// Check for potentially dangerous patterns
|
||||
const dangerousPatterns = [
|
||||
/require\s*\(/,
|
||||
/import\s+/,
|
||||
/eval\s*\(/,
|
||||
/Function\s*\(/,
|
||||
/process\./,
|
||||
/__dirname/,
|
||||
/__filename/,
|
||||
];
|
||||
|
||||
for (const pattern of dangerousPatterns) {
|
||||
if (pattern.test(code)) {
|
||||
throw new JSTemplateError(
|
||||
`Template contains potentially unsafe code: ${pattern.source}`,
|
||||
template.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the result returned by a template function
|
||||
*/
|
||||
private validateResult(result: any, template: URI): void {
|
||||
if (!result || typeof result !== 'object') {
|
||||
throw new JSTemplateError(
|
||||
'Template must return an object with filepath and content properties',
|
||||
template.path
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof result.filepath !== 'string' || !result.filepath.trim()) {
|
||||
throw new JSTemplateError(
|
||||
'Template result must have a non-empty filepath string',
|
||||
template.path
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof result.content !== 'string') {
|
||||
throw new JSTemplateError(
|
||||
'Template result must have a content string',
|
||||
template.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
63
packages/foam-vscode/src/services/js-template-sandbox.ts
Normal file
63
packages/foam-vscode/src/services/js-template-sandbox.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import { TemplateContext } from './note-creation-types';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { toSlug } from '../utils/slug';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
/**
|
||||
* Creates a sandbox environment for JavaScript template execution
|
||||
* This provides utility functions and safe globals for template functions
|
||||
*/
|
||||
export function createTemplateSandbox(context: TemplateContext) {
|
||||
return {
|
||||
// Common JavaScript globals (safe subset)
|
||||
Date,
|
||||
Math,
|
||||
Object,
|
||||
Array,
|
||||
String,
|
||||
Number,
|
||||
Boolean,
|
||||
JSON,
|
||||
RegExp,
|
||||
Error,
|
||||
|
||||
// Console for debugging (logs to Foam output channel)
|
||||
console: {
|
||||
log: (...args: any[]) =>
|
||||
Logger.info(`[Template] ${args[0]}`, ...args.slice(1)),
|
||||
warn: (...args: any[]) =>
|
||||
Logger.warn(`[Template] ${args[0]}`, ...args.slice(1)),
|
||||
error: (...args: any[]) =>
|
||||
Logger.error(`[Template] ${args[0]}`, ...args.slice(1)),
|
||||
},
|
||||
|
||||
// Utility functions
|
||||
dayjs,
|
||||
slugify: toSlug,
|
||||
URI,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* List of globals that should NOT be available in the template sandbox
|
||||
* for security reasons
|
||||
*/
|
||||
export const BLOCKED_GLOBALS = [
|
||||
'require',
|
||||
'module',
|
||||
'exports',
|
||||
'__dirname',
|
||||
'__filename',
|
||||
'global',
|
||||
'process',
|
||||
'Buffer',
|
||||
'setImmediate',
|
||||
'clearImmediate',
|
||||
'setInterval',
|
||||
'clearInterval',
|
||||
'setTimeout',
|
||||
'clearTimeout',
|
||||
'eval',
|
||||
'Function',
|
||||
];
|
||||
473
packages/foam-vscode/src/services/note-creation-engine.test.ts
Normal file
473
packages/foam-vscode/src/services/note-creation-engine.test.ts
Normal file
@@ -0,0 +1,473 @@
|
||||
import { tmpdir } from 'os';
|
||||
import { mkdtempSync } from 'fs';
|
||||
import { NoteCreationEngine } from './note-creation-engine';
|
||||
import { TriggerFactory } from './note-creation-triggers';
|
||||
import {
|
||||
Template,
|
||||
isCommandTrigger,
|
||||
isPlaceholderTrigger,
|
||||
} from './note-creation-types';
|
||||
import { readFileFromFs, strToUri } from '../test/test-utils';
|
||||
import { bootstrap } from '../core/model/foam';
|
||||
import { FileDataStore, Matcher } from '../test/test-datastore';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
Logger.setLevel('off');
|
||||
|
||||
async function setupFoamEngine() {
|
||||
// Set up Foam workspace (minimal setup for testing)
|
||||
const tmpDir = mkdtempSync(`${tmpdir()}/foam-test-`);
|
||||
const dataStore = new FileDataStore(readFileFromFs, tmpDir);
|
||||
const matcher = new Matcher([strToUri(tmpDir)], ['**/*.md']);
|
||||
const parser = createMarkdownParser();
|
||||
const provider = new MarkdownResourceProvider(dataStore, parser, ['.md']);
|
||||
const foam = await bootstrap(matcher, undefined, dataStore, parser, [
|
||||
provider,
|
||||
]);
|
||||
const engine = new NoteCreationEngine(foam, [strToUri(tmpDir)]);
|
||||
return { foam, engine };
|
||||
}
|
||||
|
||||
describe('NoteCreationEngine', () => {
|
||||
describe('processTemplate', () => {
|
||||
it('should process markdown templates correctly', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
// Create markdown template
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `---
|
||||
filepath: test-note.md
|
||||
---
|
||||
# \${FOAM_TITLE}
|
||||
|
||||
Test content with title: \${FOAM_TITLE}`,
|
||||
metadata: new Map([['filepath', 'test-note.md']]),
|
||||
};
|
||||
|
||||
// Create trigger
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Create resolver with variables
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'Test Note');
|
||||
|
||||
// Test processing
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
expect(result.filepath.path).toBe('test-note.md');
|
||||
expect(result.content).toContain('# Test Note');
|
||||
expect(result.content).toContain('Test content with title: Test Note');
|
||||
});
|
||||
|
||||
it('should handle command triggers with date parameters', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
// Create markdown template with date variables
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `# Daily Note \${FOAM_DATE_YEAR}-\${FOAM_DATE_MONTH}-\${FOAM_DATE_DATE}
|
||||
|
||||
Today is \${FOAM_DATE_DAY_NAME}`,
|
||||
};
|
||||
|
||||
// Create context with date trigger
|
||||
const testDate = new Date('2024-01-15');
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.open-daily-note',
|
||||
{
|
||||
date: testDate,
|
||||
}
|
||||
);
|
||||
|
||||
// Create resolver with date variables
|
||||
const resolver = new Resolver(new Map(), testDate);
|
||||
resolver.define('FOAM_TITLE', '2024-01-15');
|
||||
resolver.define('FOAM_DATE_YEAR', '2024');
|
||||
resolver.define('FOAM_DATE_MONTH', '01');
|
||||
resolver.define('FOAM_DATE_DATE', '15');
|
||||
resolver.define('FOAM_DATE_DAY_NAME', 'Monday');
|
||||
|
||||
// Test processing with date variables
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
expect(result.content).toContain('Daily Note 2024-01-15');
|
||||
expect(result.content).toContain('Today is Monday');
|
||||
|
||||
// Verify trigger type handling
|
||||
expect(trigger.type).toBe('command');
|
||||
if (!isCommandTrigger(trigger)) {
|
||||
throw new Error('Expected command trigger type');
|
||||
}
|
||||
expect(trigger.command).toBe('foam-vscode.open-daily-note');
|
||||
expect(trigger.params).toHaveProperty('date');
|
||||
});
|
||||
|
||||
it('should handle placeholder triggers correctly', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
// Create markdown template
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `# \${FOAM_TITLE}
|
||||
|
||||
Created from placeholder link.
|
||||
|
||||
Content goes here.`,
|
||||
};
|
||||
|
||||
// Create placeholder trigger
|
||||
const trigger = TriggerFactory.createPlaceholderTrigger(
|
||||
strToUri('/test/source.md'),
|
||||
'Source Note',
|
||||
{
|
||||
uri: strToUri('/test/source.md'),
|
||||
range: {
|
||||
start: { line: 0, character: 0 },
|
||||
end: { line: 0, character: 10 },
|
||||
},
|
||||
data: { rawText: '[[Test Note]]' },
|
||||
} as any
|
||||
);
|
||||
|
||||
// Create resolver with variables
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'Test Note');
|
||||
|
||||
// Test processing
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
expect(result.content).toContain('# Test Note');
|
||||
expect(result.content).toContain('Created from placeholder link');
|
||||
|
||||
// Verify trigger type handling
|
||||
expect(trigger.type).toBe('placeholder');
|
||||
if (!isPlaceholderTrigger(trigger)) {
|
||||
throw new Error('Expected placeholder trigger type');
|
||||
}
|
||||
expect(trigger.sourceNote.title).toBe('Source Note');
|
||||
expect(trigger.sourceNote.uri).toBe(
|
||||
strToUri('/test/source.md').toString()
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate default filepath when not specified in template', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
// Create markdown template without filepath metadata
|
||||
const template: Template = {
|
||||
type: 'markdown',
|
||||
content: `# \${FOAM_TITLE}
|
||||
|
||||
Content without filepath metadata.`,
|
||||
};
|
||||
|
||||
// Create resolver with variables
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'My New Note');
|
||||
resolver.define('title', 'My New Note');
|
||||
|
||||
// Test processing
|
||||
const result = await engine.processTemplate(
|
||||
TriggerFactory.createCommandTrigger('foam-vscode.create-note'),
|
||||
template,
|
||||
resolver
|
||||
);
|
||||
|
||||
expect(result.content).toContain('# My New Note');
|
||||
expect(result.filepath.path).toBe('My New Note.md'); // Should generate from title
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates correctly', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
// Create JavaScript template
|
||||
const template: Template = {
|
||||
type: 'javascript',
|
||||
createNote: async context => {
|
||||
const title =
|
||||
(await context.resolver.resolveFromName('FOAM_TITLE')) ||
|
||||
'Untitled';
|
||||
const content = `# ${title}\n\nGenerated by JavaScript template\n\nTrigger: ${context.trigger.type}`;
|
||||
return {
|
||||
filepath: URI.parse(
|
||||
`${title.replace(/\s+/g, '-').toLowerCase()}.md`
|
||||
),
|
||||
content,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// Create resolver with variables
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
resolver.define('FOAM_TITLE', 'JS Generated Note');
|
||||
resolver.define('title', 'JS Generated Note');
|
||||
|
||||
// Test processing
|
||||
const result = await engine.processTemplate(
|
||||
TriggerFactory.createCommandTrigger('foam-vscode.create-note'),
|
||||
template,
|
||||
resolver
|
||||
);
|
||||
|
||||
expect(result.content).toContain('# JS Generated Note');
|
||||
expect(result.content).toContain('Generated by JavaScript template');
|
||||
expect(result.content).toContain('Trigger: command');
|
||||
expect(result.filepath.path).toBe('js-generated-note.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JavaScript template error handling', () => {
|
||||
it('should handle synchronous errors thrown by JavaScript templates', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that throws synchronously
|
||||
const template: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () => {
|
||||
throw new Error('Template execution failed');
|
||||
},
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that error is properly caught and handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, template, resolver)
|
||||
).rejects.toThrow('Template execution failed');
|
||||
});
|
||||
|
||||
it('should handle asynchronous errors thrown by JavaScript templates', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that throws asynchronously
|
||||
const template: Template = {
|
||||
type: 'javascript',
|
||||
createNote: async () => {
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
throw new Error('Async template execution failed');
|
||||
},
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that async error is properly caught and handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, template, resolver)
|
||||
).rejects.toThrow('Async template execution failed');
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates returning null/undefined', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that returns null
|
||||
const nullTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () => null as any,
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that null return is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, nullTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
|
||||
// Create JavaScript template that returns undefined
|
||||
const undefinedTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () => undefined as any,
|
||||
};
|
||||
|
||||
// Test that undefined return is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, undefinedTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates returning invalid data structures', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that returns object with missing filepath
|
||||
const missingFilepathTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () =>
|
||||
({
|
||||
content: 'Valid content',
|
||||
// Missing filepath
|
||||
} as any),
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that missing filepath is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, missingFilepathTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
|
||||
// Create JavaScript template that returns object with missing content
|
||||
const missingContentTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () =>
|
||||
({
|
||||
filepath: 'valid-path.md',
|
||||
// Missing content
|
||||
} as any),
|
||||
};
|
||||
|
||||
// Test that missing content is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, missingContentTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
|
||||
// Create JavaScript template that returns wrong data types
|
||||
const wrongTypesTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () =>
|
||||
({
|
||||
filepath: 123, // Should be string
|
||||
content: true, // Should be string
|
||||
} as any),
|
||||
};
|
||||
|
||||
// Test that wrong data types are handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, wrongTypesTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates with rejected promises', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that returns rejected promise
|
||||
const rejectedPromiseTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () => Promise.reject(new Error('Promise rejected')),
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that rejected promise is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, rejectedPromiseTemplate, resolver)
|
||||
).rejects.toThrow('Promise rejected');
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates with mixed sync/async errors', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that sometimes throws sync, sometimes async
|
||||
let callCount = 0;
|
||||
const mixedErrorTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () => {
|
||||
callCount++;
|
||||
if (callCount % 2 === 0) {
|
||||
throw new Error('Sync error');
|
||||
} else {
|
||||
return Promise.reject(new Error('Async error'));
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test first call (async error)
|
||||
await expect(
|
||||
engine.processTemplate(trigger, mixedErrorTemplate, resolver)
|
||||
).rejects.toThrow('Async error');
|
||||
|
||||
// Test second call (sync error)
|
||||
await expect(
|
||||
engine.processTemplate(trigger, mixedErrorTemplate, resolver)
|
||||
).rejects.toThrow('Sync error');
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates that return promises resolving to invalid data', async () => {
|
||||
const { engine } = await setupFoamEngine();
|
||||
|
||||
// Create JavaScript template that returns promise resolving to invalid data
|
||||
const invalidPromiseTemplate: Template = {
|
||||
type: 'javascript',
|
||||
createNote: () =>
|
||||
Promise.resolve({
|
||||
filepath: null,
|
||||
content: null,
|
||||
} as any),
|
||||
};
|
||||
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.create-note'
|
||||
);
|
||||
|
||||
// Test that invalid promise resolution is handled
|
||||
await expect(
|
||||
engine.processTemplate(trigger, invalidPromiseTemplate, resolver)
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('trigger validation', () => {
|
||||
it('should validate command triggers', () => {
|
||||
const trigger = TriggerFactory.createCommandTrigger(
|
||||
'foam-vscode.open-daily-note',
|
||||
{ date: new Date() }
|
||||
);
|
||||
|
||||
expect(trigger.type).toBe('command');
|
||||
if (!isCommandTrigger(trigger)) {
|
||||
throw new Error('Expected command trigger type');
|
||||
}
|
||||
expect(trigger.command).toBe('foam-vscode.open-daily-note');
|
||||
expect(trigger.params).toHaveProperty('date');
|
||||
});
|
||||
|
||||
it('should validate placeholder triggers', () => {
|
||||
const sourceUri = strToUri('/test/source.md');
|
||||
const mockLocation = {
|
||||
uri: sourceUri,
|
||||
range: {
|
||||
start: { line: 0, character: 0 },
|
||||
end: { line: 0, character: 10 },
|
||||
},
|
||||
data: { rawText: '[[Test Note]]' },
|
||||
} as any;
|
||||
|
||||
const trigger = TriggerFactory.createPlaceholderTrigger(
|
||||
sourceUri,
|
||||
'Source Note',
|
||||
mockLocation
|
||||
);
|
||||
|
||||
expect(trigger.type).toBe('placeholder');
|
||||
if (!isPlaceholderTrigger(trigger)) {
|
||||
throw new Error('Expected placeholder trigger type');
|
||||
}
|
||||
expect(trigger.sourceNote).toMatchObject({
|
||||
uri: sourceUri.toString(),
|
||||
title: 'Source Note',
|
||||
location: mockLocation,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
197
packages/foam-vscode/src/services/note-creation-engine.ts
Normal file
197
packages/foam-vscode/src/services/note-creation-engine.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import {
|
||||
NoteCreationResult,
|
||||
NoteCreationTrigger,
|
||||
Template,
|
||||
TemplateContext,
|
||||
isCommandTrigger,
|
||||
isPlaceholderTrigger,
|
||||
} from './note-creation-types';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { asAbsoluteUri, URI } from '../core/model/uri';
|
||||
import { isAbsolute } from 'path';
|
||||
|
||||
/**
|
||||
* Unified engine for creating notes from both Markdown and JavaScript templates
|
||||
*/
|
||||
export class NoteCreationEngine {
|
||||
constructor(private foam: Foam, private roots: URI[]) {}
|
||||
|
||||
/**
|
||||
* Processes a template and generates note content and filepath
|
||||
* This method only handles template processing, not file creation
|
||||
*
|
||||
* @param trigger The trigger that initiated the note creation
|
||||
* @param template The template object containing content or function
|
||||
* @param resolver Resolver instance with all variables pre-configured
|
||||
* @returns Promise resolving to the generated content and filepath
|
||||
*/
|
||||
async processTemplate(
|
||||
trigger: NoteCreationTrigger,
|
||||
template: Template,
|
||||
resolver: Resolver
|
||||
): Promise<NoteCreationResult> {
|
||||
Logger.info(`Processing ${template.type} template`);
|
||||
this.logTriggerInfo(trigger);
|
||||
|
||||
let result: NoteCreationResult | null = null;
|
||||
if (template.type === 'javascript') {
|
||||
result = await this.executeJSTemplate(trigger, template, resolver);
|
||||
} else {
|
||||
result = await this.executeMarkdownTemplate(trigger, template, resolver);
|
||||
}
|
||||
|
||||
return {
|
||||
...result,
|
||||
filepath: result.filepath,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a JavaScript template
|
||||
*/
|
||||
private async executeJSTemplate(
|
||||
trigger: NoteCreationTrigger,
|
||||
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,
|
||||
foam: this.foam,
|
||||
foamDate: resolver.foamDate,
|
||||
};
|
||||
|
||||
try {
|
||||
const result = await template.createNote(templateContext);
|
||||
|
||||
// Validate the result structure and types
|
||||
this.validateNoteCreationResult(result);
|
||||
|
||||
if (!(result.filepath instanceof URI)) {
|
||||
result.filepath = URI.parse(result.filepath);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
Logger.error(`JavaScript template execution failed: ${errorMessage}`);
|
||||
throw new Error(`JavaScript template execution failed: ${errorMessage}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a Markdown template using variable resolution
|
||||
*/
|
||||
private async executeMarkdownTemplate(
|
||||
trigger: NoteCreationTrigger,
|
||||
template: Template & { type: 'markdown' },
|
||||
resolver: Resolver
|
||||
): Promise<NoteCreationResult> {
|
||||
// Use the provided resolver directly for variable resolution
|
||||
const resolvedContent = await resolver.resolveText(template.content);
|
||||
|
||||
// Process frontmatter metadata
|
||||
const [frontmatterMetadata, cleanContent] =
|
||||
extractFoamTemplateFrontmatterMetadata(resolvedContent);
|
||||
|
||||
// Combine template metadata with frontmatter metadata (frontmatter takes precedence)
|
||||
const metadata = new Map([
|
||||
...(template.metadata ?? new Map()),
|
||||
...frontmatterMetadata,
|
||||
]);
|
||||
|
||||
// Determine filepath - get variables from resolver for default generation
|
||||
const filepath =
|
||||
metadata.get('filepath') ??
|
||||
(await this.generateDefaultFilepath(resolver));
|
||||
|
||||
return {
|
||||
filepath: URI.parse(filepath),
|
||||
content: cleanContent,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a default filepath when none is specified in the template
|
||||
*/
|
||||
private async generateDefaultFilepath(resolver: Resolver): Promise<string> {
|
||||
const name =
|
||||
(await resolver.resolveFromName('FOAM_TITLE_SAFE')) || 'untitled';
|
||||
return `${name}.md`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the result returned by a JavaScript template
|
||||
*/
|
||||
private validateNoteCreationResult(
|
||||
result: any
|
||||
): asserts result is NoteCreationResult {
|
||||
if (!result || typeof result !== 'object') {
|
||||
throw new Error('JavaScript template must return an object');
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(result, 'filepath') ||
|
||||
(typeof result.filepath !== 'string' && !(result.filepath instanceof URI))
|
||||
) {
|
||||
throw new Error(
|
||||
'JavaScript template result must have a "filepath" property of type string or URI'
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(result, 'content') ||
|
||||
typeof result.content !== 'string'
|
||||
) {
|
||||
throw new Error(
|
||||
'JavaScript template result must have a "content" property of type string'
|
||||
);
|
||||
}
|
||||
|
||||
// Optional: Validate filepath doesn't contain dangerous characters
|
||||
const invalidChars = /[<>:"|?*\x00-\x1F]/; // eslint-disable-line no-control-regex
|
||||
if (invalidChars.test(result.filepath.path)) {
|
||||
throw new Error(
|
||||
'JavaScript template result "filepath" contains invalid characters'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs trigger-specific information for debugging
|
||||
*/
|
||||
private logTriggerInfo(trigger: NoteCreationTrigger): void {
|
||||
if (isCommandTrigger(trigger)) {
|
||||
Logger.info(`Note creation triggered by command: ${trigger.command}`);
|
||||
if (trigger.params) {
|
||||
Logger.info(`Command params:`, trigger.params);
|
||||
}
|
||||
|
||||
// Handle specific commands
|
||||
switch (trigger.command) {
|
||||
case 'foam-vscode.open-daily-note': {
|
||||
const date = trigger.params?.date;
|
||||
Logger.info(`Daily note for date: ${date}`);
|
||||
break;
|
||||
}
|
||||
case 'foam-vscode.create-note-from-template': {
|
||||
const templateUri = trigger.params?.templateUri;
|
||||
Logger.info(`Using template: ${templateUri}`);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
Logger.info(`Generic command: ${trigger.command}`);
|
||||
}
|
||||
} else if (isPlaceholderTrigger(trigger)) {
|
||||
const sourceNote = trigger.sourceNote;
|
||||
Logger.info(`Creating note from placeholder in: ${sourceNote.title}`);
|
||||
Logger.info(`Source URI: ${sourceNote.uri}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
46
packages/foam-vscode/src/services/note-creation-triggers.ts
Normal file
46
packages/foam-vscode/src/services/note-creation-triggers.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { Location } from '../core/model/location';
|
||||
import { ResourceLink } from '../core/model/note';
|
||||
import { NoteCreationTrigger } from './note-creation-types';
|
||||
|
||||
/**
|
||||
* Factory class for creating different types of note creation triggers
|
||||
*/
|
||||
export class TriggerFactory {
|
||||
/**
|
||||
* Creates a command trigger for note creation initiated by VS Code commands
|
||||
*
|
||||
* @param command The command name that triggered note creation
|
||||
* @param params Optional parameters associated with the command
|
||||
* @returns A command trigger object
|
||||
*/
|
||||
static createCommandTrigger(
|
||||
command: string,
|
||||
params?: Record<string, any>
|
||||
): NoteCreationTrigger {
|
||||
return { type: 'command', command, params };
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a placeholder trigger for note creation from wikilink placeholders
|
||||
*
|
||||
* @param sourceUri URI of the source note containing the placeholder
|
||||
* @param sourceTitle Title of the source note
|
||||
* @param location Location information for the placeholder in the source note
|
||||
* @returns A placeholder trigger object
|
||||
*/
|
||||
static createPlaceholderTrigger(
|
||||
sourceUri: URI,
|
||||
sourceTitle: string,
|
||||
location: Location<ResourceLink>
|
||||
): NoteCreationTrigger {
|
||||
return {
|
||||
type: 'placeholder',
|
||||
sourceNote: {
|
||||
uri: sourceUri.toString(),
|
||||
title: sourceTitle,
|
||||
location,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
84
packages/foam-vscode/src/services/note-creation-types.ts
Normal file
84
packages/foam-vscode/src/services/note-creation-types.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { Location } from '../core/model/location';
|
||||
import { ResourceLink } from '../core/model/note';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { URI } from '../core/model/uri';
|
||||
|
||||
/**
|
||||
* Union type for different trigger scenarios that can initiate note creation
|
||||
*/
|
||||
export type NoteCreationTrigger =
|
||||
| {
|
||||
type: 'command';
|
||||
command: string;
|
||||
params?: Record<string, any>; // Command arguments/parameters
|
||||
}
|
||||
| {
|
||||
type: 'placeholder';
|
||||
sourceNote: {
|
||||
uri: string;
|
||||
title: string;
|
||||
location: Location<ResourceLink>;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Template types supported by the note creation system
|
||||
*/
|
||||
export type Template =
|
||||
| { type: 'markdown'; content: string; metadata?: Map<string, string> }
|
||||
| {
|
||||
type: 'javascript';
|
||||
createNote: (context: TemplateContext) => Promise<NoteCreationResult>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Context provided to JavaScript template functions
|
||||
*/
|
||||
export interface TemplateContext {
|
||||
/** The trigger that initiated the note creation */
|
||||
trigger: NoteCreationTrigger;
|
||||
/** Resolver instance for variable resolution */
|
||||
resolver: Resolver;
|
||||
/** Foam instance for accessing workspace data */
|
||||
foam: Foam;
|
||||
/** Date used by the resolver for the FOAM_DATE_* variables */
|
||||
foamDate: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Context for creating a note through the unified creation system
|
||||
*/
|
||||
|
||||
/**
|
||||
* Result returned by note creation functions
|
||||
*/
|
||||
export interface NoteCreationResult {
|
||||
filepath: URI;
|
||||
content: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function signature for JavaScript template functions
|
||||
*/
|
||||
export type CreateNoteFunction = (
|
||||
context: TemplateContext
|
||||
) => Promise<NoteCreationResult> | NoteCreationResult;
|
||||
|
||||
/**
|
||||
* Type guard to check if trigger is a command trigger
|
||||
*/
|
||||
export function isCommandTrigger(
|
||||
trigger: NoteCreationTrigger
|
||||
): trigger is NoteCreationTrigger & { type: 'command' } {
|
||||
return trigger.type === 'command';
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard to check if trigger is a placeholder trigger
|
||||
*/
|
||||
export function isPlaceholderTrigger(
|
||||
trigger: NoteCreationTrigger
|
||||
): trigger is NoteCreationTrigger & { type: 'placeholder' } {
|
||||
return trigger.type === 'placeholder';
|
||||
}
|
||||
86
packages/foam-vscode/src/services/template-loader.spec.ts
Normal file
86
packages/foam-vscode/src/services/template-loader.spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
/* @unit-ready */
|
||||
import { workspace } from 'vscode';
|
||||
import { TemplateLoader } from './template-loader';
|
||||
import { createFile, deleteFile } from '../test/test-utils-vscode';
|
||||
import { randomString } from '../test/test-utils';
|
||||
|
||||
describe('TemplateLoader', () => {
|
||||
describe('workspace trust', () => {
|
||||
it('should throw error when loading JS template in untrusted workspace', async () => {
|
||||
const templateLoader = new TemplateLoader();
|
||||
const mockIsTrusted = jest.spyOn(workspace, 'isTrusted', 'get');
|
||||
mockIsTrusted.mockReturnValue(false);
|
||||
|
||||
const { uri } = await createFile(
|
||||
'function createNote() { return { filepath: "test.md", content: "test" }; }',
|
||||
[`test-template-${randomString()}.js`]
|
||||
);
|
||||
|
||||
try {
|
||||
await expect(templateLoader.loadTemplate(uri)).rejects.toThrow(
|
||||
'JavaScript templates can only be used in trusted workspaces for security reasons'
|
||||
);
|
||||
} finally {
|
||||
await deleteFile(uri);
|
||||
mockIsTrusted.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('should load JS template successfully in trusted workspace', async () => {
|
||||
const templateLoader = new TemplateLoader();
|
||||
const mockIsTrusted = jest.spyOn(workspace, 'isTrusted', 'get');
|
||||
mockIsTrusted.mockReturnValue(true);
|
||||
|
||||
const jsTemplateContent = `
|
||||
function createNote(context) {
|
||||
return {
|
||||
filepath: 'test-note.md',
|
||||
content: '# Test Note\\n\\nGenerated by JS template'
|
||||
};
|
||||
}
|
||||
`;
|
||||
const { uri } = await createFile(jsTemplateContent, [
|
||||
`test-template-${randomString()}.js`,
|
||||
]);
|
||||
|
||||
try {
|
||||
const template = await templateLoader.loadTemplate(uri);
|
||||
expect(template.type).toBe('javascript');
|
||||
if (template.type !== 'javascript') {
|
||||
throw new Error('Expected JavaScript template type');
|
||||
}
|
||||
expect(template.createNote).toBeDefined();
|
||||
expect(typeof template.createNote).toBe('function');
|
||||
} finally {
|
||||
await deleteFile(uri);
|
||||
}
|
||||
});
|
||||
|
||||
it('should load markdown template regardless of workspace trust', async () => {
|
||||
const templateLoader = new TemplateLoader();
|
||||
const mockIsTrusted = jest.spyOn(workspace, 'isTrusted', 'get');
|
||||
mockIsTrusted.mockReturnValue(false);
|
||||
|
||||
const mdTemplateContent = `---
|
||||
name: Test Template
|
||||
---
|
||||
# Test Note
|
||||
|
||||
This is a markdown template.`;
|
||||
const { uri } = await createFile(mdTemplateContent, [
|
||||
`test-template-${randomString()}.md`,
|
||||
]);
|
||||
|
||||
try {
|
||||
const template = await templateLoader.loadTemplate(uri);
|
||||
expect(template.type).toBe('markdown');
|
||||
if (template.type !== 'markdown') {
|
||||
throw new Error('Expected markdown template type');
|
||||
}
|
||||
expect(template.content).toBe(mdTemplateContent);
|
||||
} finally {
|
||||
await deleteFile(uri);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
77
packages/foam-vscode/src/services/template-loader.ts
Normal file
77
packages/foam-vscode/src/services/template-loader.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { workspace } from 'vscode';
|
||||
import { URI } from '../core/model/uri';
|
||||
import { readFile } from './editor';
|
||||
import {
|
||||
Template,
|
||||
TemplateContext,
|
||||
NoteCreationResult,
|
||||
} from './note-creation-types';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
import { JSTemplateLoader } from './js-template-loader';
|
||||
|
||||
/**
|
||||
* Utility for loading templates from file paths and converting them to Template objects
|
||||
*/
|
||||
export class TemplateLoader {
|
||||
private jsTemplateLoader: JSTemplateLoader;
|
||||
|
||||
constructor() {
|
||||
this.jsTemplateLoader = new JSTemplateLoader();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a template from a file path
|
||||
* @param template Path to the template file (relative or absolute)
|
||||
* @returns Promise resolving to a Template object
|
||||
*/
|
||||
async loadTemplate(template: URI): Promise<Template> {
|
||||
if (template.path.endsWith('.js')) {
|
||||
if (!workspace.isTrusted) {
|
||||
throw new Error(
|
||||
'JavaScript templates can only be used in trusted workspaces for security reasons'
|
||||
);
|
||||
}
|
||||
return await this.loadJavaScriptTemplate(template);
|
||||
} else {
|
||||
return await this.loadMarkdownTemplate(template);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a JavaScript template
|
||||
*/
|
||||
private async loadJavaScriptTemplate(template: URI): Promise<Template> {
|
||||
const createNoteFunction = await this.jsTemplateLoader.loadFunction(
|
||||
template
|
||||
);
|
||||
|
||||
// Ensure the function returns a Promise
|
||||
const createNote = async (
|
||||
context: TemplateContext
|
||||
): Promise<NoteCreationResult> => {
|
||||
const result = await createNoteFunction(context);
|
||||
return result;
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'javascript',
|
||||
createNote,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a Markdown template
|
||||
*/
|
||||
private async loadMarkdownTemplate(template: URI): Promise<Template> {
|
||||
const content = await readFile(template);
|
||||
|
||||
// Extract metadata from frontmatter if present
|
||||
const [metadata] = extractFoamTemplateFrontmatterMetadata(content);
|
||||
|
||||
return {
|
||||
type: 'markdown',
|
||||
content,
|
||||
metadata: metadata.size > 0 ? metadata : undefined,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Selection, Uri, ViewColumn, window, workspace } from 'vscode';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Selection, window } from 'vscode';
|
||||
import { NoteFactory } from '../services/templates';
|
||||
import {
|
||||
closeEditors,
|
||||
@@ -7,217 +6,10 @@ import {
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
showInEditor,
|
||||
withModifiedFoamConfiguration,
|
||||
} from '../test/test-utils-vscode';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { fileExists } from './editor';
|
||||
|
||||
describe('Create note from template', () => {
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
jest.clearAllMocks();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('User flow', () => {
|
||||
it('should resolve the path using the config when path is derived from note title', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementation(jest.fn(() => Promise.resolve('Title of note')));
|
||||
|
||||
const noteA = await createFile('Note A', [
|
||||
'path',
|
||||
'of-new-note',
|
||||
'note-a.md',
|
||||
]);
|
||||
await showInEditor(noteA.uri);
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.newNotePath',
|
||||
'currentDir',
|
||||
async () => {
|
||||
const result = await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(result.uri.path).toEqual(
|
||||
noteA.uri.getDirectory().joinPath('Title of note.md').path
|
||||
);
|
||||
await deleteFile(result.uri);
|
||||
}
|
||||
);
|
||||
await withModifiedFoamConfiguration(
|
||||
'files.newNotePath',
|
||||
'root',
|
||||
async () => {
|
||||
const result = await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(result.uri.path).toEqual(
|
||||
Uri.joinPath(workspace.workspaceFolders[0].uri, 'Title of note.md')
|
||||
.path
|
||||
);
|
||||
await deleteFile(result.uri);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(noteA);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should ask a user to confirm the path if note already exists', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
const fileA = await createFile('Content of file A');
|
||||
await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date()),
|
||||
fileA.uri
|
||||
);
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
prompt: `Enter the path for the new note`,
|
||||
})
|
||||
);
|
||||
|
||||
await deleteFile(fileA);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should not ask a user for path if defined in template', async () => {
|
||||
const uri = getUriInWorkspace();
|
||||
const templateA = await createFile(
|
||||
`---
|
||||
foam_template: # foam template metadata
|
||||
filepath: ${uri.toFsPath()}
|
||||
---
|
||||
`,
|
||||
['.foam', 'templates', 'template-with-path.md']
|
||||
);
|
||||
const spy = jest
|
||||
.spyOn(window, 'showInputBox')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date())
|
||||
);
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
||||
await deleteFile(uri);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
|
||||
it('should focus the editor on the newly created note', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
const target = getUriInWorkspace();
|
||||
await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date()),
|
||||
target
|
||||
);
|
||||
expect(fromVsCodeUri(window.activeTextEditor.document.uri)).toEqual(
|
||||
target
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(templateA);
|
||||
});
|
||||
});
|
||||
|
||||
it('should expand variables when using a template', async () => {
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
const template = await createFile('${FOAM_DATE_YEAR}', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-with-variables.md',
|
||||
]);
|
||||
const target = getUriInWorkspace();
|
||||
await NoteFactory.createFromTemplate(
|
||||
template.uri,
|
||||
new Resolver(new Map(), new Date()),
|
||||
target
|
||||
);
|
||||
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
`${new Date().getFullYear()}`
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(template);
|
||||
});
|
||||
|
||||
describe('Creation with active text selection', () => {
|
||||
it('should open created note in a new column if there was a selection', async () => {
|
||||
const templateA = await createFile('Template A', [
|
||||
'.foam',
|
||||
'templates',
|
||||
'template-a.md',
|
||||
]);
|
||||
const file = await createFile('This is my first file: for new file');
|
||||
const { editor } = await showInEditor(file.uri);
|
||||
editor.selection = new Selection(0, 23, 0, 35);
|
||||
const target = getUriInWorkspace();
|
||||
await NoteFactory.createFromTemplate(
|
||||
templateA.uri,
|
||||
new Resolver(new Map(), new Date()),
|
||||
target
|
||||
);
|
||||
expect(window.activeTextEditor.viewColumn).toEqual(ViewColumn.Two);
|
||||
expect(fromVsCodeUri(window.visibleTextEditors[0].document.uri)).toEqual(
|
||||
file.uri
|
||||
);
|
||||
expect(fromVsCodeUri(window.visibleTextEditors[1].document.uri)).toEqual(
|
||||
target
|
||||
);
|
||||
|
||||
await deleteFile(target);
|
||||
await deleteFile(templateA);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
it('should replace selection with a link to the newly created note', async () => {
|
||||
const template = await createFile(
|
||||
// eslint-disable-next-line no-template-curly-in-string
|
||||
'Hello ${FOAM_SELECTED_TEXT} ${FOAM_SELECTED_TEXT}',
|
||||
['.foam', 'templates', 'template-with-selection.md']
|
||||
);
|
||||
const file = await createFile('This is my first file: World');
|
||||
const { editor } = await showInEditor(file.uri);
|
||||
editor.selection = new Selection(0, 23, 0, 28);
|
||||
const target = getUriInWorkspace();
|
||||
await NoteFactory.createFromTemplate(
|
||||
template.uri,
|
||||
new Resolver(new Map(), new Date()),
|
||||
target
|
||||
);
|
||||
expect(window.activeTextEditor.document.getText()).toEqual(
|
||||
'Hello World World'
|
||||
);
|
||||
expect(window.visibleTextEditors[0].document.getText()).toEqual(
|
||||
`This is my first file: [[${target.getName()}]]`
|
||||
);
|
||||
await deleteFile(template.uri);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('NoteFactory.createNote', () => {
|
||||
beforeEach(async () => {
|
||||
await closeEditors();
|
||||
|
||||
@@ -22,7 +22,6 @@ import {
|
||||
replaceSelection,
|
||||
} from './editor';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import dateFormat from 'dateformat';
|
||||
import { getFoamVsCodeConfig } from './config';
|
||||
import { isNone } from '../core/utils';
|
||||
|
||||
@@ -36,20 +35,50 @@ export const getTemplatesDir = () =>
|
||||
);
|
||||
|
||||
/**
|
||||
* The URI of the default template
|
||||
* Gets the candidate URIs for the default note template
|
||||
* @returns An array of candidate URIs for the default note template
|
||||
*/
|
||||
export const getDefaultTemplateUri = () =>
|
||||
getTemplatesDir().joinPath('new-note.md');
|
||||
export const getDefaultNoteTemplateCandidateUris = () => [
|
||||
getTemplatesDir().joinPath('new-note.js'),
|
||||
getTemplatesDir().joinPath('new-note.md'),
|
||||
];
|
||||
|
||||
/**
|
||||
* The URI of the template for daily notes
|
||||
* Gets the default template URI
|
||||
* @returns The URI of the default template or undefined if no default template is found
|
||||
*/
|
||||
export const getDailyNoteTemplateUri = () =>
|
||||
getTemplatesDir().joinPath('daily-note.md');
|
||||
export const getDefaultTemplateUri = async () => {
|
||||
for (const uri of getDefaultNoteTemplateCandidateUris()) {
|
||||
if (await fileExists(uri)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const WIKILINK_DEFAULT_TEMPLATE_TEXT = `# $\{1:$FOAM_TITLE}\n\n$0`;
|
||||
/**
|
||||
* Gets the candidate URIs for the daily note template
|
||||
* @returns An array of candidate URIs for the daily note template
|
||||
*/
|
||||
export const getDailyNoteTemplateCandidateUris = () => [
|
||||
getTemplatesDir().joinPath('daily-note.js'),
|
||||
getTemplatesDir().joinPath('daily-note.md'),
|
||||
];
|
||||
|
||||
const TEMPLATE_CONTENT = `# \${1:$TM_FILENAME_BASE}
|
||||
/**
|
||||
* Gets the daily note template URI
|
||||
* @returns The URI of the daily note template or undefined if no template is found
|
||||
*/
|
||||
export const getDailyNoteTemplateUri = async () => {
|
||||
for (const uri of getDailyNoteTemplateCandidateUris()) {
|
||||
if (await fileExists(uri)) {
|
||||
return uri;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const DEFAULT_NEW_NOTE_TEMPLATE = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
Welcome to Foam templates.
|
||||
|
||||
@@ -78,7 +107,7 @@ export async function getTemplateMetadata(
|
||||
|
||||
export async function getTemplates(): Promise<URI[]> {
|
||||
const templates = await workspace
|
||||
.findFiles('.foam/templates/**.md', null)
|
||||
.findFiles('.foam/templates/**{.md,.js}', null)
|
||||
.then(v => v.map(uri => fromVsCodeUri(uri)));
|
||||
return templates;
|
||||
}
|
||||
@@ -223,7 +252,11 @@ const createFnForOnRelativePathStrategy =
|
||||
|
||||
switch (onRelativePath) {
|
||||
case 'resolve-from-current-dir':
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
try {
|
||||
return getCurrentEditorDirectory().joinPath(existingFile.path);
|
||||
} catch (e) {
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
}
|
||||
case 'resolve-from-root':
|
||||
return asAbsoluteWorkspaceUri(existingFile);
|
||||
case 'cancel':
|
||||
@@ -233,7 +266,7 @@ const createFnForOnRelativePathStrategy =
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
return newProposedPath && existingFile.with({ path: newProposedPath });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -257,7 +290,7 @@ const createFnForOnFileExistsStrategy =
|
||||
const newProposedPath = await askUserForFilepathConfirmation(
|
||||
existingFile
|
||||
);
|
||||
return newProposedPath && URI.file(newProposedPath);
|
||||
return newProposedPath && existingFile.with({ path: newProposedPath });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -298,6 +331,7 @@ export const NoteFactory = {
|
||||
}
|
||||
}
|
||||
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath, true);
|
||||
const expandedText = await resolver.resolveText(text);
|
||||
const selectedContent = findSelectionContent();
|
||||
await createDocAndFocus(
|
||||
@@ -326,102 +360,6 @@ export const NoteFactory = {
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new note using a template.
|
||||
* @param templateUri the URI of the template to use.
|
||||
* @param resolver the Resolver to use.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use if the template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
createFromTemplate: async (
|
||||
templateUri: URI,
|
||||
resolver: Resolver,
|
||||
filepathFallbackURI?: URI,
|
||||
templateFallbackText = '',
|
||||
onFileExists?: OnFileExistStrategy
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
try {
|
||||
const template = await getTemplateInfo(
|
||||
templateUri,
|
||||
templateFallbackText,
|
||||
resolver
|
||||
);
|
||||
|
||||
let newFilePath = template.metadata.has('filepath')
|
||||
? URI.file(template.metadata.get('filepath'))
|
||||
: filepathFallbackURI;
|
||||
|
||||
if (isNone(newFilePath)) {
|
||||
newFilePath = await getPathFromTitle(resolver);
|
||||
} else if (!newFilePath.path.startsWith('./')) {
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath);
|
||||
}
|
||||
|
||||
return NoteFactory.createNote(
|
||||
newFilePath,
|
||||
template.text,
|
||||
resolver,
|
||||
onFileExists
|
||||
);
|
||||
} catch (err) {
|
||||
if (err instanceof UserCancelledOperation) {
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a daily note from the daily note template.
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateFallbackText the template text to use if daily-note.md template does not exist. This is configurable by the caller for backwards compatibility purposes.
|
||||
*/
|
||||
createFromDailyNoteTemplate: (
|
||||
filepathFallbackURI: URI,
|
||||
templateFallbackText: string,
|
||||
targetDate: Date
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
const resolver = new Resolver(
|
||||
new Map().set('FOAM_TITLE', dateFormat(targetDate, 'yyyy-mm-dd', false)),
|
||||
targetDate
|
||||
);
|
||||
return NoteFactory.createFromTemplate(
|
||||
getDailyNoteTemplateUri(),
|
||||
resolver,
|
||||
filepathFallbackURI,
|
||||
templateFallbackText,
|
||||
_ => Promise.resolve(undefined)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates a new note when following a placeholder wikilink using the default template.
|
||||
* @param wikilinkPlaceholder the placeholder value from the wikilink. (eg. `[[Hello Joe]]` -> `Hello Joe`)
|
||||
* @param filepathFallbackURI the URI to use if the template does not specify the `filepath` metadata attribute. This is configurable by the caller for backwards compatibility purposes.
|
||||
* @param templateURI URI of the template to use. If undefined, use the default template.
|
||||
*/
|
||||
createForPlaceholderWikilink: async (
|
||||
wikilinkPlaceholder: string,
|
||||
filepathFallbackURI: URI,
|
||||
templateURI?: URI
|
||||
): Promise<{ didCreateFile: boolean; uri: URI | undefined }> => {
|
||||
const resolver = new Resolver(
|
||||
new Map().set('FOAM_TITLE', wikilinkPlaceholder),
|
||||
new Date()
|
||||
);
|
||||
|
||||
if (templateURI === undefined) {
|
||||
templateURI = getDefaultTemplateUri();
|
||||
}
|
||||
|
||||
return NoteFactory.createFromTemplate(
|
||||
templateURI,
|
||||
resolver,
|
||||
filepathFallbackURI,
|
||||
WIKILINK_DEFAULT_TEMPLATE_TEXT
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const createTemplate = async (): Promise<void> => {
|
||||
@@ -443,10 +381,10 @@ export const createTemplate = async (): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const filenameURI = URI.file(filename);
|
||||
const filenameURI = defaultTemplate.with({ path: filename });
|
||||
await workspace.fs.writeFile(
|
||||
toVsCodeUri(filenameURI),
|
||||
new TextEncoder().encode(TEMPLATE_CONTENT)
|
||||
new TextEncoder().encode(DEFAULT_NEW_NOTE_TEMPLATE)
|
||||
);
|
||||
await focusNote(filenameURI, false);
|
||||
};
|
||||
@@ -475,7 +413,7 @@ async function askUserForFilepathConfirmation(
|
||||
});
|
||||
}
|
||||
|
||||
export const getPathFromTitle = async (resolver: Resolver) => {
|
||||
export const getPathFromTitle = async (scheme: string, resolver: Resolver) => {
|
||||
const defaultName = await resolver.resolveFromName('FOAM_TITLE_SAFE');
|
||||
return URI.file(`${defaultName}.md`);
|
||||
return new URI({ scheme, path: `${defaultName}.md` });
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { Selection, window } from 'vscode';
|
||||
import { Resolver } from './variable-resolver';
|
||||
import { Variable } from '../core/common/snippetParser';
|
||||
@@ -72,7 +73,24 @@ describe('variable-resolver, variable resolution', () => {
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_TITLE', async () => {
|
||||
it('should resolve FOAM_TITLE if provided in constructor', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
|
||||
const expected = new Map<string, string>();
|
||||
expected.set('FOAM_TITLE', foamTitle);
|
||||
expected.set('FOAM_SLUG', 'my-note-title');
|
||||
|
||||
const variables = [new Variable('FOAM_TITLE'), new Variable('FOAM_SLUG')];
|
||||
|
||||
const resolver = new Resolver(
|
||||
new Map<string, string>(),
|
||||
new Date(),
|
||||
foamTitle
|
||||
);
|
||||
expect(await resolver.resolveAll(variables)).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should resolve FOAM_TITLE if provided as variable', async () => {
|
||||
const foamTitle = 'My note title';
|
||||
const variables = [new Variable('FOAM_TITLE'), new Variable('FOAM_SLUG')];
|
||||
|
||||
@@ -240,6 +258,18 @@ describe('variable-resolver, resolveText', () => {
|
||||
expect(await resolver.resolveText(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['2021-10-12T00:00:00'],
|
||||
['2021-10-12T23:59:59'],
|
||||
['2021-10-12T12:34:56'],
|
||||
])('should resolve date variables in local time', async (d: string) => {
|
||||
// Related to #1502
|
||||
const resolver = new Resolver(new Map(), new Date(d));
|
||||
expect(await resolver.resolve(new Variable('FOAM_DATE_DATE'))).toEqual(
|
||||
'12'
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing for unknown Foam-specific variables', async () => {
|
||||
const input = `
|
||||
# $FOAM_FOO
|
||||
@@ -259,7 +289,7 @@ describe('variable-resolver, resolveText', () => {
|
||||
editor.selection = new Selection(0, 11, 1, 0);
|
||||
const resolver = new Resolver(new Map(), new Date());
|
||||
expect(await resolver.resolveFromName('FOAM_SELECTED_TEXT')).toEqual(
|
||||
'note file'
|
||||
'Content of note file'
|
||||
);
|
||||
await deleteFile(file);
|
||||
});
|
||||
|
||||
@@ -38,8 +38,13 @@ export class Resolver implements VariableResolver {
|
||||
*/
|
||||
constructor(
|
||||
private givenValues: Map<string, string>,
|
||||
private foamDate: Date
|
||||
) {}
|
||||
public foamDate: Date,
|
||||
foamTitle?: string
|
||||
) {
|
||||
if (foamTitle) {
|
||||
this.givenValues.set('FOAM_TITLE', foamTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a variable definition in the resolver
|
||||
@@ -51,6 +56,16 @@ export class Resolver implements VariableResolver {
|
||||
this.givenValues.set(name, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all defined variables as a plain object
|
||||
* Useful for passing to JavaScript templates that expect extraParams
|
||||
*
|
||||
* @returns Record containing all defined variables
|
||||
*/
|
||||
getVariables(): Record<string, string> {
|
||||
return Object.fromEntries(this.givenValues);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a string, replacing the variables with their values
|
||||
*
|
||||
@@ -238,7 +253,7 @@ async function resolveFoamTitle() {
|
||||
value.trim().length === 0 ? 'Please enter a title' : undefined,
|
||||
});
|
||||
if (title === undefined) {
|
||||
throw new UserCancelledOperation();
|
||||
throw new UserCancelledOperation('User did not provide a note title');
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* @unit-ready */
|
||||
import { getNotesExtensions } from './settings';
|
||||
import { withModifiedFoamConfiguration } from './test/test-utils-vscode';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { workspace, GlobPattern } from 'vscode';
|
||||
import { uniq } from 'lodash';
|
||||
import { getFoamVsCodeConfig } from './services/config';
|
||||
import { expandAlternateGroups } from './utils/globExpand';
|
||||
|
||||
/**
|
||||
* Gets the notes extensions and default extension from the config.
|
||||
@@ -44,5 +45,5 @@ export function getIgnoredFilesSetting(): GlobPattern[] {
|
||||
'**/.foam/**',
|
||||
...workspace.getConfiguration().get('foam.files.ignore', []),
|
||||
...Object.keys(workspace.getConfiguration().get('files.exclude', {})),
|
||||
];
|
||||
].flatMap(expandAlternateGroups);
|
||||
}
|
||||
|
||||
@@ -2,22 +2,36 @@ import path from 'path';
|
||||
import { runTests } from 'vscode-test';
|
||||
import { runUnit } from './suite-unit';
|
||||
|
||||
function parseArgs(): { unit: boolean; e2e: boolean } {
|
||||
function parseArgs(): {
|
||||
unit: boolean;
|
||||
e2e: boolean;
|
||||
excludeSpecs: boolean;
|
||||
jestArgs: string[];
|
||||
} {
|
||||
const args = process.argv.slice(2);
|
||||
const unit = args.some(arg => arg === '--unit');
|
||||
const e2e = args.some(arg => arg === '--e2e');
|
||||
return unit || e2e ? { unit, e2e } : { unit: true, e2e: true };
|
||||
const unit = args.includes('--unit');
|
||||
const e2e = args.includes('--e2e');
|
||||
const excludeSpecs = args.includes('--exclude-specs');
|
||||
|
||||
// Filter out our custom flags and pass the rest to Jest
|
||||
const jestArgs = args.filter(
|
||||
arg => !['--unit', '--e2e', '--exclude-specs'].includes(arg)
|
||||
);
|
||||
|
||||
return unit || e2e
|
||||
? { unit, e2e, excludeSpecs, jestArgs }
|
||||
: { unit: true, e2e: true, excludeSpecs, jestArgs };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const { unit, e2e } = parseArgs();
|
||||
const { unit, e2e, excludeSpecs, jestArgs } = parseArgs();
|
||||
|
||||
let isSuccess = true;
|
||||
|
||||
if (unit) {
|
||||
try {
|
||||
console.log('Running unit tests');
|
||||
await runUnit();
|
||||
await runUnit(jestArgs, excludeSpecs);
|
||||
} catch (err) {
|
||||
console.log('Error occurred while running Foam unit tests:', err);
|
||||
isSuccess = false;
|
||||
@@ -45,10 +59,11 @@ async function main() {
|
||||
extensionTestsPath,
|
||||
launchArgs: [
|
||||
testWorkspace,
|
||||
'--disable-gpu',
|
||||
'--disable-extensions',
|
||||
'--disable-workspace-trust',
|
||||
],
|
||||
version: '1.70.0',
|
||||
version: '1.96.0',
|
||||
});
|
||||
} catch (err) {
|
||||
console.log('Error occurred while running Foam e2e tests:', err);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user