Skip to content

Commit

Permalink
Web extension support for daily note (#1426)
Browse files Browse the repository at this point in the history
* Using nodemon for watch task

* Added documentation and generator pattern to support getting some data from multiple sources

* asAbsoluteUrl can now take URI or string

* Tweaked daily note computation

* Replacing URI.withFragment with generic URI.with

* Removed URI.file from non-testing code

* fixed asAbsoluteUri

* Various tweaks and fixes

* Fixed create-note command
  • Loading branch information
riccardoferretti authored Feb 21, 2025
1 parent 1a99e69 commit 6b02a87
Show file tree
Hide file tree
Showing 20 changed files with 330 additions and 104 deletions.
23 changes: 22 additions & 1 deletion .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 2 additions & 1 deletion packages/foam-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -677,7 +677,7 @@
"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",
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
Expand Down Expand Up @@ -711,6 +711,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",
Expand Down
6 changes: 3 additions & 3 deletions packages/foam-vscode/src/core/model/uri.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
40 changes: 33 additions & 7 deletions packages/foam-vscode/src/core/model/uri.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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)
Expand Down Expand Up @@ -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,
});
}

/**
Expand Down Expand Up @@ -380,12 +397,21 @@ function encodeURIComponentMinimal(path: string): string {
*
* TODO this probably needs to be moved to the workspace service
*/
export function asAbsoluteUri(uri: URI, baseFolders: URI[]): URI {
const path = uri.path;
if (pathUtils.isAbsolute(path)) {
return uri;
export function asAbsoluteUri(
uriOrPath: URI | string,
baseFolders: URI[]
): 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;
if (path.startsWith('/')) {
return uriOrPath instanceof URI ? uriOrPath : baseFolders[0].with({ path });
}
let tokens = path.split('/');
while (tokens[0].trim() === '') {
tokens.shift();
}
const firstDir = tokens[0];
if (baseFolders.length > 1) {
for (const folder of baseFolders) {
Expand Down
6 changes: 3 additions & 3 deletions packages/foam-vscode/src/core/model/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
5 changes: 4 additions & 1 deletion packages/foam-vscode/src/core/model/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ export class FoamWorkspace implements IDisposable {
}
}
if (resource && fragment) {
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
resource = {
...resource,
uri: resource.uri.with({ fragment: fragment }),
};
}
return resource ?? null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
);
});

Expand All @@ -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' })
);
});

Expand Down
4 changes: 2 additions & 2 deletions packages/foam-vscode/src/core/services/markdown-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
URI.placeholder(target);

if (section) {
targetUri = targetUri.withFragment(section);
targetUri = targetUri.with({ fragment: section });
}
}
break;
Expand All @@ -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;
}
Expand Down
62 changes: 62 additions & 0 deletions packages/foam-vscode/src/core/utils/core.ts
Original file line number Diff line number Diff line change
@@ -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();
}
}
13 changes: 7 additions & 6 deletions packages/foam-vscode/src/dated-notes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { workspace } from 'vscode';
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
import { createDailyNoteIfNotExists, getDailyNoteUri } from './dated-notes';
import { isWindows } from './core/common/platform';
import {
cleanWorkspace,
Expand All @@ -10,8 +10,9 @@ import {
withModifiedFoamConfiguration,
} from './test/test-utils-vscode';
import { fromVsCodeUri } from './utils/vsc-utils';
import { URI } from './core/model/uri';

describe('getDailyNotePath', () => {
describe('getDailyNoteUri', () => {
const date = new Date('2021-02-07T00:00:00Z');
const year = date.getFullYear();
const month = date.getMonth() + 1;
Expand All @@ -21,12 +22,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)
);
});

Expand All @@ -39,7 +40,7 @@ describe('getDailyNotePath', () => {
: `${config}/${isoDate}.md`;

await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
expect(getDailyNoteUri(date).toFsPath()).toMatch(expectedPath)
);
});
});
Expand All @@ -54,7 +55,7 @@ describe('Daily note template', () => {
['.foam', 'templates', 'daily-note.md']
);

const uri = getDailyNotePath(targetDate);
const uri = getDailyNoteUri(targetDate);

await createDailyNoteIfNotExists(targetDate);

Expand Down
27 changes: 12 additions & 15 deletions packages/foam-vscode/src/dated-notes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { joinPath } from './core/utils/path';
import dateFormat from 'dateformat';
import { URI } from './core/model/uri';
import { NoteFactory } from './services/templates';
Expand Down Expand Up @@ -32,17 +33,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));
}

/**
Expand Down Expand Up @@ -76,20 +73,20 @@ export function getDailyNoteFileName(date: Date): string {
* @returns Whether the file was created.
*/
export async function createDailyNoteIfNotExists(targetDate: Date) {
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
const uriFromLegacyConfiguration = getDailyNoteUri(targetDate);
const pathFromLegacyConfiguration = uriFromLegacyConfiguration.toFsPath();
const titleFormat: string =
getFoamVsCodeConfig('openDailyNote.titleFormat') ??
getFoamVsCodeConfig('openDailyNote.filenameFormat');

const templateFallbackText = `---
foam_template:
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
---
# ${dateFormat(targetDate, titleFormat, false)}
`;
const templateFallbackText = `# ${dateFormat(
targetDate,
titleFormat,
false
)}\n`;

return await NoteFactory.createFromDailyNoteTemplate(
pathFromLegacyConfiguration,
uriFromLegacyConfiguration,
templateFallbackText,
targetDate
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 6b02a87

Please sign in to comment.