mirror of
https://github.com/foambubble/foam.git
synced 2026-01-10 22:48:09 -05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
129482a43e | ||
|
|
0c1c4da154 | ||
|
|
7f4b700b21 | ||
|
|
686e05ed25 | ||
|
|
b2c7ecbb3d | ||
|
|
2c643e0c63 | ||
|
|
3b33d3d696 | ||
|
|
87633e68b1 | ||
|
|
6c7b558f36 | ||
|
|
12037704d7 | ||
|
|
e549fb8c21 | ||
|
|
ac7d3243c4 | ||
|
|
748df5e352 | ||
|
|
dcd46f1378 | ||
|
|
f9f751a27a | ||
|
|
0764da0dd6 | ||
|
|
f747d7445a | ||
|
|
eb74e57a9e | ||
|
|
a01cf8ec8d | ||
|
|
5b63fa8108 | ||
|
|
ddf7ddf7b3 | ||
|
|
4b263667ea | ||
|
|
309194b3c3 | ||
|
|
c4f35b7649 | ||
|
|
b9e18de7e7 | ||
|
|
23cf5a021e | ||
|
|
8231ed14c5 | ||
|
|
3bea283c04 | ||
|
|
a3cffe8418 | ||
|
|
675e7fa216 | ||
|
|
87d12bf3af | ||
|
|
e118ab74b5 | ||
|
|
04a61eed0e | ||
|
|
350b3005f1 | ||
|
|
f7293b1eb4 | ||
|
|
672eb6ed20 |
@@ -851,6 +851,69 @@
|
||||
"contributions": [
|
||||
"tool"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chrisUsick",
|
||||
"name": "Chris Usick",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/6589365?v=4",
|
||||
"profile": "http://cu-dev.ca",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "josephdecock",
|
||||
"name": "Joe DeCock",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1145533?v=4",
|
||||
"profile": "https://github.com/josephdecock",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "drewtyler",
|
||||
"name": "Drew Tyler",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/5640816?v=4",
|
||||
"profile": "http://www.drewtyler.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Lauviah0622",
|
||||
"name": "Lauviah0622",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/43416399?v=4",
|
||||
"profile": "https://github.com/Lauviah0622",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "joshdover",
|
||||
"name": "Josh Dover",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/1813008?v=4",
|
||||
"profile": "https://www.elastic.co/elastic-agent",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "phelma",
|
||||
"name": "Phil Helm",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/4057948?v=4",
|
||||
"profile": "http://phelm.co.uk",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "lingyv-li",
|
||||
"name": "Larry Li",
|
||||
"avatar_url": "https://avatars.githubusercontent.com/u/8937944?v=4",
|
||||
"profile": "https://github.com/lingyv-li",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
@@ -28,6 +28,22 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
// Restrict usage of fs module outside tests to keep foam compatible with the browser
|
||||
"files": ["**/src/**"],
|
||||
"excludedFiles": ["**/src/test/**", "**/src/**/*{test,spec}.ts"],
|
||||
"rules": {
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
{
|
||||
"name": "fs",
|
||||
"message": "Extension code must not rely Node.js filesystem, use vscode.workspace.fs instead."
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"settings": {
|
||||
"import/core-modules": ["vscode"],
|
||||
"import/parsers": {
|
||||
|
||||
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -12,6 +12,7 @@ jobs:
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-18.04
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
@@ -39,6 +40,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
OS: ${{ matrix.os }}
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Setup Node
|
||||
@@ -60,4 +62,4 @@ jobs:
|
||||
- name: Run Tests
|
||||
uses: GabrielBB/xvfb-action@v1.4
|
||||
with:
|
||||
run: yarn test
|
||||
run: yarn test --stream
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Creating New Notes
|
||||
|
||||
- Write out a new `[[wikilink]]` and `Cmd` + `Click` to create a new file and enter it.
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- For keyboard navigation, use the 'Follow Definition' key `F12` (or [remap the 'editor.action.revealDefinition' key binding](https://code.visualstudio.com/docs/getstarted/keybindings) to something more ergonomic)
|
||||
- `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), execute `Foam: Create New Note` and enter a **Title Case Name** to create `Title Case Name.md`
|
||||
- Add a keyboard binding to make creating new notes easier.
|
||||
- The [[note-templates]] used by this command can be customized.
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
- Ensure that you have all the [[recommended-extensions]] installed in Visual Studio Code
|
||||
- Reload Visual Studio Code by running `Cmd` + `Shift` + `P` (`Ctrl` + `Shift` + `P` for Windows), type "reload" and run the **Developer: Reload Window** command to for the updated extensions take effect
|
||||
- Check the formatting rules for links on [[foam-file-format]], [[wikilinks]] and [[link-formatting-and-autocompletion]]
|
||||
- Check the formatting rules for links on [[foam-file-format]] and [[wikilinks]]
|
||||
|
||||
## I don't want Foam enabled for all my workspaces
|
||||
Any extension you install in Visual Studio Code is enabled by default. Give the philosophy of Foam it works out of the box without doing any configuration upfront. In case you want to disable Foam for a specific workspace, or disable Foam by default and enable it for specific workspaces, it is advised to follow the best practices as [documented by Visual Studio Code](https://code.visualstudio.com/docs/editor/extension-marketplace#_manage-extensions)
|
||||
|
||||
@@ -31,8 +31,6 @@ You can use **Foam** for organising your research, keeping re-discoverable notes
|
||||
|
||||
**Foam** is a tool that supports creating relationships between thoughts and information to help you think better.
|
||||
|
||||

|
||||
|
||||
Whether you want to build a [Second Brain](https://www.buildingasecondbrain.com/) or a [Zettelkasten](https://zettelkasten.de/posts/overview/), write a book, or just get better at long-term learning, **Foam** can help you organise your thoughts if you follow these simple rules:
|
||||
|
||||
1. Create a single **Foam** workspace for all your knowledge and research following the [Getting started](#getting-started) guide.
|
||||
@@ -227,6 +225,15 @@ If that sounds like something you're interested in, I'd love to have you along o
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://Cliffordfajardo.com"><img src="https://avatars.githubusercontent.com/u/6743796?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Clifford Fajardo </b></sub></a><br /><a href="#tool-cliffordfajardo" title="Tools">🔧</a></td>
|
||||
<td align="center"><a href="http://cu-dev.ca"><img src="https://avatars.githubusercontent.com/u/6589365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Chris Usick</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chrisUsick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/josephdecock"><img src="https://avatars.githubusercontent.com/u/1145533?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe DeCock</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=josephdecock" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.drewtyler.com"><img src="https://avatars.githubusercontent.com/u/5640816?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Drew Tyler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=drewtyler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Lauviah0622"><img src="https://avatars.githubusercontent.com/u/43416399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Lauviah0622</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lauviah0622" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.elastic.co/elastic-agent"><img src="https://avatars.githubusercontent.com/u/1813008?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Josh Dover</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joshdover" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://phelm.co.uk"><img src="https://avatars.githubusercontent.com/u/4057948?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Phil Helm</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=phelma" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/lingyv-li"><img src="https://avatars.githubusercontent.com/u/8937944?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Larry Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lingyv-li" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@ A #recipe is a guide, tip or strategy for getting the most out of your Foam work
|
||||
|
||||
- Quick commits with VS Code's built in [[git-integration]]
|
||||
- Store your workspace in an auto-synced GitHub repo with [[write-your-notes-in-github-gist]]
|
||||
- Sync your GitHub repo automatically [[todo]].
|
||||
- Sync your GitHub repo automatically using the [GitDoc VSCode Plugin](https://marketplace.visualstudio.com/items?itemName=vsls-contrib.gitdoc).
|
||||
|
||||
## Publish
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Foam enables you to Link pages together using `[[file-name]]` annotations (i.e. `[[MediaWiki]]` links).
|
||||
|
||||
- Type `[[` and start typing a file name for autocompletion.
|
||||
- See [[link-formatting-and-autocompletion]] for more information, and how to setup your link autocompletions to make this easier.
|
||||
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on file name to navigate to file (`F12` also works while your cursor is on the file name)
|
||||
- `Cmd` + `Click` ( `Ctrl` + `Click` on Windows ) on non-existent file to create that file in the workspace.
|
||||
- The note creation makes use of the special [`new-note.md` note template](features/note-templates)
|
||||
@@ -22,7 +21,6 @@ The [Foam for VSCode](https://marketplace.visualstudio.com/items?itemName=foam.f
|
||||
|
||||
- [[foam-file-format]]
|
||||
- [[note-templates]]
|
||||
- [[link-formatting-and-autocompletion]]
|
||||
- See [[link-reference-definition-improvements]] for further discussion on current problems and potential solutions.
|
||||
|
||||
[//begin]: # "Autogenerated link references for markdown compatibility"
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "0.18.1"
|
||||
"version": "0.19.0"
|
||||
}
|
||||
|
||||
@@ -8,3 +8,5 @@ vsc-extension-quickstart.md
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
assets/screenshots
|
||||
node_modules
|
||||
|
||||
@@ -4,6 +4,40 @@ 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.19.0] - 2022-07-07
|
||||
|
||||
New Features:
|
||||
- Support for attachments (PDF) and images (#1027)
|
||||
- Support for opening day notes for other days as well (#1026, thanks @alper)
|
||||
|
||||
## [0.18.5] - 2022-06-29
|
||||
|
||||
Fixes and Improvements:
|
||||
- Support for `alias` YAML property to define note alias (#1014 - thanks @lingyv-li)
|
||||
|
||||
Internal:
|
||||
- Improved extension bundling (#1015 - thanks @lingyv-li)
|
||||
- Use `vscode.workspace.fs` instead of `fs` (#1005 - thanks @joshdover)
|
||||
|
||||
## [0.18.4] - 2022-06-03
|
||||
|
||||
Fixes and Improvements:
|
||||
- move past `]]` when writing wikilinks (#998 - thanks @Lauviah0622)
|
||||
- highlight improvements (#890 - thanks @memeplex)
|
||||
|
||||
## [0.18.3] - 2022-04-17
|
||||
|
||||
Fixes and Improvements:
|
||||
- Better reporting when links fail to resolve
|
||||
- Failing link resolution during graph computation no longer fatal
|
||||
|
||||
## [0.18.2] - 2022-04-14
|
||||
|
||||
Fixes and Improvements:
|
||||
- Fixed parsing error on empty direct links (#980 - thanks @chrisUsick)
|
||||
- Improved rendering in preview of wikilinks that have link definitions (#979 - thanks @josephdecock)
|
||||
- Restored handling of section-only wikilinks (#981)
|
||||
|
||||
## [0.18.1] - 2022-04-13
|
||||
|
||||
Fixes and Improvements:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "git"
|
||||
},
|
||||
"homepage": "https://github.com/foambubble/foam",
|
||||
"version": "0.18.1",
|
||||
"version": "0.19.0",
|
||||
"license": "MIT",
|
||||
"publisher": "foam",
|
||||
"engines": {
|
||||
@@ -62,25 +62,25 @@
|
||||
{
|
||||
"id": "foam-vscode.backlinks",
|
||||
"name": "Backlinks",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(references)",
|
||||
"contextualTitle": "Backlinks"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.tags-explorer",
|
||||
"name": "Tag Explorer",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(tag)",
|
||||
"contextualTitle": "Tags Explorer"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.orphans",
|
||||
"name": "Orphans",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(debug-gripper)",
|
||||
"contextualTitle": "Orphans"
|
||||
},
|
||||
{
|
||||
"id": "foam-vscode.placeholders",
|
||||
"name": "Placeholders",
|
||||
"icon": "media/dep.svg",
|
||||
"icon": "$(debug-disconnect)",
|
||||
"contextualTitle": "Placeholders"
|
||||
}
|
||||
]
|
||||
@@ -146,6 +146,10 @@
|
||||
{
|
||||
"command": "foam-vscode.open-resource",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.completion-move-cursor",
|
||||
"when": "false"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -164,6 +168,10 @@
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"title": "Foam: Open Today's Note"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note-for-date",
|
||||
"title": "Foam: Open Daily Note"
|
||||
},
|
||||
{
|
||||
@@ -213,6 +221,10 @@
|
||||
{
|
||||
"command": "foam-vscode.create-new-template",
|
||||
"title": "Foam: Create New Template"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.completion-move-cursor",
|
||||
"title": "Foam: Move cursor after completion"
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
@@ -265,11 +277,6 @@
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"foam.decorations.links.enable": {
|
||||
"description": "Enable decorations for links",
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"foam.openDailyNote.onStartup": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
@@ -358,6 +365,11 @@
|
||||
],
|
||||
"description": "Whether or not to navigate to the target daily note when a daily note snippet is selected."
|
||||
},
|
||||
"foam.preview.embedNoteInContainer": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Wrap embedded notes in a container when displayed in preview panel"
|
||||
},
|
||||
"foam.graph.titleMaxLength": {
|
||||
"type": "number",
|
||||
"default": 24,
|
||||
@@ -374,6 +386,10 @@
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note",
|
||||
"key": "alt+d"
|
||||
},
|
||||
{
|
||||
"command": "foam-vscode.open-daily-note-for-date",
|
||||
"key": "alt+h"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -389,10 +405,9 @@
|
||||
"clean": "rimraf out",
|
||||
"watch": "tsc --build ./tsconfig.json --watch",
|
||||
"vscode:start-debugging": "yarn clean && yarn watch",
|
||||
"vscode:prepublish": "yarn npm-install && yarn run build",
|
||||
"npm-install": "rimraf node_modules && npm i",
|
||||
"npm-cleanup": "rimraf package-lock.json node_modules && yarn",
|
||||
"package-extension": "npx vsce package && yarn npm-cleanup",
|
||||
"esbuild-base": "esbuild ./src/extension.ts --bundle --outfile=out/extension.js --external:vscode --format=cjs --platform=node",
|
||||
"vscode:prepublish": "yarn run esbuild-base -- --minify",
|
||||
"package-extension": "npx vsce package --yarn",
|
||||
"install-extension": "code --install-extension ./foam-vscode-$npm_package_version.vsix",
|
||||
"publish-extension-openvsx": "npx ovsx publish foam-vscode-$npm_package_version.vsix -p $OPENVSX_TOKEN",
|
||||
"publish-extension-vscode": "npx vsce publish --packagePath foam-vscode-$npm_package_version.vsix",
|
||||
@@ -410,6 +425,7 @@
|
||||
"@types/vscode": "^1.47.1",
|
||||
"@typescript-eslint/eslint-plugin": "^2.30.0",
|
||||
"@typescript-eslint/parser": "^2.30.0",
|
||||
"esbuild": "^0.14.45",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-import-resolver-typescript": "^2.5.0",
|
||||
"eslint-plugin-import": "^2.24.2",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { generateHeading } from '.';
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { MarkdownResourceProvider } from '../services/markdown-provider';
|
||||
import { bootstrap } from '../model/foam';
|
||||
import { Resource } from '../model/note';
|
||||
@@ -20,8 +20,9 @@ describe('generateHeadings', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore);
|
||||
const foam = await bootstrap(matcher, dataStore, [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { Range } from '../model/range';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { FileDataStore, Matcher } from '../services/datastore';
|
||||
import { Logger } from '../utils/log';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
|
||||
Logger.setLevel('error');
|
||||
|
||||
@@ -21,8 +23,12 @@ describe('generateLinkReferences', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
const matcher = new Matcher([TEST_DATA_DIR.joinPath('__scaffold__')]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
const readFile = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore);
|
||||
const foam = await bootstrap(matcher, dataStore, [mdProvider]);
|
||||
_workspace = foam.workspace;
|
||||
});
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export const bootstrap = async (
|
||||
const tsWsDone = Date.now();
|
||||
Logger.info(`Workspace loaded in ${tsWsDone - tsStart}ms`);
|
||||
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true, 500);
|
||||
const graph = FoamGraph.fromWorkspace(workspace, true);
|
||||
const tsGraphDone = Date.now();
|
||||
Logger.info(`Graph loaded in ${tsGraphDone - tsWsDone}ms`);
|
||||
|
||||
|
||||
@@ -107,8 +107,18 @@ export class FoamGraph implements IDisposable {
|
||||
|
||||
for (const resource of this.workspace.resources()) {
|
||||
for (const link of resource.links) {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
try {
|
||||
const targetUri = this.workspace.resolveLink(resource, link);
|
||||
this.connect(resource.uri, targetUri, link);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while resolving link ${
|
||||
link.rawText
|
||||
} in ${resource.uri.toFsPath()}, skipping.`,
|
||||
link,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,11 @@ export interface Tag {
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Alias {
|
||||
title: string;
|
||||
range: Range;
|
||||
}
|
||||
|
||||
export interface Section {
|
||||
label: string;
|
||||
range: Range;
|
||||
@@ -39,6 +44,7 @@ export interface Resource {
|
||||
properties: any;
|
||||
sections: Section[];
|
||||
tags: Tag[];
|
||||
aliases: Alias[];
|
||||
links: ResourceLink[];
|
||||
|
||||
// TODO to remove
|
||||
@@ -65,6 +71,7 @@ export abstract class Resource {
|
||||
typeof (thing as Resource).type === 'string' &&
|
||||
typeof (thing as Resource).properties === 'object' &&
|
||||
typeof (thing as Resource).tags === 'object' &&
|
||||
typeof (thing as Resource).aliases === 'object' &&
|
||||
typeof (thing as Resource).links === 'object'
|
||||
);
|
||||
}
|
||||
|
||||
111
packages/foam-vscode/src/core/services/attachment-provider.ts
Normal file
111
packages/foam-vscode/src/core/services/attachment-provider.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { Resource, ResourceLink } from '../model/note';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, IMatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { Position } from '../model/position';
|
||||
|
||||
const imageExtensions = ['.png', '.jpg', '.gif'];
|
||||
const attachmentExtensions = ['.pdf', ...imageExtensions];
|
||||
|
||||
const asResource = (uri: URI): Resource => {
|
||||
const type = imageExtensions.includes(uri.getExtension())
|
||||
? 'image'
|
||||
: 'attachment';
|
||||
return {
|
||||
uri: uri,
|
||||
title: uri.getBasename(),
|
||||
type: type,
|
||||
aliases: [],
|
||||
properties: { type: type },
|
||||
sections: [],
|
||||
links: [],
|
||||
tags: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
contentStart: Position.create(0, 0),
|
||||
end: Position.create(0, 0),
|
||||
eol: '\n',
|
||||
text: '',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export class AttachmentResourceProvider implements ResourceProvider {
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly watcherInit?: (triggers: {
|
||||
onDidChange: (uri: URI) => void;
|
||||
onDidCreate: (uri: URI) => void;
|
||||
onDidDelete: (uri: URI) => void;
|
||||
}) => IDisposable[]
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
const filesByFolder = await Promise.all(
|
||||
this.matcher.include.map(glob =>
|
||||
this.dataStore.list(glob, this.matcher.exclude)
|
||||
)
|
||||
);
|
||||
const files = this.matcher
|
||||
.match(filesByFolder.flat())
|
||||
.filter(this.supports);
|
||||
|
||||
for (const uri of files) {
|
||||
Logger.info('Found: ' + uri.toString());
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
|
||||
this.disposables =
|
||||
this.watcherInit?.({
|
||||
onDidChange: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
},
|
||||
onDidCreate: async uri => {
|
||||
if (this.matcher.isMatch(uri) && this.supports(uri)) {
|
||||
workspace.set(asResource(uri));
|
||||
}
|
||||
},
|
||||
onDidDelete: uri => {
|
||||
this.supports(uri) && workspace.delete(uri);
|
||||
},
|
||||
}) ?? [];
|
||||
}
|
||||
|
||||
supports(uri: URI) {
|
||||
return attachmentExtensions.includes(uri.getExtension());
|
||||
}
|
||||
|
||||
read(uri: URI): Promise<string | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async readAsMarkdown(uri: URI): Promise<string | null> {
|
||||
if (imageExtensions.includes(uri.getExtension())) {
|
||||
return `}|height=200)`;
|
||||
}
|
||||
return `### ${uri.getBasename()}`;
|
||||
}
|
||||
|
||||
async fetch(uri: URI) {
|
||||
return asResource(uri);
|
||||
}
|
||||
|
||||
resolveLink(w: FoamWorkspace, resource: Resource, l: ResourceLink) {
|
||||
throw new Error('not supported');
|
||||
// Silly workaround to make VS Code and es-lint happy
|
||||
// eslint-disable-next-line
|
||||
return resource.uri;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { readFileFromFs, TEST_DATA_DIR } from '../../test/test-utils';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { FileDataStore, Matcher, toMatcherPathFormat } from './datastore';
|
||||
@@ -87,7 +87,7 @@ describe('Matcher', () => {
|
||||
describe('Datastore', () => {
|
||||
it('uses the matcher to get the file list', async () => {
|
||||
const matcher = new Matcher([testFolder], ['**/*.md'], []);
|
||||
const ds = new FileDataStore();
|
||||
const ds = new FileDataStore(readFileFromFs);
|
||||
expect((await ds.list(matcher.include[0])).length).toEqual(4);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import micromatch from 'micromatch';
|
||||
import fs from 'fs';
|
||||
import { URI } from '../model/uri';
|
||||
import { Logger } from '../utils/log';
|
||||
import { glob } from 'glob';
|
||||
@@ -115,6 +114,8 @@ export interface IDataStore {
|
||||
* File system based data store
|
||||
*/
|
||||
export class FileDataStore implements IDataStore {
|
||||
constructor(private readFile: (uri: URI) => Promise<string>) {}
|
||||
|
||||
async list(glob: string, ignoreGlob?: string | string[]): Promise<URI[]> {
|
||||
const res = await findAllFiles(glob, {
|
||||
ignore: ignoreGlob,
|
||||
@@ -125,7 +126,7 @@ export class FileDataStore implements IDataStore {
|
||||
|
||||
async read(uri: URI) {
|
||||
try {
|
||||
return (await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
return await this.readFile(uri);
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`FileDataStore: error while reading uri: ${uri.path} - ${e}`
|
||||
|
||||
@@ -12,8 +12,8 @@ describe('MarkdownLink', () => {
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.alias).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse target and section', () => {
|
||||
const link = parser.parse(
|
||||
@@ -23,14 +23,14 @@ describe('MarkdownLink', () => {
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toBeUndefined();
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse target and alias', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[wikilink|alias]]`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias');
|
||||
});
|
||||
it('should parse links with square brackets #975', () => {
|
||||
@@ -40,8 +40,8 @@ describe('MarkdownLink', () => {
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink [with] brackets');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.alias).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
it('should parse links with square brackets in alias #975', () => {
|
||||
const link = parser.parse(
|
||||
@@ -50,7 +50,7 @@ describe('MarkdownLink', () => {
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias [with] brackets');
|
||||
});
|
||||
it('should parse target and alias with escaped separator', () => {
|
||||
@@ -60,7 +60,7 @@ describe('MarkdownLink', () => {
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('wikilink');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('alias');
|
||||
});
|
||||
it('should parse target section and alias', () => {
|
||||
@@ -77,9 +77,9 @@ describe('MarkdownLink', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [[#section]]`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toBeUndefined();
|
||||
expect(parsed.target).toEqual('');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toBeUndefined();
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('MarkdownLink', () => {
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('link');
|
||||
});
|
||||
it('should parse target and section', () => {
|
||||
@@ -109,7 +109,7 @@ describe('MarkdownLink', () => {
|
||||
range: Range.create(0, 0),
|
||||
};
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toBeUndefined();
|
||||
expect(parsed.target).toEqual('');
|
||||
expect(parsed.section).toEqual('section');
|
||||
expect(parsed.alias).toEqual('link');
|
||||
});
|
||||
@@ -120,9 +120,17 @@ describe('MarkdownLink', () => {
|
||||
).links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toBeUndefined();
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('inbox [xyz]');
|
||||
});
|
||||
it('should parse links with empty label #975', () => {
|
||||
const link = parser.parse(getRandomURI(), `this is a [](to/path.md)`)
|
||||
.links[0];
|
||||
const parsed = MarkdownLink.analyzeLink(link);
|
||||
expect(parsed.target).toEqual('to/path.md');
|
||||
expect(parsed.section).toEqual('');
|
||||
expect(parsed.alias).toEqual('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('rename wikilink', () => {
|
||||
|
||||
@@ -5,27 +5,35 @@ export abstract class MarkdownLink {
|
||||
/\[\[([^#|]+)?#?([^|]+)?\|?(.*)?\]\]/
|
||||
);
|
||||
private static directLinkRegex = new RegExp(
|
||||
/\[(.+)\]\(([^#]*)?#?([^\]]+)?\)/
|
||||
/\[(.*)\]\(([^#]*)?#?([^\]]+)?\)/
|
||||
);
|
||||
|
||||
public static analyzeLink(link: ResourceLink) {
|
||||
if (link.type === 'wikilink') {
|
||||
const [, target, section, alias] = this.wikilinkRegex.exec(link.rawText);
|
||||
return {
|
||||
target: target?.replace(/\\/g, ''),
|
||||
section,
|
||||
alias,
|
||||
};
|
||||
try {
|
||||
if (link.type === 'wikilink') {
|
||||
const [, target, section, alias] = this.wikilinkRegex.exec(
|
||||
link.rawText
|
||||
);
|
||||
return {
|
||||
target: target?.replace(/\\/g, '') ?? '',
|
||||
section: section ?? '',
|
||||
alias: alias ?? '',
|
||||
};
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
const [, alias, target, section] = this.directLinkRegex.exec(
|
||||
link.rawText
|
||||
);
|
||||
return {
|
||||
target: target ?? '',
|
||||
section: section ?? '',
|
||||
alias: alias ?? '',
|
||||
};
|
||||
}
|
||||
throw new Error(`Link of type ${link.type} is not supported`);
|
||||
} catch (e) {
|
||||
throw new Error(`Couldn't parse link ${link.rawText} - ${e}`);
|
||||
}
|
||||
if (link.type === 'link') {
|
||||
const [, alias, target, section] = this.directLinkRegex.exec(
|
||||
link.rawText
|
||||
);
|
||||
return { target, section, alias };
|
||||
}
|
||||
throw new Error(
|
||||
`Unexpected state: link of type ${link.type} is not supported`
|
||||
);
|
||||
}
|
||||
|
||||
public static createUpdateLinkEdit(
|
||||
|
||||
@@ -380,4 +380,61 @@ and some content`
|
||||
expect(note2.properties.hasHeading).toBeTruthy();
|
||||
});
|
||||
});
|
||||
describe('Alias', () => {
|
||||
it('can find tags in comma separated string', () => {
|
||||
const note = parser.parse(
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
---
|
||||
alias: alias 1, alias 2 , alias3
|
||||
---
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
`
|
||||
);
|
||||
expect(note.aliases).toEqual([
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias 1',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias 2',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 3, 3),
|
||||
title: 'alias3',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
it('can find tags in yaml array', () => {
|
||||
const note = parser.parse(
|
||||
URI.file('/path/to/a'),
|
||||
`
|
||||
---
|
||||
alias:
|
||||
- alias 1
|
||||
- alias 2
|
||||
- alias3
|
||||
---
|
||||
This is a test note without headings.
|
||||
But with some content.
|
||||
`
|
||||
);
|
||||
expect(note.aliases).toEqual([
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias 1',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias 2',
|
||||
},
|
||||
{
|
||||
range: Range.create(1, 0, 6, 3),
|
||||
title: 'alias3',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -40,6 +40,7 @@ export function createMarkdownParser(
|
||||
wikilinkPlugin,
|
||||
definitionsPlugin,
|
||||
tagsPlugin,
|
||||
aliasesPlugin,
|
||||
sectionsPlugin,
|
||||
...extraPlugins,
|
||||
];
|
||||
@@ -72,6 +73,7 @@ export function createMarkdownParser(
|
||||
title: '',
|
||||
sections: [],
|
||||
tags: [],
|
||||
aliases: [],
|
||||
links: [],
|
||||
definitions: [],
|
||||
source: {
|
||||
@@ -255,6 +257,23 @@ const titlePlugin: ParserPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
const aliasesPlugin: ParserPlugin = {
|
||||
name: 'aliases',
|
||||
onDidFindProperties: (props, note, node) => {
|
||||
if (isSome(props.alias)) {
|
||||
const aliases = Array.isArray(props.alias)
|
||||
? props.alias
|
||||
: props.alias.split(',').map(m => m.trim());
|
||||
for (const alias of aliases) {
|
||||
note.aliases.push({
|
||||
title: alias,
|
||||
range: astPositionToFoamRange(node.position!),
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const wikilinkPlugin: ParserPlugin = {
|
||||
name: 'wikilink',
|
||||
visit: (node, note, noteSource) => {
|
||||
|
||||
@@ -147,6 +147,39 @@ describe('Link resolution', () => {
|
||||
expect(ws.resolveLink(noteA, noteA.links[1])).toEqual(noteC.uri);
|
||||
expect(ws.resolveLink(noteA, noteA.links[2])).toEqual(noteD.uri);
|
||||
});
|
||||
|
||||
it('should resolve wikilink with section identifier', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
// uppercased filename, lowercased slug
|
||||
{ slug: 'page-b#section' },
|
||||
],
|
||||
});
|
||||
const noteB = createTestNote({ uri: '/somewhere/PAGE-B.md' });
|
||||
const ws = createTestWorkspace()
|
||||
.set(noteA)
|
||||
.set(noteB);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteB.uri.withFragment('section')
|
||||
);
|
||||
});
|
||||
|
||||
it('should resolve section-only wikilinks', () => {
|
||||
const noteA = createTestNote({
|
||||
uri: '/path/to/page-a.md',
|
||||
links: [
|
||||
// uppercased filename, lowercased slug
|
||||
{ slug: '#section' },
|
||||
],
|
||||
});
|
||||
const ws = createTestWorkspace().set(noteA);
|
||||
|
||||
expect(ws.resolveLink(noteA, noteA.links[0])).toEqual(
|
||||
noteA.uri.withFragment('section')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Markdown direct links', () => {
|
||||
|
||||
@@ -8,7 +8,7 @@ import { isNone, isSome } from '../utils';
|
||||
import { Logger } from '../utils/log';
|
||||
import { URI } from '../model/uri';
|
||||
import { FoamWorkspace } from '../model/workspace';
|
||||
import { IDataStore, FileDataStore, IMatcher } from '../services/datastore';
|
||||
import { IDataStore, IMatcher } from '../services/datastore';
|
||||
import { IDisposable } from '../common/lifecycle';
|
||||
import { ResourceProvider } from '../model/provider';
|
||||
import { createMarkdownParser } from './markdown-parser';
|
||||
@@ -19,13 +19,13 @@ export class MarkdownResourceProvider implements ResourceProvider {
|
||||
|
||||
constructor(
|
||||
private readonly matcher: IMatcher,
|
||||
private readonly dataStore: IDataStore,
|
||||
private readonly watcherInit?: (triggers: {
|
||||
onDidChange: (uri: URI) => void;
|
||||
onDidCreate: (uri: URI) => void;
|
||||
onDidDelete: (uri: URI) => void;
|
||||
}) => IDisposable[],
|
||||
private readonly parser: ResourceParser = createMarkdownParser([]),
|
||||
private readonly dataStore: IDataStore = new FileDataStore()
|
||||
private readonly parser: ResourceParser = createMarkdownParser([])
|
||||
) {}
|
||||
|
||||
async init(workspace: FoamWorkspace) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CharCode } from '../common/charCode';
|
||||
import { posix } from 'path';
|
||||
import { promises, constants } from 'fs';
|
||||
|
||||
/**
|
||||
* Converts filesystem path to POSIX path. Supported inputs are:
|
||||
@@ -147,21 +146,6 @@ export function relativeTo(path: string, basePath: string): string {
|
||||
return posix.relative(basePath, path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously checks if there is an accessible file for a path.
|
||||
*
|
||||
* @param fsPath A filesystem-specific path.
|
||||
* @returns true if an accesible file exists, false otherwise.
|
||||
*/
|
||||
export async function existsInFs(fsPath: string) {
|
||||
try {
|
||||
await promises.access(fsPath, constants.F_OK);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function hasDrive(path: string, idx = 0): boolean {
|
||||
if (path.length <= idx) {
|
||||
return false;
|
||||
|
||||
@@ -11,12 +11,14 @@ import { NoteFactory } from './services/templates';
|
||||
* In the case that the daily note file does not exist,
|
||||
* it gets created along with any folders in its path.
|
||||
*
|
||||
* @param date A given date to be formatted as filename.
|
||||
* @param date The target date. If not provided, the function returns immediately.
|
||||
*/
|
||||
export async function openDailyNoteFor(date?: Date) {
|
||||
const targetDate = date instanceof Date ? date : new Date();
|
||||
if (date == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { didCreateFile, uri } = await createDailyNoteIfNotExists(targetDate);
|
||||
const { didCreateFile, uri } = await createDailyNoteIfNotExists(date);
|
||||
// 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
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import { workspace, ExtensionContext, window } from 'vscode';
|
||||
import { MarkdownResourceProvider } from './core/services/markdown-provider';
|
||||
import { bootstrap } from './core/model/foam';
|
||||
import { URI } from './core/model/uri';
|
||||
import { FileDataStore, Matcher } from './core/services/datastore';
|
||||
import { Logger } from './core/utils/log';
|
||||
|
||||
import { features } from './features';
|
||||
import { VsCodeOutputLogger, exposeLogger } from './services/logging';
|
||||
import { getIgnoredFilesSetting } from './settings';
|
||||
import { fromVsCodeUri } from './utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from './utils/vsc-utils';
|
||||
import { AttachmentResourceProvider } from './core/services/attachment-provider';
|
||||
|
||||
export async function activate(context: ExtensionContext) {
|
||||
const logger = new VsCodeOutputLogger();
|
||||
@@ -18,30 +20,56 @@ export async function activate(context: ExtensionContext) {
|
||||
Logger.info('Starting Foam');
|
||||
|
||||
// Prepare Foam
|
||||
const dataStore = new FileDataStore();
|
||||
const readFile = async (uri: URI) =>
|
||||
(await workspace.fs.readFile(toVsCodeUri(uri))).toString();
|
||||
const dataStore = new FileDataStore(readFile);
|
||||
const matcher = new Matcher(
|
||||
workspace.workspaceFolders.map(dir => fromVsCodeUri(dir.uri)),
|
||||
['**/*'],
|
||||
getIgnoredFilesSetting().map(g => g.toString())
|
||||
);
|
||||
const markdownProvider = new MarkdownResourceProvider(matcher, triggers => {
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
return [
|
||||
watcher.onDidChange(uri => triggers.onDidChange(fromVsCodeUri(uri))),
|
||||
watcher.onDidCreate(uri => triggers.onDidCreate(fromVsCodeUri(uri))),
|
||||
watcher.onDidDelete(uri => triggers.onDidDelete(fromVsCodeUri(uri))),
|
||||
watcher,
|
||||
];
|
||||
});
|
||||
const watcher = workspace.createFileSystemWatcher('**/*');
|
||||
const markdownProvider = new MarkdownResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
triggers => {
|
||||
return [
|
||||
watcher.onDidChange(uri => triggers.onDidChange(fromVsCodeUri(uri))),
|
||||
watcher.onDidCreate(uri => triggers.onDidCreate(fromVsCodeUri(uri))),
|
||||
watcher.onDidDelete(uri => triggers.onDidDelete(fromVsCodeUri(uri))),
|
||||
watcher,
|
||||
];
|
||||
}
|
||||
);
|
||||
const attachmentProvider = new AttachmentResourceProvider(
|
||||
matcher,
|
||||
dataStore,
|
||||
triggers => {
|
||||
return [
|
||||
watcher.onDidChange(uri => triggers.onDidChange(fromVsCodeUri(uri))),
|
||||
watcher.onDidCreate(uri => triggers.onDidCreate(fromVsCodeUri(uri))),
|
||||
watcher.onDidDelete(uri => triggers.onDidDelete(fromVsCodeUri(uri))),
|
||||
watcher,
|
||||
];
|
||||
}
|
||||
);
|
||||
|
||||
const foamPromise = bootstrap(matcher, dataStore, [markdownProvider]);
|
||||
const foamPromise = bootstrap(matcher, dataStore, [
|
||||
markdownProvider,
|
||||
attachmentProvider,
|
||||
]);
|
||||
|
||||
// Load the features
|
||||
const resPromises = features.map(f => f.activate(context, foamPromise));
|
||||
|
||||
const foam = await foamPromise;
|
||||
Logger.info(`Loaded ${foam.workspace.list().length} notes`);
|
||||
context.subscriptions.push(foam, markdownProvider);
|
||||
context.subscriptions.push(
|
||||
foam,
|
||||
watcher,
|
||||
markdownProvider,
|
||||
attachmentProvider
|
||||
);
|
||||
|
||||
const res = (await Promise.all(resPromises)).filter(r => r != null);
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { Resource, ResourceLink } from '../core/model/note';
|
||||
import { Range } from '../core/model/range';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -133,7 +133,7 @@ export class BacklinkTreeItem extends vscode.TreeItem {
|
||||
this.label = `${link.range.start.line}: ${this.label}`;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [resource.uri, { selection: link.range }],
|
||||
arguments: [toVsCodeUri(resource.uri), { selection: link.range }],
|
||||
title: 'Go to link',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
import { debounce } from 'lodash';
|
||||
import * as vscode from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import {
|
||||
ConfigurationMonitor,
|
||||
monitorFoamVsCodeConfig,
|
||||
} from '../services/config';
|
||||
import { ResourceParser } from '../core/model/note';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Range } from '../core/model/range';
|
||||
import { fromVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const CONFIG_KEY = 'decorations.links.enable';
|
||||
|
||||
const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
rangeBehavior: vscode.DecorationRangeBehavior.ClosedClosed,
|
||||
textDecoration: 'none',
|
||||
@@ -21,15 +15,10 @@ const placeholderDecoration = vscode.window.createTextEditorDecorationType({
|
||||
});
|
||||
|
||||
const updateDecorations = (
|
||||
areDecorationsEnabled: () => boolean,
|
||||
parser: ResourceParser,
|
||||
workspace: FoamWorkspace
|
||||
) => (editor: vscode.TextEditor) => {
|
||||
if (
|
||||
!editor ||
|
||||
!areDecorationsEnabled() ||
|
||||
editor.document.languageId !== 'markdown'
|
||||
) {
|
||||
if (!editor || editor.document.languageId !== 'markdown') {
|
||||
return;
|
||||
}
|
||||
const note = parser.parse(
|
||||
@@ -43,9 +32,9 @@ const updateDecorations = (
|
||||
placeholderRanges.push(
|
||||
Range.create(
|
||||
link.range.start.line,
|
||||
link.range.start.character + 2,
|
||||
link.range.start.character + (link.type === 'wikilink' ? 2 : 0),
|
||||
link.range.end.line,
|
||||
link.range.end.character - 2
|
||||
link.range.end.character - (link.type === 'wikilink' ? 2 : 0)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -58,14 +47,10 @@ const feature: FoamFeature = {
|
||||
context: vscode.ExtensionContext,
|
||||
foamPromise: Promise<Foam>
|
||||
) => {
|
||||
const areDecorationsEnabled: ConfigurationMonitor<boolean> = monitorFoamVsCodeConfig(
|
||||
CONFIG_KEY
|
||||
);
|
||||
const foam = await foamPromise;
|
||||
let activeEditor = vscode.window.activeTextEditor;
|
||||
|
||||
const immediatelyUpdateDecorations = updateDecorations(
|
||||
areDecorationsEnabled,
|
||||
foam.services.parser,
|
||||
foam.workspace
|
||||
);
|
||||
@@ -78,7 +63,6 @@ const feature: FoamFeature = {
|
||||
immediatelyUpdateDecorations(activeEditor);
|
||||
|
||||
context.subscriptions.push(
|
||||
areDecorationsEnabled,
|
||||
placeholderDecoration,
|
||||
vscode.window.onDidChangeActiveTextEditor(editor => {
|
||||
activeEditor = editor;
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createMarkdownParser } from '../core/services/markdown-parser';
|
||||
import { MarkdownResourceProvider } from '../core/services/markdown-provider';
|
||||
import { FoamGraph } from '../core/model/graph';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Matcher } from '../core/services/datastore';
|
||||
import { FileDataStore, Matcher } from '../core/services/datastore';
|
||||
import {
|
||||
cleanWorkspace,
|
||||
closeEditors,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from '../test/test-utils-vscode';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { HoverProvider } from './hover-provider';
|
||||
import { readFileFromFs } from '../test/test-utils';
|
||||
|
||||
// We can't use createTestWorkspace from /packages/foam-vscode/src/test/test-utils.ts
|
||||
// because we need a MarkdownResourceProvider with a real instance of FileDataStore.
|
||||
@@ -19,7 +20,8 @@ const createWorkspace = () => {
|
||||
const matcher = new Matcher(
|
||||
vscode.workspace.workspaceFolders.map(f => fromVsCodeUri(f.uri))
|
||||
);
|
||||
const resourceProvider = new MarkdownResourceProvider(matcher);
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const resourceProvider = new MarkdownResourceProvider(matcher, dataStore);
|
||||
const workspace = new FoamWorkspace();
|
||||
workspace.registerProvider(resourceProvider);
|
||||
return workspace;
|
||||
|
||||
@@ -13,11 +13,12 @@ import backlinks from './backlinks';
|
||||
import utilityCommands from './utility-commands';
|
||||
import hoverProvider from './hover-provider';
|
||||
import previewNavigation from './preview-navigation';
|
||||
import completionProvider from './link-completion';
|
||||
import completionProvider, { completionCursorMove } from './link-completion';
|
||||
import tagCompletionProvider from './tag-completion';
|
||||
import linkDecorations from './document-decorator';
|
||||
import navigationProviders from './navigation-provider';
|
||||
import wikilinkDiagnostics from './wikilink-diagnostics';
|
||||
// import completionMoveCursor from './completion-cursor-move';
|
||||
import refactor from './refactor';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
@@ -43,4 +44,5 @@ export const features: FoamFeature[] = [
|
||||
previewNavigation,
|
||||
completionProvider,
|
||||
tagCompletionProvider,
|
||||
completionCursorMove,
|
||||
];
|
||||
|
||||
@@ -5,14 +5,17 @@ import {
|
||||
commands,
|
||||
ProgressLocation,
|
||||
} from 'vscode';
|
||||
import * as fs from 'fs';
|
||||
import { FoamFeature } from '../types';
|
||||
|
||||
import {
|
||||
getWikilinkDefinitionSetting,
|
||||
LinkReferenceDefinitionsSetting,
|
||||
} from '../settings';
|
||||
import { toVsCodePosition, toVsCodeRange } from '../utils/vsc-utils';
|
||||
import {
|
||||
toVsCodePosition,
|
||||
toVsCodeRange,
|
||||
toVsCodeUri,
|
||||
} from '../utils/vsc-utils';
|
||||
import { Foam } from '../core/model/foam';
|
||||
import { Resource } from '../core/model/note';
|
||||
import { generateHeading, generateLinkReferences } from '../core/janitor';
|
||||
@@ -125,7 +128,7 @@ async function runJanitor(foam: Foam) {
|
||||
text = definitions ? applyTextEdit(text, definitions) : text;
|
||||
text = heading ? applyTextEdit(text, heading) : text;
|
||||
|
||||
return fs.promises.writeFile(note.uri.toFsPath(), text);
|
||||
return workspace.fs.writeFile(toVsCodeUri(note.uri), Buffer.from(text));
|
||||
});
|
||||
|
||||
await Promise.all(fileWritePromises);
|
||||
|
||||
@@ -157,4 +157,30 @@ Content of section 2
|
||||
new Set(['Section 1', 'Section 2'])
|
||||
);
|
||||
});
|
||||
|
||||
it('should return page alias', async () => {
|
||||
const { uri, content } = await createFile(
|
||||
`
|
||||
---
|
||||
alias: alias-a
|
||||
---
|
||||
[[
|
||||
`,
|
||||
['new-note-with-alias.md']
|
||||
);
|
||||
ws.set(parser.parse(uri, content));
|
||||
|
||||
const { doc } = await showInEditor(uri);
|
||||
const provider = new CompletionProvider(ws, graph);
|
||||
|
||||
const links = await provider.provideCompletionItems(
|
||||
doc,
|
||||
new vscode.Position(4, 2)
|
||||
);
|
||||
|
||||
const aliasCompletionItem = links.items.find(i => i.label === 'alias-a');
|
||||
expect(aliasCompletionItem).not.toBeNull();
|
||||
expect(aliasCompletionItem.label).toBe('alias-a');
|
||||
expect(aliasCompletionItem.insertText).toBe('new-note-with-alias|alias-a');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,15 @@ import { FoamFeature } from '../types';
|
||||
import { getNoteTooltip, mdDocSelector } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
|
||||
export const aliasCommitCharacters = ['#'];
|
||||
export const linkCommitCharacters = ['#', '|'];
|
||||
export const sectionCommitCharacters = ['|'];
|
||||
|
||||
const COMPLETION_CURSOR_MOVE = {
|
||||
command: 'foam-vscode.completion-move-cursor',
|
||||
title: 'Foam: Move cursor after completion',
|
||||
};
|
||||
|
||||
export const WIKILINK_REGEX = /\[\[[^[\]]*(?!.*\]\])/;
|
||||
export const SECTION_REGEX = /\[\[([^[\]]*#(?!.*\]\]))/;
|
||||
|
||||
@@ -31,6 +40,65 @@ const feature: FoamFeature = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* always jump to the closing bracket, but jump back the cursor when commit
|
||||
* by alias divider `|` and section divider `#`
|
||||
* See https://github.com/foambubble/foam/issues/962,
|
||||
*/
|
||||
|
||||
export const completionCursorMove: FoamFeature = {
|
||||
activate: (context: vscode.ExtensionContext, foamPromise: Promise<Foam>) => {
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand(
|
||||
COMPLETION_CURSOR_MOVE.command,
|
||||
async () => {
|
||||
const activeEditor = vscode.window.activeTextEditor;
|
||||
const document = activeEditor.document;
|
||||
const currentPosition = activeEditor.selection.active;
|
||||
const cursorChange = vscode.window.onDidChangeTextEditorSelection(
|
||||
async e => {
|
||||
const changedPosition = e.selections[0].active;
|
||||
const preChar = document
|
||||
.lineAt(changedPosition.line)
|
||||
.text.charAt(changedPosition.character - 1);
|
||||
|
||||
const {
|
||||
character: selectionChar,
|
||||
line: selectionLine,
|
||||
} = e.selections[0].active;
|
||||
|
||||
const {
|
||||
line: completionLine,
|
||||
character: completionChar,
|
||||
} = currentPosition;
|
||||
|
||||
const inCompleteBySectionDivider =
|
||||
linkCommitCharacters.includes(preChar) &&
|
||||
selectionLine === completionLine &&
|
||||
selectionChar === completionChar + 1;
|
||||
|
||||
cursorChange.dispose();
|
||||
if (inCompleteBySectionDivider) {
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'left',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
await vscode.commands.executeCommand('cursorMove', {
|
||||
to: 'right',
|
||||
by: 'character',
|
||||
value: 2,
|
||||
});
|
||||
}
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export class SectionCompletionProvider
|
||||
implements vscode.CompletionItemProvider<vscode.CompletionItem> {
|
||||
constructor(private ws: FoamWorkspace) {}
|
||||
@@ -70,6 +138,8 @@ export class SectionCompletionProvider
|
||||
);
|
||||
item.sortText = String(b.range.start.line).padStart(5, '0');
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = sectionCommitCharacters;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
return item;
|
||||
});
|
||||
return new vscode.CompletionList(items);
|
||||
@@ -104,12 +174,12 @@ export class CompletionProvider
|
||||
// Requires autocomplete only if cursorPrefix matches `[[` that NOT ended by `]]`.
|
||||
// See https://github.com/foambubble/foam/pull/596#issuecomment-825748205 for details.
|
||||
const requiresAutocomplete = cursorPrefix.match(WIKILINK_REGEX);
|
||||
|
||||
if (!requiresAutocomplete || requiresAutocomplete[0].indexOf('#') >= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const text = requiresAutocomplete[0];
|
||||
|
||||
const replacementRange = new vscode.Range(
|
||||
position.line,
|
||||
position.character - (text.length - 2),
|
||||
@@ -123,12 +193,32 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.File,
|
||||
resource.uri
|
||||
);
|
||||
item.sortText =
|
||||
resource.type === 'attachment' ? `1-${item.label}` : `0-${item.label}`;
|
||||
item.filterText = resource.uri.getName();
|
||||
item.insertText = this.ws.getIdentifier(resource.uri);
|
||||
item.range = replacementRange;
|
||||
item.commitCharacters = ['#'];
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.commitCharacters = linkCommitCharacters;
|
||||
return item;
|
||||
});
|
||||
const aliases = this.ws.list().flatMap(resource =>
|
||||
resource.aliases.map(a => {
|
||||
const item = new ResourceCompletionItem(
|
||||
a.title,
|
||||
vscode.CompletionItemKind.Reference,
|
||||
resource.uri
|
||||
);
|
||||
item.insertText = this.ws.getIdentifier(resource.uri) + '|' + a.title;
|
||||
item.detail = `Alias of ${vscode.workspace.asRelativePath(
|
||||
toVsCodeUri(resource.uri)
|
||||
)}`;
|
||||
item.range = replacementRange;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.commitCharacters = aliasCommitCharacters;
|
||||
return item;
|
||||
})
|
||||
);
|
||||
const placeholders = Array.from(this.graph.placeholders.values()).map(
|
||||
uri => {
|
||||
const item = new vscode.CompletionItem(
|
||||
@@ -136,12 +226,17 @@ export class CompletionProvider
|
||||
vscode.CompletionItemKind.Interface
|
||||
);
|
||||
item.insertText = uri.path;
|
||||
item.command = COMPLETION_CURSOR_MOVE;
|
||||
item.range = replacementRange;
|
||||
return item;
|
||||
}
|
||||
);
|
||||
|
||||
return new vscode.CompletionList([...resources, ...placeholders]);
|
||||
return new vscode.CompletionList([
|
||||
...resources,
|
||||
...aliases,
|
||||
...placeholders,
|
||||
]);
|
||||
}
|
||||
|
||||
resolveCompletionItem(
|
||||
|
||||
@@ -128,7 +128,14 @@ export class NavigationProvider
|
||||
: Range.createFromPosition(targetRange.start);
|
||||
|
||||
const result: vscode.LocationLink = {
|
||||
originSelectionRange: toVsCodeRange(targetLink.range),
|
||||
originSelectionRange: new vscode.Range(
|
||||
targetLink.range.start.line,
|
||||
targetLink.range.start.character +
|
||||
(targetLink.type === 'wikilink' ? 2 : 0),
|
||||
targetLink.range.end.line,
|
||||
targetLink.range.end.character -
|
||||
(targetLink.type === 'wikilink' ? 2 : 0)
|
||||
),
|
||||
targetUri: toVsCodeUri(uri.asPlain()),
|
||||
targetRange: toVsCodeRange(targetRange),
|
||||
targetSelectionRange: toVsCodeRange(targetSelectionRange),
|
||||
|
||||
27
packages/foam-vscode/src/features/open-daily-note.spec.ts
Normal file
27
packages/foam-vscode/src/features/open-daily-note.spec.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import dateFormat from 'dateformat';
|
||||
import { commands, window } from 'vscode';
|
||||
|
||||
describe('Open daily note command', () => {
|
||||
it('offers to pick which template to use', async () => {
|
||||
const spy = jest
|
||||
.spyOn(window, 'showQuickPick')
|
||||
.mockImplementationOnce(jest.fn(() => Promise.resolve(undefined)));
|
||||
|
||||
await commands.executeCommand('foam-vscode.open-daily-note-for-date');
|
||||
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining([
|
||||
expect.objectContaining({
|
||||
label: expect.stringContaining(
|
||||
dateFormat(new Date(), 'mmm dd, yyyy')
|
||||
),
|
||||
}),
|
||||
]),
|
||||
{
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,36 @@
|
||||
import { ExtensionContext, commands } from 'vscode';
|
||||
import { ExtensionContext, commands, window, QuickPickItem } from 'vscode';
|
||||
import { FoamFeature } from '../types';
|
||||
import { openDailyNoteFor } from '../dated-notes';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
import { openDailyNoteFor } from '../dated-notes';
|
||||
import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { range } from 'lodash';
|
||||
import dateFormat from 'dateformat';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: (context: ExtensionContext) => {
|
||||
activate: (context: ExtensionContext, foamPromise) => {
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand('foam-vscode.open-daily-note', openDailyNoteFor)
|
||||
commands.registerCommand('foam-vscode.open-daily-note', () =>
|
||||
openDailyNoteFor(new Date())
|
||||
)
|
||||
);
|
||||
|
||||
context.subscriptions.push(
|
||||
commands.registerCommand(
|
||||
'foam-vscode.open-daily-note-for-date',
|
||||
async () => {
|
||||
const ws = (await foamPromise).workspace;
|
||||
const date = await window
|
||||
.showQuickPick<DateItem>(generateDateItems(ws), {
|
||||
placeHolder: 'Choose or type a date (YYYY-MM-DD)',
|
||||
matchOnDescription: true,
|
||||
matchOnDetail: true,
|
||||
})
|
||||
.then(item => {
|
||||
return item?.date;
|
||||
});
|
||||
return openDailyNoteFor(date);
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (getFoamVsCodeConfig('openDailyNote.onStartup', false)) {
|
||||
@@ -15,4 +39,45 @@ const feature: FoamFeature = {
|
||||
},
|
||||
};
|
||||
|
||||
class DateItem implements QuickPickItem {
|
||||
public label: string;
|
||||
public detail: string;
|
||||
public description: string;
|
||||
public alwaysShow?: boolean;
|
||||
constructor(public date: Date, offset: number, public exists: boolean) {
|
||||
const icon = exists ? '$(calendar)' : '$(new-file)';
|
||||
this.label = `${icon} ${dateFormat(date, 'mmm dd, yyyy')}`;
|
||||
this.detail = dateFormat(date, 'dddd');
|
||||
if (offset === 0) {
|
||||
this.detail = 'Today';
|
||||
} else if (offset === -1) {
|
||||
this.detail = 'Yesterday';
|
||||
} else if (offset === 1) {
|
||||
this.detail = 'Tomorrow';
|
||||
} else if (offset > -8 && offset < -1) {
|
||||
this.detail = `Last ${dateFormat(date, 'dddd')}`;
|
||||
} else if (offset > 1 && offset < 8) {
|
||||
this.detail = `Next ${dateFormat(date, 'dddd')}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function generateDateItems(ws: FoamWorkspace): DateItem[] {
|
||||
const items = [
|
||||
...range(0, 32), // next month
|
||||
...range(-31, 0), // last month
|
||||
].map(offset => {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + offset);
|
||||
// TODO this is only compatible with default settings as it would
|
||||
// be otherwise hard to "guess" the daily note path
|
||||
// Ideally we would read the daily note path from the config or template to properly match
|
||||
const noteBasename = dateFormat(date, 'yyyy-mm-dd', false);
|
||||
const exists = ws.find(noteBasename) ? true : false;
|
||||
return new DateItem(date, offset, exists);
|
||||
});
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
export default feature;
|
||||
|
||||
@@ -6,11 +6,14 @@ import {
|
||||
createFile,
|
||||
deleteFile,
|
||||
getUriInWorkspace,
|
||||
withModifiedFoamConfiguration,
|
||||
} from '../test/test-utils-vscode';
|
||||
import {
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithFoamTags,
|
||||
markdownItWithNoteInclusion,
|
||||
markdownItWithRemoveLinkReferences,
|
||||
} from './preview-navigation';
|
||||
|
||||
describe('Link generation in preview', () => {
|
||||
@@ -22,7 +25,11 @@ describe('Link generation in preview', () => {
|
||||
links: [{ slug: 'placeholder' }],
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA);
|
||||
const md = markdownItWithFoamLinks(MarkdownIt(), ws);
|
||||
|
||||
const md = [
|
||||
markdownItWithFoamLinks,
|
||||
markdownItWithRemoveLinkReferences,
|
||||
].reduce((acc, extension) => extension(acc, ws), MarkdownIt());
|
||||
|
||||
it('generates a link to a note', () => {
|
||||
expect(md.render(`[[note-a]]`)).toEqual(
|
||||
@@ -41,6 +48,14 @@ describe('Link generation in preview', () => {
|
||||
`<p><a class='foam-placeholder-link' title="Link to non-existing resource" href="javascript:void(0);">random-text</a></p>\n`
|
||||
);
|
||||
});
|
||||
|
||||
it('generates a wikilink even when there is a link reference', () => {
|
||||
const note = `[[note-a]]
|
||||
[note-a]: <note-a.md> "Note A"`;
|
||||
expect(md.render(note)).toEqual(
|
||||
`<p><a class='foam-note-link' title='${noteA.title}' href='/path/to/note-a.md' data-href='/path/to/note-a.md'>note-a</a>\n[note-a]: <note-a.md> "Note A"</p>\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stylable tag generation in preview', () => {
|
||||
@@ -60,22 +75,48 @@ describe('Stylable tag generation in preview', () => {
|
||||
});
|
||||
|
||||
describe('Displaying included notes in preview', () => {
|
||||
it('should render an included note', () => {
|
||||
it('should render an included note in flat mode', async () => {
|
||||
const note = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(note);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-a]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><p>This is the text of note A</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('should render an included note in container mode', async () => {
|
||||
const note = createTestNote({
|
||||
uri: 'note-a.md',
|
||||
text: 'This is the text of note A',
|
||||
});
|
||||
const ws = new FoamWorkspace().set(note);
|
||||
await await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
true,
|
||||
() => {
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
const res = md.render(`This is the root node. ![[note-a]]`);
|
||||
expect(res).toContain('This is the root node');
|
||||
expect(res).toContain('embed-container-note');
|
||||
expect(res).toContain('This is the text of note A');
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
@@ -99,15 +140,21 @@ This is the third section of note D
|
||||
const ws = new FoamWorkspace().set(parser.parse(note.uri, note.content));
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
await withModifiedFoamConfiguration(
|
||||
CONFIG_EMBED_NOTE_IN_CONTAINER,
|
||||
false,
|
||||
() => {
|
||||
expect(
|
||||
md.render(`This is the root node.
|
||||
|
||||
![[note-e#Section 2]]`)
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
).toMatch(
|
||||
`<p>This is the root node.</p>
|
||||
<p><h1>Section 2</h1>
|
||||
<p>This is the second section of note D</p>
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
await deleteFile(note);
|
||||
@@ -132,12 +179,10 @@ This is the third section of note D
|
||||
});
|
||||
const ws = new FoamWorkspace().set(noteA).set(noteB);
|
||||
const md = markdownItWithNoteInclusion(MarkdownIt(), ws);
|
||||
const res = md.render(noteB.source.text);
|
||||
|
||||
expect(md.render(noteB.source.text)).toMatch(
|
||||
`<p>This is the text of note B which includes <p>This is the text of note A which includes <p>This is the text of note B which includes <div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: note-a</div></p>
|
||||
</p>
|
||||
</p>
|
||||
`
|
||||
);
|
||||
expect(res).toContain('This is the text of note B which includes');
|
||||
expect(res).toContain('This is the text of note A which includes');
|
||||
expect(res).toContain('Cyclic link detected for wikilink: note-a');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,9 +7,10 @@ import { FoamWorkspace } from '../core/model/workspace';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { Resource } from '../core/model/note';
|
||||
|
||||
const ALIAS_DIVIDER_CHAR = '|';
|
||||
const refsStack: string[] = [];
|
||||
import { MarkdownLink } from '../core/services/markdown-link';
|
||||
import { Range } from '../core/model/range';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { getFoamVsCodeConfig } from '../services/config';
|
||||
|
||||
const feature: FoamFeature = {
|
||||
activate: async (
|
||||
@@ -31,6 +32,8 @@ const feature: FoamFeature = {
|
||||
},
|
||||
};
|
||||
|
||||
export const CONFIG_EMBED_NOTE_IN_CONTAINER = 'preview.embedNoteInContainer';
|
||||
const refsStack: string[] = [];
|
||||
export const markdownItWithNoteInclusion = (
|
||||
md: markdownit,
|
||||
workspace: FoamWorkspace
|
||||
@@ -56,22 +59,44 @@ export const markdownItWithNoteInclusion = (
|
||||
|
||||
if (cyclicLinkDetected) {
|
||||
return `<div class="foam-cyclic-link-warning">Cyclic link detected for wikilink: ${wikilink}</div>`;
|
||||
} else {
|
||||
let content = includedNote.source.text;
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
}
|
||||
let content = `Embed for [[${wikilink}]]`;
|
||||
switch (includedNote.type) {
|
||||
case 'note':
|
||||
content = getFoamVsCodeConfig(CONFIG_EMBED_NOTE_IN_CONTAINER)
|
||||
? `<div class="embed-container-note">${md.render(
|
||||
includedNote.source.text
|
||||
)}</div>`
|
||||
: includedNote.source.text;
|
||||
break;
|
||||
case 'attachment':
|
||||
content = `
|
||||
<div class="embed-container-attachment">
|
||||
${md.renderInline('[[' + wikilink + ']]')}<br/>
|
||||
Embed for attachments is not supported
|
||||
</div>`;
|
||||
break;
|
||||
case 'image':
|
||||
content = `<div class="embed-container-image">${md.render(
|
||||
`
|
||||
)})`
|
||||
)}</div>`;
|
||||
break;
|
||||
}
|
||||
const section = Resource.findSection(
|
||||
includedNote,
|
||||
includedNote.uri.fragment
|
||||
);
|
||||
if (isSome(section)) {
|
||||
const rows = content.split('\n');
|
||||
content = rows
|
||||
.slice(section.range.start.line, section.range.end.line)
|
||||
.join('\n');
|
||||
}
|
||||
const html = md.render(content);
|
||||
refsStack.pop();
|
||||
return html;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while including [[${wikilink}]] into the current document of the Preview panel`,
|
||||
@@ -92,22 +117,20 @@ export const markdownItWithFoamLinks = (
|
||||
regex: /\[\[([^[\]]+?)\]\]/,
|
||||
replace: (wikilink: string) => {
|
||||
try {
|
||||
const linkHasAlias = wikilink.includes(ALIAS_DIVIDER_CHAR);
|
||||
const resourceLink = linkHasAlias
|
||||
? wikilink.substring(0, wikilink.indexOf('|'))
|
||||
: wikilink;
|
||||
const { target, alias } = MarkdownLink.analyzeLink({
|
||||
rawText: '[[' + wikilink + ']]',
|
||||
type: 'wikilink',
|
||||
range: Range.create(0, 0),
|
||||
});
|
||||
const label = isEmpty(alias) ? target : alias;
|
||||
|
||||
const resource = workspace.find(resourceLink);
|
||||
const resource = workspace.find(target);
|
||||
if (isNone(resource)) {
|
||||
return getPlaceholderLink(resourceLink);
|
||||
return getPlaceholderLink(label);
|
||||
}
|
||||
|
||||
const linkLabel = linkHasAlias
|
||||
? wikilink.substr(wikilink.indexOf('|') + 1)
|
||||
: wikilink;
|
||||
|
||||
const link = vscode.workspace.asRelativePath(toVsCodeUri(resource.uri));
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='/${link}' data-href='/${link}'>${linkLabel}</a>`;
|
||||
return `<a class='foam-note-link' title='${resource.title}' href='/${link}' data-href='/${link}'>${label}</a>`;
|
||||
} catch (e) {
|
||||
Logger.error(
|
||||
`Error while creating link for [[${wikilink}]] in Preview panel`,
|
||||
@@ -155,18 +178,17 @@ export const markdownItWithRemoveLinkReferences = (
|
||||
) => {
|
||||
md.inline.ruler.before('link', 'clear-references', state => {
|
||||
if (state.env.references) {
|
||||
Object.keys(state.env.references).forEach(refKey => {
|
||||
// Forget about reference links that contain an alias divider
|
||||
// Aliased reference links will lead the MarkdownParser to include wrong link references
|
||||
if (refKey.includes(ALIAS_DIVIDER_CHAR)) {
|
||||
delete state.env.references[refKey];
|
||||
}
|
||||
const src = state.src.toLowerCase();
|
||||
const foamLinkRegEx = /\[\[([^[\]]+?)\]\]/g;
|
||||
const foamLinks = [...src.matchAll(foamLinkRegEx)].map(m =>
|
||||
m[1].toLowerCase()
|
||||
);
|
||||
|
||||
// When the reference is present due to an inclusion of that note, we
|
||||
// need to remove that reference. This ensures the MarkdownIt parser
|
||||
// will not replace the wikilink syntax with an <a href> link and as a result
|
||||
// break our inclusion logic.
|
||||
if (state.src.toLowerCase().includes(`![[${refKey.toLowerCase()}]]`)) {
|
||||
Object.keys(state.env.references).forEach(refKey => {
|
||||
// Remove all references that have corresponding wikilinks.
|
||||
// If the markdown parser sees a reference, it will format it before
|
||||
// we get a chance to create the wikilink.
|
||||
if (foamLinks.includes(refKey.toLowerCase())) {
|
||||
delete state.env.references[refKey];
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createTestNote } from '../test/test-utils';
|
||||
import { createTestNote, readFileFromFs } from '../test/test-utils';
|
||||
import { cleanWorkspace, closeEditors } from '../test/test-utils-vscode';
|
||||
import { TagItem, TagReference, TagsProvider } from './tags-tree-view';
|
||||
import { bootstrap, Foam } from '../core/model/foam';
|
||||
@@ -9,8 +9,9 @@ describe('Tags tree panel', () => {
|
||||
let _foam: Foam;
|
||||
let provider: TagsProvider;
|
||||
|
||||
const dataStore = new FileDataStore(readFileFromFs);
|
||||
const matcher = new Matcher([]);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher);
|
||||
const mdProvider = new MarkdownResourceProvider(matcher, dataStore);
|
||||
|
||||
beforeAll(async () => {
|
||||
await cleanWorkspace();
|
||||
@@ -22,7 +23,7 @@ describe('Tags tree panel', () => {
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
_foam = await bootstrap(matcher, new FileDataStore(), [mdProvider]);
|
||||
_foam = await bootstrap(matcher, dataStore, [mdProvider]);
|
||||
provider = new TagsProvider(_foam, _foam.workspace);
|
||||
await closeEditors();
|
||||
});
|
||||
|
||||
@@ -164,16 +164,14 @@ export class TagReference extends vscode.TreeItem {
|
||||
public readonly title: string;
|
||||
constructor(public readonly tag: Tag, public readonly note: Resource) {
|
||||
super(note.title, vscode.TreeItemCollapsibleState.None);
|
||||
const uri = toVsCodeUri(note.uri);
|
||||
this.title = note.title;
|
||||
this.description = note.uri.path.replace(
|
||||
vscode.workspace.getWorkspaceFolder(toVsCodeUri(note.uri))?.uri.path,
|
||||
''
|
||||
);
|
||||
this.description = vscode.workspace.asRelativePath(uri);
|
||||
this.tooltip = undefined;
|
||||
this.command = {
|
||||
command: 'vscode.open',
|
||||
arguments: [
|
||||
note.uri,
|
||||
uri,
|
||||
{
|
||||
preview: true,
|
||||
selection: toVsCodeRange(tag.range),
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { URI } from '../core/model/uri';
|
||||
import { existsSync } from 'fs';
|
||||
import { TextEncoder } from 'util';
|
||||
import { SnippetString, ViewColumn, window, workspace } from 'vscode';
|
||||
import { FileType, SnippetString, ViewColumn, window, workspace } from 'vscode';
|
||||
import { focusNote } from '../utils';
|
||||
import { fromVsCodeUri, toVsCodeUri } from '../utils/vsc-utils';
|
||||
import { extractFoamTemplateFrontmatterMetadata } from '../utils/template-frontmatter-parser';
|
||||
@@ -75,7 +74,7 @@ export async function getTemplateInfo(
|
||||
templateFallbackText = '',
|
||||
resolver: Resolver
|
||||
) {
|
||||
const templateText = existsSync(templateUri.toFsPath())
|
||||
const templateText = (await fileExists(templateUri))
|
||||
? await workspace.fs
|
||||
.readFile(toVsCodeUri(templateUri))
|
||||
.then(bytes => bytes.toString())
|
||||
@@ -137,7 +136,7 @@ export const NoteFactory = {
|
||||
filepathFallbackURI,
|
||||
resolver
|
||||
);
|
||||
while (existsSync(newFilePath.toFsPath())) {
|
||||
while (await fileExists(newFilePath)) {
|
||||
const proposedNewFilepath = await onFileExists(newFilePath);
|
||||
|
||||
if (proposedNewFilepath === undefined) {
|
||||
@@ -224,10 +223,10 @@ export const createTemplate = async (): Promise<void> => {
|
||||
prompt: `Enter the filename for the new template`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
validateInput: async value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
: (await fileExists(URI.parse(value)))
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
@@ -252,10 +251,10 @@ async function askUserForFilepathConfirmation(
|
||||
prompt: `Enter the filename for the new note`,
|
||||
value: fsPath,
|
||||
valueSelection: [fsPath.length - defaultFilename.length, fsPath.length - 3],
|
||||
validateInput: value =>
|
||||
validateInput: async value =>
|
||||
value.trim().length === 0
|
||||
? 'Please enter a value'
|
||||
: existsSync(value)
|
||||
: (await fileExists(URI.parse(value)))
|
||||
? 'File already exists'
|
||||
: undefined,
|
||||
});
|
||||
@@ -286,3 +285,12 @@ export async function determineNewNoteFilepath(
|
||||
);
|
||||
return defaultFilepath;
|
||||
}
|
||||
|
||||
async function fileExists(uri: URI): Promise<boolean> {
|
||||
try {
|
||||
const stat = await workspace.fs.stat(toVsCodeUri(uri));
|
||||
return stat.type === FileType.File;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,3 +89,29 @@ export const createNote = (r: Resource) => {
|
||||
new TextEncoder().encode(content)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a function with a modified configuration and
|
||||
* restores the original configuration afterwards
|
||||
*
|
||||
* @param key the key of the configuration to modify
|
||||
* @param value the value to set the configuration to
|
||||
* @param fn the function to execute
|
||||
*/
|
||||
export const withModifiedConfiguration = async (key, value, fn: () => void) => {
|
||||
const old = vscode.workspace.getConfiguration().get(key);
|
||||
await vscode.workspace.getConfiguration().update(key, value);
|
||||
await fn();
|
||||
await vscode.workspace.getConfiguration().update(key, old);
|
||||
};
|
||||
|
||||
/**
|
||||
* Runs a function with a modified Foam configuration and
|
||||
* restores the original configuration afterwards
|
||||
*
|
||||
* @param key the key of the Foam configuration to modify
|
||||
* @param value the value to set the configuration to
|
||||
* @param fn the function to execute
|
||||
*/
|
||||
export const withModifiedFoamConfiguration = (key, value, fn: () => void) =>
|
||||
withModifiedConfiguration(`foam.${key}`, value, fn);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/*
|
||||
* This file should not depend on VS Code as it's used for unit tests
|
||||
*/
|
||||
import fs from 'fs';
|
||||
import { Logger } from '../core/utils/log';
|
||||
import { Range } from '../core/model/range';
|
||||
import { URI } from '../core/model/uri';
|
||||
@@ -35,7 +36,7 @@ export const strToUri = URI.file;
|
||||
export const createTestWorkspace = () => {
|
||||
const workspace = new FoamWorkspace();
|
||||
const matcher = new Matcher([URI.file('/')], ['**/*']);
|
||||
const provider = new MarkdownResourceProvider(matcher, undefined, undefined, {
|
||||
const provider = new MarkdownResourceProvider(matcher, {
|
||||
read: _ => Promise.resolve(''),
|
||||
list: _ => Promise.resolve([]),
|
||||
});
|
||||
@@ -49,6 +50,7 @@ export const createTestNote = (params: {
|
||||
definitions?: NoteLinkDefinition[];
|
||||
links?: Array<{ slug: string } | { to: string }>;
|
||||
tags?: string[];
|
||||
aliases?: string[];
|
||||
text?: string;
|
||||
sections?: string[];
|
||||
root?: URI;
|
||||
@@ -69,6 +71,11 @@ export const createTestNote = (params: {
|
||||
label: t,
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
})) ?? [],
|
||||
aliases:
|
||||
params.aliases?.map(a => ({
|
||||
title: a,
|
||||
range: Range.create(0, 0, 0, 0),
|
||||
})) ?? [],
|
||||
links: params.links
|
||||
? params.links.map((link, index) => {
|
||||
const range = Range.create(
|
||||
@@ -111,3 +118,7 @@ export const randomString = (len = 5) =>
|
||||
|
||||
export const getRandomURI = () =>
|
||||
URI.file('/random-uri-root/' + randomString() + '.md');
|
||||
|
||||
/** Use fs for reading files in units where vscode.workspace is unavailable */
|
||||
export const readFileFromFs = async (uri: URI) =>
|
||||
(await fs.promises.readFile(uri.toFsPath())).toString();
|
||||
|
||||
@@ -16,3 +16,22 @@
|
||||
background-color: var(--vscode-editorError-background);
|
||||
color: var(--vscode-editorError-foreground);
|
||||
}
|
||||
|
||||
.embed-container-note {
|
||||
padding: 0.5em;
|
||||
margin: 1.5em 0;
|
||||
border: 1px solid var(--vscode-editorLineNumber-foreground);
|
||||
}
|
||||
|
||||
.embed-container-attachment {
|
||||
padding: 0.25em;
|
||||
margin: 1.5em 0;
|
||||
text-align: center;
|
||||
border: 1px solid var(--vscode-editorLineNumber-foreground);
|
||||
}
|
||||
|
||||
.embed-container-image {
|
||||
margin: auto;
|
||||
padding: 0.25em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
{
|
||||
"scopeName": "foam.wikilink.injection",
|
||||
"injectionSelector": "L:meta.paragraph.markdown",
|
||||
"injectionSelector": "L:meta.paragraph.markdown, L:markup.heading.markdown",
|
||||
"patterns": [
|
||||
{
|
||||
"contentName": "string.other.link.title.markdown.foam",
|
||||
"begin": "\\[\\[",
|
||||
"end": "\\]\\]"
|
||||
"beginCaptures": {
|
||||
"0": { "name": "punctuation.definition.metadata.markdown.foam" }
|
||||
},
|
||||
"end": "\\]\\]",
|
||||
"endCaptures": {
|
||||
"0": { "name": "punctuation.definition.metadata.markdown.foam" }
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
18
readme.md
18
readme.md
@@ -5,7 +5,7 @@
|
||||
👀*This is an early stage project under rapid development. For updates join the [Foam community Discord](https://foambubble.github.io/join-discord/g)! 💬*
|
||||
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
|
||||
[](#contributors-)
|
||||
[](#contributors-)
|
||||
<!-- ALL-CONTRIBUTORS-BADGE:END -->
|
||||
|
||||
[](https://foambubble.github.io/join-discord/g)
|
||||
@@ -146,9 +146,10 @@ Whether you want to build a [Second Brain](https://www.buildingasecondbrain.com/
|
||||
|
||||
You can also use our Foam template:
|
||||
|
||||
1. [Create a GitHub repository from foam-template](https://github.com/foambubble/foam-template/generate). If you want to keep your thoughts to yourself, remember to set the repository private.
|
||||
2. Clone the repository and open it in VS Code.
|
||||
3. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).
|
||||
1. Log in on your GitHub account.
|
||||
2. [Create a GitHub repository from foam-template](https://github.com/foambubble/foam-template/generate). If you want to keep your thoughts to yourself, remember to set the repository private.
|
||||
3. Clone the repository and open it in VS Code.
|
||||
4. When prompted to install recommended extensions, click **Install all** (or **Show Recommendations** if you want to review and install them one by one).
|
||||
|
||||
This will also install `Foam`, but if you already have it installed, that's ok, just make sure you're up to date on the latest version.
|
||||
|
||||
@@ -315,6 +316,15 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://Cliffordfajardo.com"><img src="https://avatars.githubusercontent.com/u/6743796?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Clifford Fajardo </b></sub></a><br /><a href="#tool-cliffordfajardo" title="Tools">🔧</a></td>
|
||||
<td align="center"><a href="http://cu-dev.ca"><img src="https://avatars.githubusercontent.com/u/6589365?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Chris Usick</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=chrisUsick" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/josephdecock"><img src="https://avatars.githubusercontent.com/u/1145533?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Joe DeCock</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=josephdecock" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://www.drewtyler.com"><img src="https://avatars.githubusercontent.com/u/5640816?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Drew Tyler</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=drewtyler" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/Lauviah0622"><img src="https://avatars.githubusercontent.com/u/43416399?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Lauviah0622</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=Lauviah0622" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://www.elastic.co/elastic-agent"><img src="https://avatars.githubusercontent.com/u/1813008?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Josh Dover</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=joshdover" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://phelm.co.uk"><img src="https://avatars.githubusercontent.com/u/4057948?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Phil Helm</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=phelma" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/lingyv-li"><img src="https://avatars.githubusercontent.com/u/8937944?v=4?s=60" width="60px;" alt=""/><br /><sub><b>Larry Li</b></sub></a><br /><a href="https://github.com/foambubble/foam/commits?author=lingyv-li" title="Code">💻</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
126
yarn.lock
126
yarn.lock
@@ -4476,6 +4476,132 @@ es6-promisify@^5.0.0:
|
||||
dependencies:
|
||||
es6-promise "^4.0.3"
|
||||
|
||||
esbuild-android-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-64/-/esbuild-android-64-0.14.47.tgz#ef95b42c67bcf4268c869153fa3ad1466c4cea6b"
|
||||
integrity sha512-R13Bd9+tqLVFndncMHssZrPWe6/0Kpv2/dt4aA69soX4PRxlzsVpCvoJeFE8sOEoeVEiBkI0myjlkDodXlHa0g==
|
||||
|
||||
esbuild-android-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.47.tgz#4ebd7ce9fb250b4695faa3ee46fd3b0754ecd9e6"
|
||||
integrity sha512-OkwOjj7ts4lBp/TL6hdd8HftIzOy/pdtbrNA4+0oVWgGG64HrdVzAF5gxtJufAPOsEjkyh1oIYvKAUinKKQRSQ==
|
||||
|
||||
esbuild-darwin-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.47.tgz#e0da6c244f497192f951807f003f6a423ed23188"
|
||||
integrity sha512-R6oaW0y5/u6Eccti/TS6c/2c1xYTb1izwK3gajJwi4vIfNs1s8B1dQzI1UiC9T61YovOQVuePDcfqHLT3mUZJA==
|
||||
|
||||
esbuild-darwin-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.47.tgz#cd40fd49a672fca581ed202834239dfe540a9028"
|
||||
integrity sha512-seCmearlQyvdvM/noz1L9+qblC5vcBrhUaOoLEDDoLInF/VQ9IkobGiLlyTPYP5dW1YD4LXhtBgOyevoIHGGnw==
|
||||
|
||||
esbuild-freebsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.47.tgz#8da6a14c095b29c01fc8087a16cb7906debc2d67"
|
||||
integrity sha512-ZH8K2Q8/Ux5kXXvQMDsJcxvkIwut69KVrYQhza/ptkW50DC089bCVrJZZ3sKzIoOx+YPTrmsZvqeZERjyYrlvQ==
|
||||
|
||||
esbuild-freebsd-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.47.tgz#ad31f9c92817ff8f33fd253af7ab5122dc1b83f6"
|
||||
integrity sha512-ZJMQAJQsIOhn3XTm7MPQfCzEu5b9STNC+s90zMWe2afy9EwnHV7Ov7ohEMv2lyWlc2pjqLW8QJnz2r0KZmeAEQ==
|
||||
|
||||
esbuild-linux-32@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.47.tgz#de085e4db2e692ea30c71208ccc23fdcf5196c58"
|
||||
integrity sha512-FxZOCKoEDPRYvq300lsWCTv1kcHgiiZfNrPtEhFAiqD7QZaXrad8LxyJ8fXGcWzIFzRiYZVtB3ttvITBvAFhKw==
|
||||
|
||||
esbuild-linux-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.47.tgz#2a9321bbccb01f01b04cebfcfccbabeba3658ba1"
|
||||
integrity sha512-nFNOk9vWVfvWYF9YNYksZptgQAdstnDCMtR6m42l5Wfugbzu11VpMCY9XrD4yFxvPo9zmzcoUL/88y0lfJZJJw==
|
||||
|
||||
esbuild-linux-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.47.tgz#b9da7b6fc4b0ca7a13363a0c5b7bb927e4bc535a"
|
||||
integrity sha512-ywfme6HVrhWcevzmsufjd4iT3PxTfCX9HOdxA7Hd+/ZM23Y9nXeb+vG6AyA6jgq/JovkcqRHcL9XwRNpWG6XRw==
|
||||
|
||||
esbuild-linux-arm@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.47.tgz#56fec2a09b9561c337059d4af53625142aded853"
|
||||
integrity sha512-ZGE1Bqg/gPRXrBpgpvH81tQHpiaGxa8c9Rx/XOylkIl2ypLuOcawXEAo8ls+5DFCcRGt/o3sV+PzpAFZobOsmA==
|
||||
|
||||
esbuild-linux-mips64le@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.47.tgz#9db21561f8f22ed79ef2aedb7bbef082b46cf823"
|
||||
integrity sha512-mg3D8YndZ1LvUiEdDYR3OsmeyAew4MA/dvaEJxvyygahWmpv1SlEEnhEZlhPokjsUMfRagzsEF/d/2XF+kTQGg==
|
||||
|
||||
esbuild-linux-ppc64le@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.47.tgz#dc3a3da321222b11e96e50efafec9d2de408198b"
|
||||
integrity sha512-WER+f3+szmnZiWoK6AsrTKGoJoErG2LlauSmk73LEZFQ/iWC+KhhDsOkn1xBUpzXWsxN9THmQFltLoaFEH8F8w==
|
||||
|
||||
esbuild-linux-riscv64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.47.tgz#9bd6dcd3dca6c0357084ecd06e1d2d4bf105335f"
|
||||
integrity sha512-1fI6bP3A3rvI9BsaaXbMoaOjLE3lVkJtLxsgLHqlBhLlBVY7UqffWBvkrX/9zfPhhVMd9ZRFiaqXnB1T7BsL2g==
|
||||
|
||||
esbuild-linux-s390x@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.47.tgz#a458af939b52f2cd32fc561410d441a51f69d41f"
|
||||
integrity sha512-eZrWzy0xFAhki1CWRGnhsHVz7IlSKX6yT2tj2Eg8lhAwlRE5E96Hsb0M1mPSE1dHGpt1QVwwVivXIAacF/G6mw==
|
||||
|
||||
esbuild-netbsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.47.tgz#6388e785d7e7e4420cb01348d7483ab511b16aa8"
|
||||
integrity sha512-Qjdjr+KQQVH5Q2Q1r6HBYswFTToPpss3gqCiSw2Fpq/ua8+eXSQyAMG+UvULPqXceOwpnPo4smyZyHdlkcPppQ==
|
||||
|
||||
esbuild-openbsd-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.47.tgz#309af806db561aa886c445344d1aacab850dbdc5"
|
||||
integrity sha512-QpgN8ofL7B9z8g5zZqJE+eFvD1LehRlxr25PBkjyyasakm4599iroUpaj96rdqRlO2ShuyqwJdr+oNqWwTUmQw==
|
||||
|
||||
esbuild-sunos-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.47.tgz#3f19612dcdb89ba6c65283a7ff6e16f8afbf8aaa"
|
||||
integrity sha512-uOeSgLUwukLioAJOiGYm3kNl+1wJjgJA8R671GYgcPgCx7QR73zfvYqXFFcIO93/nBdIbt5hd8RItqbbf3HtAQ==
|
||||
|
||||
esbuild-windows-32@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.47.tgz#a92d279c8458d5dc319abcfeb30aa49e8f2e6f7f"
|
||||
integrity sha512-H0fWsLTp2WBfKLBgwYT4OTfFly4Im/8B5f3ojDv1Kx//kiubVY0IQunP2Koc/fr/0wI7hj3IiBDbSrmKlrNgLQ==
|
||||
|
||||
esbuild-windows-64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.47.tgz#2564c3fcf0c23d701edb71af8c52d3be4cec5f8a"
|
||||
integrity sha512-/Pk5jIEH34T68r8PweKRi77W49KwanZ8X6lr3vDAtOlH5EumPE4pBHqkCUdELanvsT14yMXLQ/C/8XPi1pAtkQ==
|
||||
|
||||
esbuild-windows-arm64@0.14.47:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.47.tgz#86d9db1a22d83360f726ac5fba41c2f625db6878"
|
||||
integrity sha512-HFSW2lnp62fl86/qPQlqw6asIwCnEsEoNIL1h2uVMgakddf+vUuMcCbtUY1i8sst7KkgHrVKCJQB33YhhOweCQ==
|
||||
|
||||
esbuild@^0.14.45:
|
||||
version "0.14.47"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.47.tgz#0d6415f6bd8eb9e73a58f7f9ae04c5276cda0e4d"
|
||||
integrity sha512-wI4ZiIfFxpkuxB8ju4MHrGwGLyp1+awEHAHVpx6w7a+1pmYIq8T9FGEVVwFo0iFierDoMj++Xq69GXWYn2EiwA==
|
||||
optionalDependencies:
|
||||
esbuild-android-64 "0.14.47"
|
||||
esbuild-android-arm64 "0.14.47"
|
||||
esbuild-darwin-64 "0.14.47"
|
||||
esbuild-darwin-arm64 "0.14.47"
|
||||
esbuild-freebsd-64 "0.14.47"
|
||||
esbuild-freebsd-arm64 "0.14.47"
|
||||
esbuild-linux-32 "0.14.47"
|
||||
esbuild-linux-64 "0.14.47"
|
||||
esbuild-linux-arm "0.14.47"
|
||||
esbuild-linux-arm64 "0.14.47"
|
||||
esbuild-linux-mips64le "0.14.47"
|
||||
esbuild-linux-ppc64le "0.14.47"
|
||||
esbuild-linux-riscv64 "0.14.47"
|
||||
esbuild-linux-s390x "0.14.47"
|
||||
esbuild-netbsd-64 "0.14.47"
|
||||
esbuild-openbsd-64 "0.14.47"
|
||||
esbuild-sunos-64 "0.14.47"
|
||||
esbuild-windows-32 "0.14.47"
|
||||
esbuild-windows-64 "0.14.47"
|
||||
esbuild-windows-arm64 "0.14.47"
|
||||
|
||||
escalade@^3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
|
||||
Reference in New Issue
Block a user