Skip to content

Commit 6b02a87

Browse files
Web extension support for daily note (#1426)
* 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
1 parent 1a99e69 commit 6b02a87

20 files changed

+330
-104
lines changed

.vscode/tasks.json

+22-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,28 @@
77
"label": "watch: foam-vscode",
88
"type": "npm",
99
"script": "watch",
10-
"problemMatcher": "$tsc-watch",
10+
"problemMatcher": {
11+
"owner": "typescript",
12+
"fileLocation": ["relative", "${workspaceFolder}"],
13+
"pattern": [
14+
{
15+
"regexp": "^(.*?)\\((\\d+),(\\d+)\\):\\s+(.*)$",
16+
"file": 1,
17+
"line": 2,
18+
"column": 3,
19+
"message": 4
20+
}
21+
],
22+
"background": {
23+
"activeOnStart": true,
24+
"beginsPattern": {
25+
"regexp": ".*"
26+
},
27+
"endsPattern": {
28+
"regexp": ".*"
29+
}
30+
}
31+
},
1132
"isBackground": true,
1233
"presentation": {
1334
"reveal": "always"

packages/foam-vscode/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -677,7 +677,7 @@
677677
"test:e2e": "yarn test-setup && node ./out/test/run-tests.js --e2e",
678678
"lint": "dts lint src",
679679
"clean": "rimraf out",
680-
"watch": "tsc --build ./tsconfig.json --watch",
680+
"watch": "nodemon --watch 'src/**/*.ts' --exec 'yarn build' --ext ts",
681681
"vscode:start-debugging": "yarn clean && yarn watch",
682682
"package-extension": "npx vsce package --yarn",
683683
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
@@ -711,6 +711,7 @@
711711
"jest-extended": "^3.2.3",
712712
"markdown-it": "^12.0.4",
713713
"micromatch": "^4.0.2",
714+
"nodemon": "^3.1.7",
714715
"rimraf": "^3.0.2",
715716
"ts-jest": "^29.1.1",
716717
"tslib": "^2.0.0",

packages/foam-vscode/src/core/model/uri.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ describe('Foam URI', () => {
88
const base = URI.file('/path/to/file.md');
99
test.each([
1010
['https://www.google.com', URI.parse('https://www.google.com')],
11-
['/path/to/a/file.md', URI.file('/path/to/a/file.md')],
12-
['../relative/file.md', URI.file('/path/relative/file.md')],
13-
['#section', base.withFragment('section')],
11+
['/path/to/a/file.md', URI.parse('file:///path/to/a/file.md')],
12+
['../relative/file.md', URI.parse('file:///path/relative/file.md')],
13+
['#section', base.with({ fragment: 'section' })],
1414
[
1515
'../relative/file.md#section',
1616
URI.parse('file:/path/relative/file.md#section'),

packages/foam-vscode/src/core/model/uri.ts

+33-7
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ export class URI {
5858
});
5959
}
6060

61+
/**
62+
* @deprecated Will not work with web extension. Use only for testing.
63+
* @param value the path to turn into a URI
64+
* @returns the file URI
65+
*/
6166
static file(value: string): URI {
6267
const [path, authority] = pathUtils.fromFsPath(value);
6368
return new URI({ scheme: 'file', authority, path });
@@ -71,7 +76,7 @@ export class URI {
7176
const uri = value instanceof URI ? value : URI.parse(value);
7277
if (!uri.isAbsolute()) {
7378
if (uri.scheme === 'file' || uri.scheme === 'placeholder') {
74-
let newUri = this.withFragment(uri.fragment);
79+
let newUri = this.with({ fragment: uri.fragment });
7580
if (uri.path) {
7681
newUri = (isDirectory ? newUri : newUri.getDirectory())
7782
.joinPath(uri.path)
@@ -119,8 +124,20 @@ export class URI {
119124
return new URI({ ...this, path });
120125
}
121126

122-
withFragment(fragment: string): URI {
123-
return new URI({ ...this, fragment });
127+
with(change: {
128+
scheme?: string;
129+
authority?: string;
130+
path?: string;
131+
query?: string;
132+
fragment?: string;
133+
}): URI {
134+
return new URI({
135+
scheme: change.scheme ?? this.scheme,
136+
authority: change.authority ?? this.authority,
137+
path: change.path ?? this.path,
138+
query: change.query ?? this.query,
139+
fragment: change.fragment ?? this.fragment,
140+
});
124141
}
125142

126143
/**
@@ -380,12 +397,21 @@ function encodeURIComponentMinimal(path: string): string {
380397
*
381398
* TODO this probably needs to be moved to the workspace service
382399
*/
383-
export function asAbsoluteUri(uri: URI, baseFolders: URI[]): URI {
384-
const path = uri.path;
385-
if (pathUtils.isAbsolute(path)) {
386-
return uri;
400+
export function asAbsoluteUri(
401+
uriOrPath: URI | string,
402+
baseFolders: URI[]
403+
): URI {
404+
if (baseFolders.length === 0) {
405+
throw new Error('At least one base folder needed to compute URI');
406+
}
407+
const path = uriOrPath instanceof URI ? uriOrPath.path : uriOrPath;
408+
if (path.startsWith('/')) {
409+
return uriOrPath instanceof URI ? uriOrPath : baseFolders[0].with({ path });
387410
}
388411
let tokens = path.split('/');
412+
while (tokens[0].trim() === '') {
413+
tokens.shift();
414+
}
389415
const firstDir = tokens[0];
390416
if (baseFolders.length > 1) {
391417
for (const folder of baseFolders) {

packages/foam-vscode/src/core/model/workspace.test.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -126,9 +126,9 @@ describe('Identifier computation', () => {
126126
});
127127
const ws = new FoamWorkspace('.md').set(first).set(second).set(third);
128128

129-
expect(ws.getIdentifier(first.uri.withFragment('section name'))).toEqual(
130-
'to/page-a#section name'
131-
);
129+
expect(
130+
ws.getIdentifier(first.uri.with({ fragment: 'section name' }))
131+
).toEqual('to/page-a#section name');
132132
});
133133

134134
const needle = '/project/car/todo';

packages/foam-vscode/src/core/model/workspace.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,10 @@ export class FoamWorkspace implements IDisposable {
175175
}
176176
}
177177
if (resource && fragment) {
178-
resource = { ...resource, uri: resource.uri.withFragment(fragment) };
178+
resource = {
179+
...resource,
180+
uri: resource.uri.with({ fragment: fragment }),
181+
};
179182
}
180183
return resource ?? null;
181184
}

packages/foam-vscode/src/core/services/markdown-provider.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ describe('Link resolution', () => {
148148
const ws = createTestWorkspace().set(noteA).set(noteB);
149149

150150
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
151-
noteB.uri.withFragment('section')
151+
noteB.uri.with({ fragment: 'section' })
152152
);
153153
});
154154

@@ -163,7 +163,7 @@ describe('Link resolution', () => {
163163
const ws = createTestWorkspace().set(noteA);
164164

165165
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
166-
noteA.uri.withFragment('section')
166+
noteA.uri.with({ fragment: 'section' })
167167
);
168168
});
169169

packages/foam-vscode/src/core/services/markdown-provider.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
7676
URI.placeholder(target);
7777

7878
if (section) {
79-
targetUri = targetUri.withFragment(section);
79+
targetUri = targetUri.with({ fragment: section });
8080
}
8181
}
8282
break;
@@ -93,7 +93,7 @@ export class MarkdownResourceProvider implements ResourceProvider {
9393
workspace.find(path, resource.uri)?.uri ??
9494
URI.placeholder(resource.uri.resolve(path).path);
9595
if (section && !targetUri.isPlaceholder()) {
96-
targetUri = targetUri.withFragment(section);
96+
targetUri = targetUri.with({ fragment: section });
9797
}
9898
break;
9999
}
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,85 @@
11
import sha1 from 'js-sha1';
22

3+
/**
4+
* Checks if a value is not null.
5+
*
6+
* @param value - The value to check.
7+
* @returns True if the value is not null, otherwise false.
8+
*/
39
export function isNotNull<T>(value: T | null): value is T {
410
return value != null;
511
}
612

13+
/**
14+
* Checks if a value is not null, undefined, or void.
15+
*
16+
* @param value - The value to check.
17+
* @returns True if the value is not null, undefined, or void, otherwise false.
18+
*/
719
export function isSome<T>(
820
value: T | null | undefined | void
921
): value is NonNullable<T> {
1022
return value != null;
1123
}
1224

25+
/**
26+
* Checks if a value is null, undefined, or void.
27+
*
28+
* @param value - The value to check.
29+
* @returns True if the value is null, undefined, or void, otherwise false.
30+
*/
1331
export function isNone<T>(
1432
value: T | null | undefined | void
1533
): value is null | undefined | void {
1634
return value == null;
1735
}
1836

37+
/**
38+
* Checks if a string is numeric.
39+
*
40+
* @param value - The string to check.
41+
* @returns True if the string is numeric, otherwise false.
42+
*/
1943
export function isNumeric(value: string): boolean {
2044
return /-?\d+$/.test(value);
2145
}
2246

47+
/**
48+
* Generates a SHA-1 hash of the given text.
49+
*
50+
* @param text - The text to hash.
51+
* @returns The SHA-1 hash of the text.
52+
*/
2353
export const hash = (text: string) => sha1.sha1(text);
54+
55+
/**
56+
* Executes an array of functions and returns the first result that satisfies the predicate.
57+
*
58+
* @param functions - The array of functions to execute.
59+
* @param predicate - The predicate to test the results. Defaults to checking if the result is not null.
60+
* @returns The first result that satisfies the predicate, or undefined if no result satisfies the predicate.
61+
*/
62+
export async function firstFrom<T>(
63+
functions: Array<() => T | Promise<T>>,
64+
predicate: (result: T) => boolean = result => result != null
65+
): Promise<T | undefined> {
66+
for (const fn of functions) {
67+
const result = await fn();
68+
if (predicate(result)) {
69+
return result;
70+
}
71+
}
72+
return undefined;
73+
}
74+
75+
/**
76+
* Lazily executes an array of functions and yields their results.
77+
*
78+
* @param functions - The array of functions to execute.
79+
* @returns A generator yielding the results of the functions.
80+
*/
81+
function* lazyExecutor<T>(functions: Array<() => T>): Generator<T> {
82+
for (const fn of functions) {
83+
yield fn();
84+
}
85+
}

packages/foam-vscode/src/dated-notes.spec.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { workspace } from 'vscode';
2-
import { createDailyNoteIfNotExists, getDailyNotePath } from './dated-notes';
2+
import { createDailyNoteIfNotExists, getDailyNoteUri } from './dated-notes';
33
import { isWindows } from './core/common/platform';
44
import {
55
cleanWorkspace,
@@ -10,8 +10,9 @@ import {
1010
withModifiedFoamConfiguration,
1111
} from './test/test-utils-vscode';
1212
import { fromVsCodeUri } from './utils/vsc-utils';
13+
import { URI } from './core/model/uri';
1314

14-
describe('getDailyNotePath', () => {
15+
describe('getDailyNoteUri', () => {
1516
const date = new Date('2021-02-07T00:00:00Z');
1617
const year = date.getFullYear();
1718
const month = date.getMonth() + 1;
@@ -21,12 +22,12 @@ describe('getDailyNotePath', () => {
2122
test('Adds the root directory to relative directories', async () => {
2223
const config = 'journal';
2324

24-
const expectedPath = fromVsCodeUri(
25+
const expectedUri = fromVsCodeUri(
2526
workspace.workspaceFolders[0].uri
2627
).joinPath(config, `${isoDate}.md`);
2728

2829
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
29-
expect(getDailyNotePath(date).toFsPath()).toEqual(expectedPath.toFsPath())
30+
expect(getDailyNoteUri(date)).toEqual(expectedUri)
3031
);
3132
});
3233

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

4142
await withModifiedFoamConfiguration('openDailyNote.directory', config, () =>
42-
expect(getDailyNotePath(date).toFsPath()).toMatch(expectedPath)
43+
expect(getDailyNoteUri(date).toFsPath()).toMatch(expectedPath)
4344
);
4445
});
4546
});
@@ -54,7 +55,7 @@ describe('Daily note template', () => {
5455
['.foam', 'templates', 'daily-note.md']
5556
);
5657

57-
const uri = getDailyNotePath(targetDate);
58+
const uri = getDailyNoteUri(targetDate);
5859

5960
await createDailyNoteIfNotExists(targetDate);
6061

packages/foam-vscode/src/dated-notes.ts

+12-15
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { joinPath } from './core/utils/path';
12
import dateFormat from 'dateformat';
23
import { URI } from './core/model/uri';
34
import { NoteFactory } from './services/templates';
@@ -32,17 +33,13 @@ export async function openDailyNoteFor(date?: Date) {
3233
* This function first checks the `foam.openDailyNote.directory` configuration string,
3334
* defaulting to the current directory.
3435
*
35-
* In the case that the directory path is not absolute,
36-
* the resulting path will start on the current workspace top-level.
37-
*
3836
* @param date A given date to be formatted as filename.
39-
* @returns The path to the daily note file.
37+
* @returns The URI to the daily note file.
4038
*/
41-
export function getDailyNotePath(date: Date): URI {
39+
export function getDailyNoteUri(date: Date): URI {
4240
const folder = getFoamVsCodeConfig<string>('openDailyNote.directory') ?? '.';
43-
const dailyNoteDirectory = asAbsoluteWorkspaceUri(URI.file(folder));
4441
const dailyNoteFilename = getDailyNoteFileName(date);
45-
return dailyNoteDirectory.joinPath(dailyNoteFilename);
42+
return asAbsoluteWorkspaceUri(joinPath(folder, dailyNoteFilename));
4643
}
4744

4845
/**
@@ -76,20 +73,20 @@ export function getDailyNoteFileName(date: Date): string {
7673
* @returns Whether the file was created.
7774
*/
7875
export async function createDailyNoteIfNotExists(targetDate: Date) {
79-
const pathFromLegacyConfiguration = getDailyNotePath(targetDate);
76+
const uriFromLegacyConfiguration = getDailyNoteUri(targetDate);
77+
const pathFromLegacyConfiguration = uriFromLegacyConfiguration.toFsPath();
8078
const titleFormat: string =
8179
getFoamVsCodeConfig('openDailyNote.titleFormat') ??
8280
getFoamVsCodeConfig('openDailyNote.filenameFormat');
8381

84-
const templateFallbackText = `---
85-
foam_template:
86-
filepath: "${pathFromLegacyConfiguration.toFsPath().replace(/\\/g, '\\\\')}"
87-
---
88-
# ${dateFormat(targetDate, titleFormat, false)}
89-
`;
82+
const templateFallbackText = `# ${dateFormat(
83+
targetDate,
84+
titleFormat,
85+
false
86+
)}\n`;
9087

9188
return await NoteFactory.createFromDailyNoteTemplate(
92-
pathFromLegacyConfiguration,
89+
uriFromLegacyConfiguration,
9390
templateFallbackText,
9491
targetDate
9592
);

packages/foam-vscode/src/features/commands/convert-links-format-in-note.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -175,11 +175,11 @@ async function convertLinkInCopy(
175175
const resource = fParser.parse(fromVsCodeUri(doc.uri), text);
176176
const basePath = doc.uri.path.split('/').slice(0, -1).join('/');
177177

178-
const fileUri = Uri.file(
179-
`${
178+
const fileUri = doc.uri.with({
179+
path: `${
180180
basePath ? basePath + '/' : ''
181-
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`
182-
);
181+
}${resource.uri.getName()}.copy${resource.uri.getExtension()}`,
182+
});
183183
const encoder = new TextEncoder();
184184
await workspace.fs.writeFile(fileUri, encoder.encode(text));
185185
await window.showTextDocument(fileUri);

0 commit comments

Comments
 (0)