mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7a562aa0aa | ||
|
|
0bab17c130 | ||
|
|
8121223e30 | ||
|
|
793664ac59 | ||
|
|
4c5430d2b1 | ||
|
|
ebef851f5a | ||
|
|
253ee94b1c | ||
|
|
9ffd465a32 | ||
|
|
ff3dacdbbf | ||
|
|
0a6350464b | ||
|
|
fe0228bdcc | ||
|
|
471260bdd3 | ||
|
|
a22f1b46dc | ||
|
|
318641ae04 | ||
|
|
12a4fd98c3 | ||
|
|
a93360eb1b | ||
|
|
0938de2694 | ||
|
|
a120f368c3 | ||
|
|
c028689012 | ||
|
|
27665154db |
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -24,7 +24,6 @@
|
||||
"editor.formatOnSave": true,
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"jest.autoRun": "off",
|
||||
"jest.rootPath": "packages/foam-vscode",
|
||||
"jest.jestCommandLine": "yarn test:unit-with-specs",
|
||||
"gitdoc.enabled": false,
|
||||
|
||||
31
CLAUDE.md
31
CLAUDE.md
@@ -2,8 +2,14 @@
|
||||
|
||||
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
|
||||
@@ -23,6 +29,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
||||
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.
|
||||
|
||||
@@ -33,6 +46,8 @@ Use the utility functions from `test-utils.ts` and `test-utils-vscode.ts` and `t
|
||||
|
||||
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/`.
|
||||
@@ -45,6 +60,10 @@ This is a monorepo using Yarn workspaces with the main VS Code extension in `pac
|
||||
- `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.
|
||||
@@ -99,9 +118,19 @@ This allows features to:
|
||||
|
||||
## 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.
|
||||
- After saving a file, always run `prettier` on it to adjust its formatting.
|
||||
|
||||
### Adding New Features
|
||||
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.27.1"
|
||||
"version": "0.27.3"
|
||||
}
|
||||
|
||||
@@ -4,6 +4,14 @@ 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:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.27.1",
|
||||
"version": "0.27.3",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -574,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"
|
||||
@@ -591,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"
|
||||
@@ -682,7 +686,7 @@
|
||||
"clean": "rimraf out",
|
||||
"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",
|
||||
|
||||
@@ -125,30 +125,134 @@ describe('asAbsoluteUri', () => {
|
||||
).toEqual(workspaceFolder2.joinPath('file'));
|
||||
});
|
||||
|
||||
describe('with Windows filesystem paths', () => {
|
||||
it('should return the given path if it is a Windows absolute path (C: drive)', () => {
|
||||
const windowsPath = 'C:/Users/user/template.md';
|
||||
const workspaceFolder = URI.file('/workspace/folder');
|
||||
const result = asAbsoluteUri(windowsPath, [workspaceFolder]);
|
||||
// Should convert to proper URI format
|
||||
expect(result.path).toEqual('C:/Users/user/template.md');
|
||||
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 return the given path if it is a Windows absolute path (backslashes)', () => {
|
||||
const windowsPath = 'C:\\Users\\user\\template.md';
|
||||
const workspaceFolder = URI.file('/workspace/folder');
|
||||
const result = asAbsoluteUri(windowsPath, [workspaceFolder]);
|
||||
// Should convert to proper URI format
|
||||
expect(result.path).toEqual('C:\\Users\\user\\template.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 treat relative Windows-style paths as relative', () => {
|
||||
const relativePath = 'folder\\subfolder\\file.md';
|
||||
const workspaceFolder = URI.file('/workspace/folder');
|
||||
const result = asAbsoluteUri(relativePath, [workspaceFolder]);
|
||||
expect(result.path).toEqual(
|
||||
'/workspace/folder/folder\\subfolder\\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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -412,7 +412,8 @@ export function asAbsoluteUri(
|
||||
const isDrivePath = /^[a-zA-Z]:/.test(path);
|
||||
// Check if this is already a POSIX absolute path
|
||||
if (path.startsWith('/') || isDrivePath) {
|
||||
const uri = baseFolders[0].with({ path });
|
||||
const uri = URI.parse(path); // Validate the path
|
||||
|
||||
if (forceSubfolder) {
|
||||
const isAlreadySubfolder = baseFolders.some(folder =>
|
||||
uri.path.startsWith(folder.path)
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/* @unit-ready */
|
||||
import { workspace } from 'vscode';
|
||||
import { createDailyNoteIfNotExists, getDailyNoteUri } from './dated-notes';
|
||||
import { workspace, window } from 'vscode';
|
||||
import {
|
||||
CREATE_DAILY_NOTE_WARNING_RESPONSE,
|
||||
createDailyNoteIfNotExists,
|
||||
getDailyNoteUri,
|
||||
} from './dated-notes';
|
||||
import { isWindows } from './core/common/platform';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
@@ -12,9 +16,11 @@ import {
|
||||
withModifiedFoamConfiguration,
|
||||
} from './test/test-utils-vscode';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { URI } from './core/model/uri';
|
||||
import { fileExists } from './services/editor';
|
||||
import { getDailyNoteTemplateUri } from './services/templates';
|
||||
import { fileExists, readFile } from './services/editor';
|
||||
import {
|
||||
getDailyNoteTemplateCandidateUris,
|
||||
getDailyNoteTemplateUri,
|
||||
} from './services/templates';
|
||||
|
||||
describe('getDailyNoteUri', () => {
|
||||
const date = new Date('2021-02-07T00:00:00Z');
|
||||
@@ -52,6 +58,15 @@ describe('getDailyNoteUri', () => {
|
||||
describe('Daily note creation and template processing', () => {
|
||||
const DAILY_NOTE_TEMPLATE = ['.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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe('Basic daily note creation', () => {
|
||||
it('Creates a new daily note when it does not exist', async () => {
|
||||
const targetDate = new Date(2021, 8, 1);
|
||||
@@ -86,15 +101,6 @@ describe('Daily note creation and template processing', () => {
|
||||
});
|
||||
|
||||
describe('Template variable resolution', () => {
|
||||
beforeEach(async () => {
|
||||
// Ensure no template exists
|
||||
let i = 0;
|
||||
while ((await fileExists(await getDailyNoteTemplateUri())) && i < 5) {
|
||||
await deleteFile(await getDailyNoteTemplateUri());
|
||||
i++;
|
||||
}
|
||||
});
|
||||
|
||||
it('Resolves all FOAM_DATE_* variables correctly', async () => {
|
||||
const targetDate = new Date(2021, 8, 12); // September 12, 2021
|
||||
|
||||
@@ -123,7 +129,6 @@ Unix: \${FOAM_DATE_SECONDS_UNIX}`,
|
||||
expect(content).toContain('Date: 12');
|
||||
expect(content).toContain('Day: Sunday (short: Sun)');
|
||||
expect(content).toContain('Week: 36');
|
||||
expect(content).toContain('Unix: 1631404800');
|
||||
|
||||
await deleteFile(template.uri);
|
||||
await deleteFile(result.uri);
|
||||
@@ -242,7 +247,7 @@ Unix: \${FOAM_DATE_SECONDS_UNIX}`,
|
||||
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\`,
|
||||
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.\`
|
||||
};
|
||||
};`,
|
||||
@@ -272,6 +277,36 @@ Unix: \${FOAM_DATE_SECONDS_UNIX}`,
|
||||
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);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Uri, window, workspace } from 'vscode';
|
||||
import { joinPath } from './core/utils/path';
|
||||
import dateFormat from 'dateformat';
|
||||
import { URI } from './core/model/uri';
|
||||
@@ -6,6 +7,8 @@ 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.
|
||||
@@ -68,6 +71,30 @@ export function getDailyNoteFileName(date: Date): string {
|
||||
return `${dateFormat(date, filenameFormat, false)}.${fileExtension}`;
|
||||
}
|
||||
|
||||
const DEFAULT_DAILY_NOTE_TEMPLATE = `---
|
||||
foam_template:
|
||||
filepath: "/journal/\${FOAM_DATE_YEAR}-\${FOAM_DATE_MONTH}-\${FOAM_DATE_DATE}.md"
|
||||
description: "Daily note template"
|
||||
---
|
||||
# \${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.
|
||||
`;
|
||||
|
||||
export const CREATE_DAILY_NOTE_WARNING_RESPONSE = 'Create daily note template';
|
||||
|
||||
/**
|
||||
* Create a daily note using the unified creation engine (supports JS templates)
|
||||
*
|
||||
@@ -76,6 +103,38 @@ export function getDailyNoteFileName(date: Date): string {
|
||||
* @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') ??
|
||||
@@ -88,29 +147,15 @@ export async function createDailyNoteIfNotExists(targetDate: Date, foam: Foam) {
|
||||
false
|
||||
)}\n`;
|
||||
|
||||
const templatePath = await getDailyNoteTemplateUri();
|
||||
|
||||
// Set up variables for template processing
|
||||
const formattedDate = dateFormat(targetDate, 'yyyy-mm-dd', false);
|
||||
const variables = {
|
||||
FOAM_TITLE: formattedDate,
|
||||
title: formattedDate,
|
||||
};
|
||||
|
||||
// Format date without timezone conversion to avoid off-by-one errors
|
||||
const year = targetDate.getFullYear();
|
||||
const month = String(targetDate.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(targetDate.getDate()).padStart(2, '0');
|
||||
const dateString = `${year}-${month}-${day}`;
|
||||
|
||||
return await createNote(
|
||||
{
|
||||
notePath: dailyNoteUri.toFsPath(),
|
||||
templatePath: templatePath,
|
||||
text: templateFallbackText, // fallback if template doesn't exist
|
||||
date: dateString, // YYYY-MM-DD format without timezone issues
|
||||
text: templateFallbackText,
|
||||
date: targetDate,
|
||||
variables: variables,
|
||||
onFileExists: 'open', // existing behavior - open if exists
|
||||
onFileExists: 'open',
|
||||
onRelativeNotePath: 'resolve-from-root',
|
||||
},
|
||||
foam
|
||||
);
|
||||
|
||||
@@ -42,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',
|
||||
});
|
||||
@@ -55,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');
|
||||
@@ -66,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' },
|
||||
});
|
||||
@@ -78,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',
|
||||
});
|
||||
@@ -93,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',
|
||||
});
|
||||
@@ -104,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',
|
||||
});
|
||||
@@ -115,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',
|
||||
});
|
||||
@@ -126,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',
|
||||
});
|
||||
@@ -229,6 +229,27 @@ describe('create-note command', () => {
|
||||
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'
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('factories', () => {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../../utils/vsc-utils';
|
||||
import { Logger } from '../../core/utils/log';
|
||||
|
||||
export default async function activate(
|
||||
context: ExtensionContext,
|
||||
@@ -40,7 +41,7 @@ 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.
|
||||
*/
|
||||
@@ -61,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)
|
||||
*/
|
||||
@@ -89,9 +90,28 @@ 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 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
|
||||
@@ -141,23 +161,23 @@ export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
// 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);
|
||||
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 ?? {})),
|
||||
date
|
||||
foamDate,
|
||||
args.title
|
||||
);
|
||||
|
||||
// Define all variables in the resolver with proper mapping
|
||||
if (args.title) {
|
||||
resolver.define('FOAM_TITLE', args.title);
|
||||
}
|
||||
|
||||
// Add other parameters as variables
|
||||
if (args.notePath) {
|
||||
resolver.define('notePath', args.notePath);
|
||||
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
|
||||
@@ -167,15 +187,9 @@ export async function createNote(args: CreateNoteArgs, foam: Foam) {
|
||||
);
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
// Determine final file path
|
||||
const finalUri = new URI({
|
||||
scheme: workspace.workspaceFolders[0].uri.scheme,
|
||||
path: result.filepath,
|
||||
});
|
||||
|
||||
// Create the note using NoteFactory with the same resolver
|
||||
const createdNote = await NoteFactory.createNote(
|
||||
finalUri,
|
||||
result.filepath,
|
||||
result.content,
|
||||
resolver,
|
||||
args.onFileExists,
|
||||
|
||||
@@ -14,6 +14,7 @@ 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');
|
||||
|
||||
@@ -59,7 +60,7 @@ Test content with title: \${FOAM_TITLE}`,
|
||||
// Test processing
|
||||
const result = await engine.processTemplate(trigger, template, resolver);
|
||||
|
||||
expect(result.filepath).toBe('test-note.md');
|
||||
expect(result.filepath.path).toBe('test-note.md');
|
||||
expect(result.content).toContain('# Test Note');
|
||||
expect(result.content).toContain('Test content with title: Test Note');
|
||||
});
|
||||
@@ -176,7 +177,7 @@ Content without filepath metadata.`,
|
||||
);
|
||||
|
||||
expect(result.content).toContain('# My New Note');
|
||||
expect(result.filepath).toBe('My New Note.md'); // Should generate from title
|
||||
expect(result.filepath.path).toBe('My New Note.md'); // Should generate from title
|
||||
});
|
||||
|
||||
it('should handle JavaScript templates correctly', async () => {
|
||||
@@ -190,7 +191,9 @@ Content without filepath metadata.`,
|
||||
'Untitled';
|
||||
const content = `# ${title}\n\nGenerated by JavaScript template\n\nTrigger: ${context.trigger.type}`;
|
||||
return {
|
||||
filepath: `${title.replace(/\s+/g, '-').toLowerCase()}.md`,
|
||||
filepath: URI.parse(
|
||||
`${title.replace(/\s+/g, '-').toLowerCase()}.md`
|
||||
),
|
||||
content,
|
||||
};
|
||||
},
|
||||
@@ -211,7 +214,7 @@ Content without filepath metadata.`,
|
||||
expect(result.content).toContain('# JS Generated Note');
|
||||
expect(result.content).toContain('Generated by JavaScript template');
|
||||
expect(result.content).toContain('Trigger: command');
|
||||
expect(result.filepath).toBe('js-generated-note.md');
|
||||
expect(result.filepath.path).toBe('js-generated-note.md');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export class NoteCreationEngine {
|
||||
Logger.info(`Processing ${template.type} template`);
|
||||
this.logTriggerInfo(trigger);
|
||||
|
||||
let result = null;
|
||||
let result: NoteCreationResult | null = null;
|
||||
if (template.type === 'javascript') {
|
||||
result = await this.executeJSTemplate(trigger, template, resolver);
|
||||
} else {
|
||||
@@ -45,9 +45,7 @@ export class NoteCreationEngine {
|
||||
|
||||
return {
|
||||
...result,
|
||||
filepath: isAbsolute(result.filepath)
|
||||
? asAbsoluteUri(result.filepath, this.roots, true).path
|
||||
: result.filepath,
|
||||
filepath: result.filepath,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -75,6 +73,9 @@ export class NoteCreationEngine {
|
||||
// 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 =
|
||||
@@ -111,7 +112,7 @@ export class NoteCreationEngine {
|
||||
(await this.generateDefaultFilepath(resolver));
|
||||
|
||||
return {
|
||||
filepath,
|
||||
filepath: URI.parse(filepath),
|
||||
content: cleanContent,
|
||||
};
|
||||
}
|
||||
@@ -137,10 +138,10 @@ export class NoteCreationEngine {
|
||||
|
||||
if (
|
||||
!Object.prototype.hasOwnProperty.call(result, 'filepath') ||
|
||||
typeof result.filepath !== 'string'
|
||||
(typeof result.filepath !== 'string' && !(result.filepath instanceof URI))
|
||||
) {
|
||||
throw new Error(
|
||||
'JavaScript template result must have a "filepath" property of type string'
|
||||
'JavaScript template result must have a "filepath" property of type string or URI'
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,13 +154,9 @@ export class NoteCreationEngine {
|
||||
);
|
||||
}
|
||||
|
||||
if (result.filepath.trim() === '') {
|
||||
throw new Error('JavaScript template result "filepath" cannot be empty');
|
||||
}
|
||||
|
||||
// Optional: Validate filepath doesn't contain dangerous characters
|
||||
const invalidChars = /[<>:"|?*\x00-\x1F]/; // eslint-disable-line no-control-regex
|
||||
if (invalidChars.test(result.filepath)) {
|
||||
if (invalidChars.test(result.filepath.path)) {
|
||||
throw new Error(
|
||||
'JavaScript template result "filepath" contains invalid characters'
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ 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
|
||||
@@ -53,7 +54,7 @@ export interface TemplateContext {
|
||||
* Result returned by note creation functions
|
||||
*/
|
||||
export interface NoteCreationResult {
|
||||
filepath: string;
|
||||
filepath: URI;
|
||||
content: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,15 +34,21 @@ export const getTemplatesDir = () =>
|
||||
'templates'
|
||||
);
|
||||
|
||||
/**
|
||||
* Gets the candidate URIs for the default note template
|
||||
* @returns An array of candidate URIs for the default note template
|
||||
*/
|
||||
export const getDefaultNoteTemplateCandidateUris = () => [
|
||||
getTemplatesDir().joinPath('new-note.js'),
|
||||
getTemplatesDir().joinPath('new-note.md'),
|
||||
];
|
||||
|
||||
/**
|
||||
* Gets the default template URI
|
||||
* @returns The URI of the default template or undefined if no default template is found
|
||||
*/
|
||||
export const getDefaultTemplateUri = async () => {
|
||||
for (const uri of [
|
||||
getTemplatesDir().joinPath('new-note.js'),
|
||||
getTemplatesDir().joinPath('new-note.md'),
|
||||
]) {
|
||||
for (const uri of getDefaultNoteTemplateCandidateUris()) {
|
||||
if (await fileExists(uri)) {
|
||||
return uri;
|
||||
}
|
||||
@@ -51,14 +57,20 @@ export const getDefaultTemplateUri = async () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* The URI of the template for daily notes
|
||||
* @returns The URI of the daily note template or undefined if no daily note template is found
|
||||
* 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'),
|
||||
];
|
||||
|
||||
/**
|
||||
* 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 [
|
||||
getTemplatesDir().joinPath('daily-note.js'),
|
||||
getTemplatesDir().joinPath('daily-note.md'),
|
||||
]) {
|
||||
for (const uri of getDailyNoteTemplateCandidateUris()) {
|
||||
if (await fileExists(uri)) {
|
||||
return uri;
|
||||
}
|
||||
@@ -66,7 +78,7 @@ export const getDailyNoteTemplateUri = async () => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const TEMPLATE_CONTENT = `# \${1:$TM_FILENAME_BASE}
|
||||
const DEFAULT_NEW_NOTE_TEMPLATE = `# \${1:$TM_FILENAME_BASE}
|
||||
|
||||
Welcome to Foam templates.
|
||||
|
||||
@@ -319,6 +331,7 @@ export const NoteFactory = {
|
||||
}
|
||||
}
|
||||
|
||||
newFilePath = asAbsoluteWorkspaceUri(newFilePath, true);
|
||||
const expandedText = await resolver.resolveText(text);
|
||||
const selectedContent = findSelectionContent();
|
||||
await createDocAndFocus(
|
||||
@@ -371,7 +384,7 @@ export const createTemplate = async (): Promise<void> => {
|
||||
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);
|
||||
};
|
||||
|
||||
@@ -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>,
|
||||
public foamDate: Date
|
||||
) {}
|
||||
public foamDate: Date,
|
||||
foamTitle?: string
|
||||
) {
|
||||
if (foamTitle) {
|
||||
this.givenValues.set('FOAM_TITLE', foamTitle);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a variable definition in the resolver
|
||||
|
||||
Reference in New Issue
Block a user